commit 2f2f0217d8a3de719cea980384804c315fa9f348
Author: Jan Pokorny <jpokorny(a)redhat.com>
Date: Fri Aug 20 19:30:50 2010 +0200
Fix rhbz#536841: Ability to change number of votes
luci/controllers/cluster.py | 21 +++++-
luci/controllers/root.py | 7 ++-
luci/lib/ClusterConf/Cluster.py | 28 +++++++-
luci/lib/flash2.py | 126 +++++++++++++++++++++++++++++++++++++++
luci/public/css/node.css | 10 +++
luci/public/css/style.css | 44 +++++++++-----
luci/templates/configure.html | 25 +++++---
luci/templates/master.html | 13 ++++-
luci/templates/node.html | 30 ++++++++-
9 files changed, 263 insertions(+), 41 deletions(-)
---
diff --git a/luci/controllers/cluster.py b/luci/controllers/cluster.py
index 86bc1e0..166b11a 100644
--- a/luci/controllers/cluster.py
+++ b/luci/controllers/cluster.py
@@ -19,6 +19,7 @@ from pylons.i18n import ugettext as _
from luci.lib.base import BaseController
from luci.lib.db_helpers import get_cluster_list, get_model_for_cluster,
get_status_for_cluster, db_remove_cluster, get_agent_for_cluster, get_cluster_db_obj
import luci.lib.ricci_helpers as rh
+from luci.lib.flash2 import flash2
import luci.widget_validators.validate_cluster_prop as vcp
from luci.widget_validators.validate_create_cluster_form import
validate_create_cluster_form, validate_node_add_form
from luci.widget_validators.validate_fence import validateNewFenceDevice,
validateFenceDevice
@@ -303,7 +304,7 @@ class IndividualClusterController(BaseController):
rh.update_cluster_conf(self.model)
redirect('%s%s' % (tmpl_context.cluster_url, kw.get('node')))
-
+
# This processes all of the commands that we can apply to a node
@expose("luci.templates.node")
def nodes_cmd(self, command=None, **kw):
@@ -351,6 +352,18 @@ class IndividualClusterController(BaseController):
flash(_("Deleting nodes: %s") % ', '.join(cur_list),
status='info')
rh.cluster_node_delete(self.name, self.model, cur_list)
+ elif command == 'Update Attributes':
+ node = self.model.retrieveNodeByName(cur_list[0])
+ votes = kw.get('votes', '1')
+ node.addAttribute('votes', votes)
+ log.info('User "%s" changed properties for node "%s"
from cluster "%s"'
+ % (self.username, cur_list[0], self.name))
+ flash2.info(_('Updating properties of node: %s') % cur_list[0])
+ if int(votes) > 1:
+ flash2.warning(_('Warning: Setting number of votes greater than 1 for
any node within cluster will disable Quorum Disk functionality.'))
+ flash2.flush()
+ self.model.setModified(True)
+ rh.update_cluster_conf(self.model)
else:
log.error('User "%s" submitted unknown command "%s"
for nodes "%s" from cluster "%s"' % (self.username, command,
', '.join(cur_list), self.name))
flash(_('An unknown command "%s" was given for nodes
"%s"')
@@ -373,7 +386,7 @@ class IndividualClusterController(BaseController):
def resources_cmd(self, command=None, **kw):
self.get_model()
tmpl_context.cluster_url = '/cluster/%s/resources' % self.name
-
+
if not self.model:
flash(_('Unable to contact any nodes in this cluster'),
status="error")
@@ -481,7 +494,7 @@ class IndividualClusterController(BaseController):
flash(_('Unable to contact any nodes in this cluster'),
status="error")
redirect(tmpl_context.cluster_url)
-
+
preferred_node = kw.get('preferred_node')
cur_list = []
@@ -612,7 +625,7 @@ class IndividualClusterController(BaseController):
flash(_('The settings of exactly one failover domain may be updated at
one time'),
status='error')
redirect(tmpl_context.cluster_url)
-
+
if command == 'update_settings':
vret = vcp.validate_fdom_prop_settings_form(self.model, **kw)
if vret[0] is True:
diff --git a/luci/controllers/root.py b/luci/controllers/root.py
index 1d11cd4..82e8810 100644
--- a/luci/controllers/root.py
+++ b/luci/controllers/root.py
@@ -6,6 +6,7 @@ from pylons.i18n import ugettext as _
from repoze.what import predicates
from luci.lib.base import BaseController
+from luci.lib.flash2 import Flash2, flash2
from luci.controllers.error import ErrorController
from luci.controllers.cluster import ClusterController
@@ -37,6 +38,10 @@ class RootController(BaseController):
def __call__(self, environ, start_response):
"""Invoke the Controller"""
+
+ # Register flash2 component for thread/request-local use.
+ environ['paste.registry'].register(flash2, Flash2())
+
tmpl_context.title_list = []
tmpl_context.already_used = dict()
tmpl_context.already_used.setdefault(False)
@@ -49,7 +54,7 @@ class RootController(BaseController):
async = AsyncController()
# METHODS OF THE ROOT CONTROLLER
-
+
@expose('luci.templates.index')
def index(self):
"""Handle the front-page."""
diff --git a/luci/lib/ClusterConf/Cluster.py b/luci/lib/ClusterConf/Cluster.py
index 17c1e66..ecba069 100644
--- a/luci/lib/ClusterConf/Cluster.py
+++ b/luci/lib/ClusterConf/Cluster.py
@@ -35,12 +35,32 @@ class Cluster(TagObject):
#self.is_cfg_version_dirty = True
def doesClusterUseQuorumDisk(self):
+ """Returns a pair (A, B) describing Quorum Disk situation within
cluster.
+
+ A ... whether 'quorumd' xml-node present in cluster.conf
+ B ... whether all nodes within cluster have either 'votes' attribute equal to
'1'
+ or no such attribute (which defaults to '1' automatically)
+
+ """
+ clusternodes_ptr = None
+ qdisk_found = False
kids = self.getChildren()
for kid in kids:
- if kid.getTagName().strip() == "quorumd":
- return True
-
- return False
+ kid_tag_name = kid.getTagName()
+ qdisk_found = qdisk_found or (kid_tag_name == "quorumd")
+ if kid_tag_name == "clusternodes":
+ clusternodes_ptr = kid
+ if qdisk_found:
+ break
+ if clusternodes_ptr == None:
+ return qdisk_found, True
+ else:
+ qdisk_supported = True
+ for kid in clusternodes_ptr.getChildren():
+ if kid.getTagName() == 'clusternode' \
+ and kid.getAttribute('votes') != None:
+ qdisk_supported = qdisk_supported and kid.getAttribute('votes')
== '1'
+ return qdisk_found, qdisk_supported
def getQuorumdPtr(self):
kids = self.getChildren()
diff --git a/luci/lib/flash2.py b/luci/lib/flash2.py
new file mode 100644
index 0000000..777c58f
--- /dev/null
+++ b/luci/lib/flash2.py
@@ -0,0 +1,126 @@
+# Original published by Ansel at
+#
http://groups.google.com/group/turbogears/browse_thread/thread/c86f1c18d6...
+# Slighly edited to fit current version of TG2 and project requirements.
+
+from tg import session
+from paste.registry import StackedObjectProxy
+
+import logging
+log = logging.getLogger(__name__)
+
+
+__all__ = ['Flash2', 'flash2']
+
+class _Flash2Message(object):
+ def __init__(self, msg, cls='info', html=False, hideable=False):
+ self.message = msg
+ self.css = cls
+ self.html = html
+ self.hideable = hideable
+ self.hash = id(self)
+
+ def __repr__(self):
+ return "<_Flash2Message: %s>" % self.__str__()
+
+ def __str__(self):
+ return "%s [%s]" % (self.message, self.css)
+
+
+class Flash2:
+ """A more advanced replacement for TurboGears2 built-in
``flash'' functionality.
+
+ It is intended to use this component in a local/request-local manner
+ (singleton object of this class) which can be reached this way:
+
+ 1. Register instance of this class together with 'flash2' proxy object
+ exported by this module with paste.registry object in your framework
+ (it is supposed that this middleware is plugged-in0.
+ For TurboGears2, you can use following in your root controller:
+
+ from luci.lib.flash2 import flash2, Flash2
+ class RootController(BaseController):
+ def __call__(self, environ, start_response):
+ environ['paste.registry'].register(flash2, Flash2())
+ # you code
+
+ 2. In the module/controller where you want to use the instance of this class,
+ just import 'flash2' proxy object exported by this module. Then, simply
+ use flash2 as if it was directly the instance of this class.
+
+ Example usage:
+ flash2.info('This is info.')
+ flash2.warning('This is warning.')
+ flash2.flush()
+
+ Note the same can be achieved by:
+ flash2.info('This is info.').warning('This is warning').flush()
+
+ As shown, you have always use 'flush' method in order to store the state
+ of flash messages to session's data which are read on the templates side
+ afterwards and displayed on respective page. During (the first) iteration
+ over the instance of this class, all messages are consequently popped
+ making this instance messages-free. That's why this iteration is intended
+ way of use on the templates side.
+
+ """
+ def __init__(self):
+ log.info("Creating _Flash2, id = %d" % id(self))
+ self.__messages = []
+ #self.__messages_dict = {'info':[], 'warning':[],
'error':[], 'ok': []}
+
+ def __iter__(self):
+ class DeletingIterator:
+ def __init__(self, messages):
+ self.__messages = messages
+ def __iter__(self):
+ return self
+ def next(self):
+ try:
+ # Note: self.__messages_dict should be handled if used.
+ item = self.__messages.pop(0)
+ return item
+ except IndexError:
+ raise StopIteration
+ return DeletingIterator(self.__messages)
+
+ def __del__(self):
+ log.info("Deleting _Flash2, id = %d", id(self))
+
+ def __repr__(self):
+ return "<Flash2 with messages: %s>" % ",
".join(map(lambda m: str(m), self.get_messages()))
+
+ def __add_message(self, msg, cls, html=False, hideable=False):
+ m = _Flash2Message(msg, cls, html, hideable)
+ self.__messages.append(m)
+ #if cls not in self.__messages_dict:
+ # self.__messages_dict[cls] = []
+ #self.__messages_dict[cls].append(m)
+ return self
+
+ def flush(self):
+ session['flash2'] = self
+ session.save()
+
+ def get_messages(self):
+ return self.__messages[:]
+
+ #def get_messages_dict(self)
+ # return self.__messages_dict.copy()
+
+ #---
+
+ def info(self, msg, html=False, hideable=True):
+ return self.__add_message(msg, 'info', html, hideable)
+
+ def warning(self, msg, html=False, hideable=True):
+ return self.__add_message(msg, 'warning', html, hideable)
+
+ def error(self, msg, html=False, hideable=False):
+ return self.__add_message(msg, 'error', html, hideable)
+
+ def ok(self, msg, html=False, hideable=True):
+ return self.__add_message(msg, 'ok', html, hideable)
+
+
+# Proxy object as mentioned in Flash2 class doc.
+flash2 = StackedObjectProxy()
diff --git a/luci/public/css/node.css b/luci/public/css/node.css
index 9062ede..9c4e049 100644
--- a/luci/public/css/node.css
+++ b/luci/public/css/node.css
@@ -19,6 +19,16 @@
/* details */
+/* attributes setting */
+
+#node_tattr {
+ margin: 0 15em 0 0;
+}
+
+.node_tattr_input {
+ padding: 8px 0 8px 5px;
+}
+
/* list of services */
#node_tservices tr {
diff --git a/luci/public/css/style.css b/luci/public/css/style.css
index f0590cb..9143260 100644
--- a/luci/public/css/style.css
+++ b/luci/public/css/style.css
@@ -68,7 +68,7 @@ ul.mainmenu li {
}
ul.mainmenu li a {
- background:transparent url(../images/breadcrumb_separator_0.png) no-repeat scroll 100%
50%;
+ background:transparent url(../images/breadcrumb_separator_0.png) no-repeat scroll 100%
50%;
color: #a4c9dd;
float: left;
display: block;
@@ -162,7 +162,7 @@ ul.headermenu li {
ul.headermenu li a {
color:#fff;
- text-decoration: none;
+ text-decoration: none;
}
#wrapper {
@@ -280,7 +280,7 @@ form>input.text, input.text {
padding: 3px;
border:1px solid #CAD0D4;
font-size: 12px;
- font-family:"Droid Sans","Lucida Grande","Lucida Sans
Unicode",geneva,verdana,sans-serif;
+ font-family:"Droid Sans","Lucida Grande","Lucida Sans
Unicode",geneva,verdana,sans-serif;
}
input.checkbox, input.radio {
@@ -302,7 +302,7 @@ input.checkbox, input.radio {
font-weight: bold;
padding: .5em 2em .55em;
text-shadow: 0 1px 1px rgba(0,0,0,.3);
- -webkit-border-radius: .5em;
+ -webkit-border-radius: .5em;
-moz-border-radius: .5em;
border-radius: .5em;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2);
@@ -398,7 +398,7 @@ input.checkbox, input.radio {
}
table.clusternodes, table#clusternodes, table#addclusternode, table#qdisk_heuristics {
- width: auto;
+ width: auto;
}
table.clusternodes, table#clusternodes td, table#addclusternode td,
table#qdisk_heuristics td{
@@ -479,14 +479,14 @@ form>h2 {
border-radius: 2px;
-webkit-box-shadow: 0 1px 1px #fff;
-moz-box-shadow: 0 1px 1px #fff;
- box-shadow: 0 1px 1px #fff;
+ box-shadow: 0 1px 1px #fff;
}
ul#clusterlist {
border-top: 1px solid #cad0d4;
- font-size: 12px;
+ font-size: 12px;
margin-top: 14px;
- padding-top: 14px;
+ padding-top: 14px;
}
.sidenav ul#clusterlist li a {
@@ -504,7 +504,7 @@ ul#clusterlist li a.problem {
.sidenav ul#clusterlist li a.current {
background-color: #d2e4f0;
color: #000;
- border: 1px solid #c5ddeb;
+ border: 1px solid #c5ddeb;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
@@ -598,25 +598,28 @@ span.code {
background:#EEEEEE none repeat scroll 0% 0%;
font-weight:bold;
}
-#flash, .notice {
+.message, .notice {
font-size:120%;
font-weight:normal;
margin:0 auto 0.5em;
}
-#flash div, .notice {
+.message div, .notice {
padding:20px 15px 20px 65px;
}
-#flash .ok {
+.message #flash, .notice {
+padding:0;
+}
+.message .ok {
background:#d8ecd8 url(../images/ok.png) no-repeat scroll 10px center;
}
-#flash .warning {
+.message .warning {
background:#fff483 url(../images/warning.png) no-repeat scroll 10px center;
}
-#flash .error {
+.message .error {
background:#f9c5c1 url(../images/error.png) no-repeat scroll 10px center;
}
-#flash .alert,
-#flash .info {
+.message .alert,
+.message .info {
background:#e9f4fa url(../images/info.png) no-repeat scroll 10px center;
}
.notice {
@@ -632,3 +635,12 @@ clear:both;
.hidden {
display: none ! important;
}
+
+.hide_btn {
+ float: right;
+}
+
+.important {
+ color: red;
+ padding: 1em 0em;
+}
diff --git a/luci/templates/configure.html b/luci/templates/configure.html
index 98bedf6..0ffc436 100644
--- a/luci/templates/configure.html
+++ b/luci/templates/configure.html
@@ -37,7 +37,7 @@
<script type="text/javascript">
$(function() {
function runEffect() {
-
+
$("#advanced").toggle("blind",{},500);
};
@@ -61,7 +61,7 @@
<h2>General Properties</h2>
<div class="row"><label>Cluster Name</label>
<input name="clusterName" type="text" class="text"
value="${tmpl_context.cluster_name}" disabled="disabled" />
- </div>
+ </div>
<div class="row"><label>Configuration Version</label>
<input type="text" class="text"
name="config_version" value="${cluster_data and
cluster_data.getClusterConfigVersion()}"/>
</div>
@@ -75,7 +75,7 @@
<h2>Fence Daemon Properties</h2>
<div class="row"><label>Post Fail Delay
(seconds)</label>
<input type="text" class="text"
name="post_fail_delay" value="${cluster_data and
cluster_data.getFenceDaemonPtr().getAttribute('post_fail_delay')}"/>
- </div>
+ </div>
<div class="row"><label>Post Join Delay
(seconds)</label>
<input type="text" class="text"
name="post_join_delay" value="${cluster_data and
cluster_data.getFenceDaemonPtr().getAttribute('post_join_delay')}"/>
</div>
@@ -100,14 +100,14 @@
<input name="multicast" value="multicast_manual"
class="radio" type="radio"
py:attrs="cluster_data and cluster_data.getMcastAddr() != None and
{'checked': 'checked'} or {}"/>
<label class="choice">Specify the multicast address
manually</label>
- <br />
+ <br />
<label class=" indent">Multicast address</label>
<input type="text" class="text"
name="mcast_address" value="${cluster_data and
cluster_data.getMcastAddr()}"/>
<br />
<input name="multicast" value="broadcast"
class="radio" type="radio"
py:attrs="cluster_data and cluster_data.get_cluster_broadcast() is True and
{'checked': 'checked'} or {}"/>
<label class="choice">Use broadcast (demos only - no production
support)</label>
- </div>
+ </div>
<div class="row"><div id="button"><button
class="button small silver">Show Advanced
Properties</button></div>
</div>
@@ -122,19 +122,24 @@
</div>
</form>
</div>
- <div id="tabs-4" py:with="quorumd_ptr = cluster_data and
cluster_data.quorumd_ptr">
+ <div id="tabs-4" py:with="quorumd_ptr = cluster_data and
cluster_data.quorumd_ptr;
+ qdisk_found, qdisk_supported = cluster_data and
cluster_data.getClusterPtr().doesClusterUseQuorumDisk();
+ qdisk_used = qdisk_found and qdisk_supported">
<form action="${tg.url(configure_cmd)}" method="post">
<input type="hidden" name="page" value="QDisk"
/>
<input type="hidden" name="command" value="Update"
/>
<h2>Quorum Disk Configuration</h2>
<div class="row">
- <input name="quorumd" type="radio"
class="radio" value="false"
+ <div py:if="not qdisk_supported"
class="important">
+ Quorum Disk will not be used unless each node of the cluster has only 1
vote!
+ </div>
+ <input name="quorumd" type="radio"
class="radio" value="false"
onclick="disableChildrenInput('qdisk_config')"
- py:attrs="not quorumd_ptr and {'checked': ''} or
{}"/><label class="choice">Do not use a Quorum Disk</label>
+ py:attrs="not qdisk_used and {'checked': 'checked'} or
{}"/><label class="choice">Do not use a Quorum Disk</label>
<br />
<input name="quorumd" type="radio"
class="radio" value="true"
onclick="enableChildrenInput('qdisk_config')"
- py:attrs="quorumd_ptr and {'checked': ''} or
{}"/><label class="choice">Use a Quorum Disk</label>
+ py:attrs="qdisk_used and {'checked': 'checked'} or
{}"/><label class="choice">Use a Quorum Disk</label>
</div>
<div id="qdisk_config">
@@ -234,7 +239,7 @@
</table>
<tr>
<td><input type="button" class="button small
silver" value="Add Another Heuristic"
onclick="add_qdisk_heuristic()" /></td>
- </tr>
+ </tr>
</fieldset>
</div>
<div class="row"><input type="submit"
class="button formsubmit blue" value="Apply"/>
diff --git a/luci/templates/master.html b/luci/templates/master.html
index 5b37d77..3ae5411 100644
--- a/luci/templates/master.html
+++ b/luci/templates/master.html
@@ -40,8 +40,17 @@
<div id="content">
<py:with vars="flash=tg.flash_obj.render('flash',
use_js=False)">
- <div py:if="flash" py:content="XML(flash)" />
- </py:with>
+ <div py:if="flash" py:content="XML(flash)"
class="message"/>
+ </py:with>
+ <py:if test="'flash2' in session">
+ <div py:for="message in session['flash2']"
class="message" id="${message.hash}">
+ <div py:if="message.hideable" class="hide_btn">
+ <a href="#"
onclick="$('#${message.hash}').hide('blind',{},500,function ()
{});">Hide</a>
+ </div>
+ <div py:if="message.html" class="${message.css}"
py:content="XML(message.message)"/>
+ <div py:if="not message.html" class="${message.css}"
py:content="message.message"/>
+ </div>
+ </py:if>
<py:if test="'show_sidebar' in dir(tmpl_context) and
tmpl_context.show_sidebar">
<xi:include href="mainmenu.html" />
diff --git a/luci/templates/node.html b/luci/templates/node.html
index 5956048..599a717 100644
--- a/luci/templates/node.html
+++ b/luci/templates/node.html
@@ -174,7 +174,7 @@
<div id="not_selected" py:choose="cluster_status and
len(cluster_status.nodes) or 'unknown'">
<py:when test="'unknown'">
Unable to contact any of the nodes in this cluster.
- </py:when>
+ </py:when>
<py:when test="0">No item to display</py:when>
<py:otherwise>
Select an item to view details
@@ -202,6 +202,28 @@
</span>
</div>
+ <!--! DETAILS - attributes section. -->
+ <div class="details_section">
+ <h4>Properties</h4>
+ <div class="details_inner">
+ <form action="${'%snodes_cmd?command=Update+Attributes' %
tmpl_context.cluster_url}" method="post"
+ py:with="node_data = cluster_data.getNodeByName(name)">
+ <input type="hidden" name="name"
value="${name}"/>
+ <input type="submit" value="Update Properties"
class="float_button"/>
+ <table id="node_tattr">
+ <tr>
+ <td class="node_tattr_caption">
+ Number of votes
+ </td><td class="node_tattr_input">
+ <input type="text" name="votes"
py:attrs="{'value': node_data.getVotes()}"/>
+ </td>
+ </tr>
+ </table>
+ </form>
+
+ </div>
+ </div>
+
<!--! DETAILS - services section. -->
<div class="details_section">
<h4>Services</h4>
@@ -333,7 +355,7 @@
<input type="Submit" value="Submit" class="button
formsubmit blue" />
<input type="button" class="button formsubmit silver"
value="Cancel"
onclick="$('#edit_fencedev_dialog_${instance_id}').dialog('close')"
/>
- </div>
+ </div>
</form>
</div>
</div>
@@ -437,7 +459,7 @@
<input type="Submit" value="Submit" class="button
formsubmit blue" />
<input type="button" class="button formsubmit silver"
value="Cancel"
onclick="$('#create_fencedev_dialog').dialog('close')"
/>
- </div>
+ </div>
</form>
</div>
</div>
@@ -450,7 +472,7 @@
<input type="Submit" value="Submit" class="button
formsubmit blue" />
<input type="button" class="button formsubmit silver"
value="Cancel"
onclick="$('#create_fencemethod_dialog').dialog('close')"
/>
- </div>
+ </div>
</form>
</div>
</div>