commit e2b56e3bd1ba81e2e242650bdddeba003a8b045e Author: Jan Pokorny jpokorny@redhat.com Date: Tue Aug 25 20:08:08 2009 +0200
Luci: Failovers + Fences (mainly internal changes)
Added support for UTF-8 in some places, following patch for Genshi 0.5.1 should be applied (http://file.brq.redhat.com/~jpokorny/patches/genshi/genshi_0.5.1_utf8_patch....):
--- base.py 2009-08-24 18:10:57.000000000 +0200 +++ base.py 2009-08-24 17:52:28.000000000 +0200 @@ -513,7 +513,10 @@ value = [x for x in values if x is not None] if not value: continue - new_attrs.append((name, u''.join(value))) + try: + new_attrs.append((name, u''.join(value))) + except UnicodeDecodeError, e: + new_attrs.append((name, u''.join([x.decode('utf-8') for x in value]))) yield kind, (tag, Attrs(new_attrs)), pos
elif kind is EXPR:
luci/controllers/decorators.py | 24 +++ luci/controllers/root.py | 425 ++++++++++++++++++++++++++------------- luci/lib/form_helpers.py | 43 ++++ luci/public/css/shared.css | 14 ++- luci/public/images/question.png | Bin 0 -> 565 bytes luci/public/js/fdom.js | 39 ++++ luci/templates/fdom.html | 88 ++++++--- luci/templates/fence.html | 71 +++++-- luci/templates/master.html | 2 + 9 files changed, 513 insertions(+), 193 deletions(-) --- diff --git a/luci/controllers/decorators.py b/luci/controllers/decorators.py new file mode 100644 index 0000000..67433da --- /dev/null +++ b/luci/controllers/decorators.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"""Decorators used in the main controller.""" + +from tg import url, redirect, request + +def noAdditionalKeyArgs(fn): + """Decorator for stripping off additional keyword args & purifying URL.""" + def purified(*args, **kw): + key_args = fn.func_code.co_varnames # All keyword args of original fn. + redir = False + for keyw in kw.keys(): + # If some keyword arg that is not supported by original function + # was found, remove it from keyword args dict and set + # the redirect flag for redirection to purified URL. + if not keyw in key_args: + del kw[keyw] + redir = True + if redir: + redirect(url(request.path, **kw)) + else: + return fn(*args, **kw) + + return purified + diff --git a/luci/controllers/root.py b/luci/controllers/root.py index 068803e..575ff3d 100644 --- a/luci/controllers/root.py +++ b/luci/controllers/root.py @@ -6,6 +6,7 @@ from tg import tmpl_context from pylons.i18n import ugettext as _, lazy_ugettext as l_ from catwalk.tg2 import Catwalk from repoze.what import predicates +from sys import stderr
from luci.lib.base import BaseController from luci.model import DBSession, metadata @@ -16,8 +17,8 @@ from luci.controllers.secure import SecureController from luci.lib.ricci_helpers import send_batch_to_hosts from luci.lib.ricci_communicator import RicciCommunicator import luci.lib.ricci_queries as rq -from sys import stderr - +from luci.lib.form_helpers import string2id, id2string +from luci.controllers.decorators import noAdditionalKeyArgs
__all__ = ['RootController']
@@ -42,63 +43,10 @@ class RootController(BaseController):
error = ErrorController()
- @expose('luci.templates.create') - def create(self, name=""): - return dict() - - @expose() - def cluster_create(self, **kw): - errors = () - cluster_name = kw.get('clustername') - enable_storage = kw.get('enable_storage') - reboot_nodes = kw.get('reboot_nodes') - download_pkgs = kw.get('download_pkgs') - - nodes = [ - [ kw.get('__SYSTEM0_addr'), kw.get('__SYSTEM0_passwd') ], - [ kw.get('__SYSTEM1_addr'), kw.get('__SYSTEM1_passwd') ], - [ kw.get('__SYSTEM2_addr'), kw.get('__SYSTEM2_passwd') ] - ] - node_list = [ i[0] for i in nodes ] - stderr.write('nodes: %s\n' % str(node_list)) - - if not cluster_name: - flash(_('No cluster name was given')) - redirect('/create') - if len(cluster_name) > 15: - flash(_('Cluster names must be less than 16 characters long')) - redirect('/create') - - for node in nodes: - rc = RicciCommunicator(node[0], enforce_trust=False) - rc.trust() - rc.auth(node[1]) - if not rc.authed(): - errors.append('Authentication to node %s failed' % node[0]) - break - else: - rc = RicciCommunicator(node[0]) - - cur_cluster_name = rc.cluster_info()[0] - if cur_cluster_name: - errors.append('%s is already a member of a cluster %s' \ - % (node[0], cur_cluster_name)) - break - - if len(errors) > 0: - flash('The following errors occurred: %s' % str(errors)) - redirect("/create") - - ret = send_batch_to_hosts(node_list, 10, rq.create_cluster, - 'rhel5', cluster_name, cluster_name, - node_list, True, True, enable_storage, False, - download_pkgs, None, reboot_nodes) - - redirect('/create')
########################################################################### - # Static demo data. - + # STATIC DEMO DATA + ###########################################################################
# Nodes in the current cluster.
@@ -107,71 +55,110 @@ class RootController(BaseController): NODE_UNKNOWN = '1' NODE_INACTIVE = '2'
- # Temporary data for failover domains, format: - # {'nodename': {'status': NODE_X, 'msg': 'optional status description'}, ...} + # Demo data for nodes (format is self-explained). nodes = {'NodeAlpha': {'status': NodeConstants.NODE_ACTIVE}, 'NodeBeta': {'status': NodeConstants.NODE_ACTIVE}, - 'NodeDelta': {'status': NodeConstants.NODE_UNKNOWN, + 'NodeDelta': {'status': NodeConstants.NODE_INACTIVE, 'msg': 'Something terrible'}, 'NodeGamma': {'status': NodeConstants.NODE_ACTIVE}, - 'NodeEpsilon': {'status': NodeConstants.NODE_UNKNOWN, - 'msg': 'Something terrible'}} - - # Failover domains. - - # Temporary data for failover domains, format: - # {'name': (prioritized, restricted, failback, [('service', is ok?), ...], - # {'member': priority, ...}), ...} - fdoms = {'Failover1':(False, True, False, - [('SAP', True), - ('Oracle', True)], - {'NodeAlpha': 1, - 'NodeBeta': 2}), - 'Failover2':(True, True, False, - [('Service X', True), - ('Service Y', False), - ('Service Z', True)], - {'NodeAlpha': 1, - 'NodeBeta': 2, - 'NodeEpsilon': 0}), - 'FDOM a': (True, False, False, - [('NFS', True), - ('Samba', True)], - {'NodeBeta': 2, - 'NodeGamma': 0}), - 'FDOM b': (True, False, False, - [('Apache', True)], - {'NodeDelta': 0, - 'NodeGamma': 0}), - 'FDOM c': (True, True, True, - [('Tomcat', True), - ('PostgreSQL', False), - ('LVM', True)], - {'NodeAlpha': 1, - 'NodeEpsilon': 0})} - - # Shared fences. - - # Temporary data for fences, format: - # {'name': (type, hostname, address, port, [("node", status, has primary priority?), ...]) - - fences = { 'Fence A':('iLO', 'hostname.host.org', '123.123.78.90', 6666, - {'NodeDelta': 1}), - 'Fence B':('APC Power Device', 'fenceb.host.org', '123.123.78.11', 6667, - {'NodeAlpha': 1, - 'NodeDelta': 2}), - 'Fence C':('Virtual Machine', 'fencec.host.org', '123.123.78.156', 11023, - {'NodeAlpha': 1, - 'NodeBeta': 2, - 'NodeGamma': 2, - 'NodeDelta': 2}), - 'Fence D':('SCSI Reservation', 'fenced.host.org', '123.123.78.35', 5487, - {'NodeAlpha': 2, - 'NodeBeta': 1, - 'NodeGamma': 2})} + 'NodeEpsilon': {'status': NodeConstants.NODE_UNKNOWN}} + + # Services in the current cluster. + + # Demo data for services (format is self-explained). + services = {'Service W': {'running': True, + 'autostart': True, + 'faildom': 'Failover1', + 'node': 'NodeGamma'}, + 'Service Y': {'running': False, + 'autostart': False, + #'faildom': None, + 'node': 'NodeAlpha'}, + 'Service X': {'running': True, + 'autostart': True, + 'faildom': 'Failover2', + 'node': 'NodeBeta'}, + 'Service Z': {'running': False, + 'autostart': True, + #'faildom': None, + 'node': 'NodeAlpha'}} + + # Failover domains in the current cluster. + + # Demo data for failover domains (format is self-explained). + fdoms = {'Failover1':{'prioritized': False, + 'restricted': True, + 'failback': False, + 'services': ('Service Y', + 'Service X'), + 'nodes': {'NodeAlpha': 1, + 'NodeBeta': 2}}, + 'Failover2':{'prioritized': True, + 'restricted': True, + 'failback': False, + 'services': ('Service X', + 'Service Y', + 'Service Z'), + 'nodes': {'NodeAlpha': 1, + 'NodeBeta': 2, + 'NodeEpsilon': 0}}, + 'FDOM a': {'prioritized': True, + 'restricted': False, + 'failback': False, + 'services': ('Service W', + 'Service Z'), + 'nodes': {'NodeBeta': 2, + 'NodeGamma': 0}}, + 'FDOM b': {'prioritized': True, + 'restricted': False, + 'failback': False, + 'services': ('Service Y'), + 'nodes': {'NodeDelta': 0, + 'NodeGamma': 0}}, + 'FDOM c': {'prioritized': True, + 'restricted': True, + 'failback': True, + 'services': ('Service Z', + 'Service X', + 'Service W'), + 'nodes': {'NodeAlpha': 1, + 'NodeEpsilon': 0}}} + + # Shared fences in the current cluster. + + # Demo data for fences (format is self-explained). + fences = { 'Fence A':{'type': 'iLO', + 'host': 'hostname.host.org', + 'ip': '123.123.78.90', + 'port': 6666, + 'nodes': {'NodeDelta': 1}}, + 'Fence B':{'type': 'APC Power Device', + 'host': 'fenceb.host.org', + 'ip': '123.123.78.11', + 'port': 6667, + 'nodes': {'NodeAlpha': 1, + 'NodeDelta': 2}}, + 'Fence C':{'type': 'Virtual Machine', + 'host': 'fencec.host.org', + 'ip': '123.123.78.156', + 'port': 11023, + 'nodes': {'NodeAlpha': 1, + 'NodeBeta': 2, + 'NodeGamma': 2, + 'NodeDelta': 2, + 'NodeEpsilon': 2}}, + 'Fence D':{'type': 'SCSI Reservation', + 'host': 'fenced.host.org', + 'ip': '123.123.78.35', + 'port': 5487, + 'nodes': {'NodeAlpha': 2, + 'NodeBeta': 1, + 'NodeGamma': 2}}}
- ###########################################################################
+ ########################################################################### + # METHODS OF THE CONTROLLER + ###########################################################################
@expose('luci.templates.index') def index(self): @@ -183,6 +170,9 @@ class RootController(BaseController): """Handle the 'about' page.""" return dict(page='about')
+ + # TODO: Automatically generated/experimental methods? Perhaps clean needed. + @expose('luci.templates.authentication') def auth(self): """Display some information about auth* on this application.""" @@ -234,87 +224,236 @@ class RootController(BaseController): redirect(came_from)
+ # Create cluster part. + + @expose('luci.templates.create') + @noAdditionalKeyArgs + def create(self, name=""): + return dict() + + @expose() + def cluster_create(self, **kw): + errors = () + cluster_name = kw.get('clustername') + enable_storage = kw.get('enable_storage') + reboot_nodes = kw.get('reboot_nodes') + download_pkgs = kw.get('download_pkgs') + + nodes = [ + [ kw.get('__SYSTEM0_addr'), kw.get('__SYSTEM0_passwd') ], + [ kw.get('__SYSTEM1_addr'), kw.get('__SYSTEM1_passwd') ], + [ kw.get('__SYSTEM2_addr'), kw.get('__SYSTEM2_passwd') ] + ] + node_list = [ i[0] for i in nodes ] + stderr.write('nodes: %s\n' % str(node_list)) + + if not cluster_name: + flash(_('No cluster name was given')) + redirect('/create') + if len(cluster_name) > 15: + flash(_('Cluster names must be less than 16 characters long')) + redirect('/create') + + for node in nodes: + rc = RicciCommunicator(node[0], enforce_trust=False) + rc.trust() + rc.auth(node[1]) + if not rc.authed(): + errors.append('Authentication to node %s failed' % node[0]) + break + else: + rc = RicciCommunicator(node[0]) + + cur_cluster_name = rc.cluster_info()[0] + if cur_cluster_name: + errors.append('%s is already a member of a cluster %s' \ + % (node[0], cur_cluster_name)) + break + + if len(errors) > 0: + flash('The following errors occurred: %s' % str(errors)) + redirect("/create") + + ret = send_batch_to_hosts(node_list, 10, rq.create_cluster, + 'rhel5', cluster_name, cluster_name, + node_list, True, True, enable_storage, False, + download_pkgs, None, reboot_nodes) + + redirect('/create') + + # Failover Domains part.
@expose('luci.templates.fdom') - def fdom(self, name=""): + @noAdditionalKeyArgs + def fdom(self, name=None): """Handle 'failover domains' page.""" details = None - fdoms_overview = [[k,v[0],v[1],v[2]] for k,v in self.fdoms.iteritems()] + fdoms_overview = [[k,v['prioritized'],v['restricted'],v['failback']] + for k,v in self.fdoms.iteritems()]
# Check whether we are able to display details to selected failover - # domain. - if name != "": + # domain (and if the given failover domain really exists). + if name: if self.fdoms.has_key(name): details = self.fdoms[name] + else: + redirect('/fdom') + return dict(name=name, fdoms=fdoms_overview, details=details, - nodes=self.nodes) + services=self.services, nodes=self.nodes, + nodeconst=self.NodeConstants, + string2id=string2id, id2string=id2string)
@expose() - def fdom_delete(self): + def fdom_delete(self, name=None, **kw): """Handle removal of selected failover domain(s).""" - flash('Demo of deleting...') + if name: + which = [name] + else: + which = [id2string(s) for s in kw.iterkeys()] + + if not which: + flash(_('Nothing was chosen!'), status='warning') + else: + flash(_('Deleting selected failover domain(s)... %s') % + (", ".join(which)), status='info') + for fdom in which: + if self.fdoms.has_key(fdom): + del self.fdoms[fdom] + else: + # If the luci's data are consistent this should never happen. + flash(_('Internal error'), status='error') + break + redirect(request.referer or '/fdom')
@expose() #@expose('luci.templates.fdom_add_form') + @noAdditionalKeyArgs def fdom_add(self): """Handle creation of a new failover domain.""" - flash('Demo of adding...') - redirect('/fence') + flash('It should be a dialog to add new failover domain here instead,' + 'but not implemented yet...', status='info') + redirect('/fdom') # following code is only my experiment, how to continue: #tmpl_context.form = create_fdom_add_form #return dict()
@expose() - def fdom_update(self): - """Handle changes in a failover domain.""" - flash('Demo of updating properties...') + def fdom_update_props(self, name=None, **kw): + """Handle changes in a failover domain -- properties.""" + if not name: + redirect(request.referer or '/fdom') + + fdom = id2string(name) + if self.fdoms.has_key(fdom): + for attr in ['prioritized', 'restricted', 'failback']: + if kw.has_key(attr): + self.fdoms[fdom][attr] = True + else: + self.fdoms[fdom][attr] = False + flash(_('Updating properties for %s') % fdom, status='info') + else: + # If the luci's data are consistent this should never happen. + flash(_('Internal error'), status='error') + + redirect(request.referer or '/fdom') + + @expose() + def fdom_update_members(self, name=None, **kw): + """Handle changes in a failover domain -- member nodes.""" + if not name: + redirect(request.referer or '/fdom') + + fdom = id2string(name) + if self.fdoms.has_key(fdom): + # Clear the dictionary of member nodes and start from scratch. + self.fdoms[fdom]['nodes'].clear() + nodes = map(lambda id_check: id2string(id_check.replace('.check', '')), + filter(lambda id_str: id_str.endswith('.check'), + kw.iterkeys())) + flash(_('Updating members for %s') % fdom, status='info') + for node in nodes: + if self.nodes.has_key(node): + self.fdoms[fdom]['nodes'][node] = \ + kw.get(string2id(node) + '.priority', 0) + else: + # If the luci's data are consistent this should never happen. + flash(_('Internal error'), status='error') + break + redirect(request.referer or '/fdom')
@expose() + @noAdditionalKeyArgs def fdom_service(self): """Handle creation of a new service.""" - flash('Demo of adding a service...') + flash('Demo of adding a service...', status='info') redirect('/fdom')
# Fences part.
@expose('luci.templates.fence') - def fence(self, name=""): + @noAdditionalKeyArgs + def fence(self, name=None): """Handle 'fences' page.""" details = None fences_overview = \ - [[k,v[0], - len(v[4])==1 - and (v[4].keys()[0], - self.nodes.get(v[4].keys()[0], + [[k,v['type'], + len(v['nodes'])==1 + and (v['nodes'].keys()[0], + self.nodes.get(v['nodes'].keys()[0], {'status': self.NodeConstants.NODE_UNKNOWN})) - or len(v[4]), - v[2]] for k,v in self.fences.iteritems()] + or len(v['nodes']), + v['ip']] for k,v in self.fences.iteritems()]
# Check whether we are able to display details to selected fence. - if name != "": + if name: if self.fences.has_key(name): details = self.fences[name] + else: + redirect('/fence') + return dict(name=name, fences=fences_overview, details=details, - nodes=self.nodes, nodeconst=self.NodeConstants) + nodes=self.nodes, nodeconst=self.NodeConstants, + string2id=string2id, id2string=id2string)
@expose() - def fence_delete(self): + def fence_delete(self, name=None, **kw): """Handle removal of selected fence(s).""" - flash('Demo of deleting...') + if name: + which = [name] + else: + which = [id2string(s) for s in kw.iterkeys()] + + if not which: + flash(_('Nothing was chosen!'), status='warning') + else: + flash(_('Deleting selected fence(s)... %s') + % (", ".join(which)), status='info') + for fence in which: + if self.fences.has_key(fence): + del self.fences[fence] + else: + # If the luci's data are consistent this should never happen. + flash(_('Internal error'), status='error') + break + redirect(request.referer or '/fence')
@expose() + @noAdditionalKeyArgs def fence_add(self): """Handle creation of a new fence.""" - flash('Demo of adding...') - redirect('/fence') + flash('It should be a dialog to add new fence here instead, but not' + 'implemented yet...') + redirect('/fence', status='info')
@expose() + @noAdditionalKeyArgs def fence_manage(self): """Handle the nodes management.""" - flash('Demo of managing nodes...') + flash('Demo of managing nodes...', status='info') redirect('/fence') diff --git a/luci/lib/form_helpers.py b/luci/lib/form_helpers.py new file mode 100644 index 0000000..6f8481f --- /dev/null +++ b/luci/lib/form_helpers.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +"""HTML form's helpers.""" + +from base64 import b64encode, b64decode +from string import maketrans + +__all__ = ['string2id', 'id2string'] + +STARTING_CHAR = 'Z' + +B64_CONV_CHARS = "-_:" +B64_ORIG_CHARS = "=+/" + +def string2id(s): + """Convert the string to the form usable as an element's id or name. + + 'Usable' means that it conforms (X)HTML requirements for the value + of 'id' and 'name' attribute of an element. + + The implementation uses base64 encoding with some other additions; + references: + http://www.w3.org/TR/xhtml1/#C_8 + http://www.w3.org/TR/html4/types.html#h-6.2 + http://tools.ietf.org/html/rfc3548.html + + Keyword arguments: + s String to convert. + + """ + a = 'Z' + b64encode(s.encode('utf-8')).translate(maketrans(B64_ORIG_CHARS,B64_CONV_CHARS)) + print 'string2id:\t' + "'" + a + "'" + return a + +def id2string(s): + """Receive the string previously encoded by string2id(). + + Keyword arguments: + s String to convert back to human readable format. + """ + a = b64decode(str(s)[1:].translate(maketrans(B64_CONV_CHARS,B64_ORIG_CHARS))).decode('utf-8') + print 'id2string:\t' + "'" + a + "'" + return a diff --git a/luci/public/css/shared.css b/luci/public/css/shared.css index 3ce0d12..c581eb7 100644 --- a/luci/public/css/shared.css +++ b/luci/public/css/shared.css @@ -54,8 +54,15 @@ a.float_button { background-color: #d2edaa; }
-.input_priority { +.input_priority, .input_priority_disabled { width: 3em; + border: thin solid #6d6d6d; + font-size: small; +} + +.input_priority_disabled { + background-color: #f2f2f2; + cursor: default; }
.icon { @@ -83,6 +90,11 @@ a.float_button { color: red; }
+.entity_unknown { + color: #5b5a5b; + font-style: italic; +} + .hide { visibility: hidden; overflow: hidden; diff --git a/luci/public/images/question.png b/luci/public/images/question.png new file mode 100644 index 0000000..50082ea Binary files /dev/null and b/luci/public/images/question.png differ diff --git a/luci/public/js/fdom.js b/luci/public/js/fdom.js new file mode 100644 index 0000000..b8bac5f --- /dev/null +++ b/luci/public/js/fdom.js @@ -0,0 +1,39 @@ +/* + * JS functions for Failover domains. + * + */ + + +/** + * On page load, disable the priority settings for the nodes that are not + * members of the current failover domain. + */ +function onLoad() + { + var input_elems = document.getElementsByTagName("input"); + for (var i = input_elems.length - 1; i >= 0; i--) + { + var input_elem = input_elems[i]; + if (input_elem.className == "input_priority_disabled") + { + input_elem.disabled = true; + input_elem.className = "input_priority"; + } + } + } + + +/** + * On 'member' checkbox change, enable/disable the priority settings + * for the node and change the style appropriately. + * + * @param elem_id Identifier of the checkbox being changed. + */ +function onCheckMember(elem_id) + { + var elem = document.getElementById(elem_id.replace(".check", ".priority")); + if (elem) + { + elem.disabled = elem.disabled ^ 1; + } + } diff --git a/luci/templates/fdom.html b/luci/templates/fdom.html index 4d588e0..f0a3bd1 100644 --- a/luci/templates/fdom.html +++ b/luci/templates/fdom.html @@ -9,9 +9,10 @@ <head> <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/> <title>Failover domains</title> + <script type="text/javascript" src="${tg.url('/js/fdom.js')}"></script> </head>
-<body> +<body onload="onLoad()">
<form action="${tg.url('/fdom_delete')}" method="post"> <div id="toolbar"> @@ -19,7 +20,7 @@ <a href="${tg.url('/fdom_add')}" class="toolbar_button" id="tb_add">add</a> </div>
- <!--! Overview section. --> + <!--! OVERVIEW SECTION. --> <div id="overview"> <table id="fdom_tlist"> <thead> @@ -39,8 +40,9 @@ <tbody> <!--! List all the failover domains. --> <tr py:for="i, fdom in enumerate(fdoms)" - py:attrs="fdom[0]==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"> - <td class="checkbox"><input type="checkbox"/></td> + py:attrs="fdom[0]==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)" + py:with="identifier=string2id(fdom[0])"> + <td class="checkbox"><input type="checkbox" name="${identifier}"/></td> <td class="icon"></td> <td class="main_id"><a href="${tg.url('/fdom?name=' + fdom[0])}">${fdom[0]}</a></td> <!--! TODO: Is the following column necessary? --> @@ -56,7 +58,7 @@ </div> </form>
- <!--! Details section. --> + <!--! DETAILS SECTION. --> <div id="details" py:choose="details">
<py:when test="None"> @@ -69,23 +71,24 @@
- <!--! Details - Header. --> + <!--! DETAILS - header section. --> <div id="details_header"> - <h3 py:content="name">Failover1</h3> + <h3 py:content="name">Failover name</h3> <div id="details_header_buttons"> - <a href="${tg.url('/fdom_delete')}" id="dh_delete" title="delete"><span class="hide">delete</span></a> + <a href="${tg.url('/fdom_delete?name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a> </div> </div>
- <!--! Details - Attributes section. --> + <!--! DETAILS - attributes section. --> <div class="details_section"> <div class="details_inner"> - <form action="${tg.url('/fdom_update')}" method="post"> + <form action="${tg.url('/fdom_update_props')}" method="post"> + <input type="hidden" name="name" value="${string2id(name)}"/> <input type="submit" value="Update Properties" class="float_button"/> <table id="fdom_tattr"> <tr> <td class="fdom_tattr_checkbox"> - <input type="checkbox" id="prioritized" py:attrs="details[0] and {'checked': 'checked'} or None"/> + <input type="checkbox" id="prioritized" name="prioritized" py:attrs="details['prioritized'] and {'checked': 'checked'} or None"/> </td><td class="fdom_tattr_caption"> <label for="prioritized">Prioritized</label> </td><td class="fdom_tattr_hint"> @@ -94,7 +97,7 @@ </tr> <tr> <td class="fdom_tattr_checkbox"> - <input type="checkbox" id="restricted" py:attrs="details[1] and {'checked': 'checked'} or None"/> + <input type="checkbox" id="restricted" name="restricted" py:attrs="details['restricted'] and {'checked': 'checked'} or None"/> </td><td class="fdom_tattr_caption"> <label for="restricted">Restricted</label> </td><td class="fdom_tattr_hint"> @@ -103,7 +106,7 @@ </tr> <tr> <td class="fdom_tattr_checkbox"> - <input type="checkbox" id="failback" py:attrs="details[1] and {'checked': 'checked'} or None"/> + <input type="checkbox" id="failback" name="failback" py:attrs="details['failback'] and {'checked': 'checked'} or None"/> </td><td class="fdom_tattr_caption"> <label for="failback">Failback</label> </td><td class="fdom_tattr_hint"> @@ -115,7 +118,7 @@ </div> </div>
- <!--! Details - Services section. --> + <!--! DETAILS - services section. --> <div class="details_section"> <a href="${tg.url('/fdom_service')}" class="float_button">Add a Service</a> <h4>Services</h4> @@ -123,11 +126,15 @@ <table id="fdom_tservices"> <tbody> <!--! List all the services connected with the current failover domain. --> - <tr py:for="service in details[3]" class="grid_row"> + <!--! Note: "('Service')" is syntactically a string, not a tuple, + so this case is also solved. --> + <tr py:for="service in (type(details['services']) is tuple and details['services'] or (details['services'],) )" class="grid_row"> + <py:with vars="running = services.has_key(service) and (services[service]['running'])"> <td class="icon"> - <img py:if="not service[1]" src="${tg.url('/images/exclamation.png')}" alt="Service has a problem." /> + <img py:if="not running" src="${tg.url('/images/exclamation.png')}" alt="Service has a problem." /> </td> - <td class="fdom_tservices_service"><span py:attrs="service[1] and {'class': 'entity_ok'} or {'class': 'entity_fail'}">${service[0]}</span></td> + <td class="fdom_tservices_service"><span py:attrs="running and {'class': 'entity_ok'} or {'class': 'entity_fail'}">${service}</span></td> + </py:with> <td class="table_space"></td> </tr> </tbody> @@ -135,9 +142,10 @@ </div> </div>
- <!--! Details - Members section. --> + <!--! DETAILS - members section. --> <div class="details_section"> - <form action="${tg.url('/fdom_update')}" method="post"> + <form action="${tg.url('/fdom_update_members')}" method="post"> + <input type="hidden" name="name" value="${string2id(name)}"/> <input type="submit" value="Update Settings" class="float_button"/> <h4>Members</h4> <div class="details_inner"> @@ -152,29 +160,49 @@ </tr> </thead> <tbody> - <!--! List all the members of the current failover domain. --> + <!--! List all the nodes (of the current cluster). --> <tr py:for="node, node_dict in nodes.iteritems()" class="grid_row"> + <!--! Branch according to the status of the node. --> + <py:choose test="node_dict.get('status', nodeconst.NODE_UNKNOWN)"> + <!--! 1) Node is active. --> + <py:when test="nodeconst.NODE_ACTIVE"> <td class="icon"></td> - <td class="fdom_tmembers_name">${node}</td> - <!--! Test whether the current node from the list is a member - of this failover domains. --> - <py:choose test="details[4].has_key(node)"> + <td class="fdom_tmembers_name"><span class="entity_ok">${node}</span></td> + </py:when> + <!--! 2) Node is inactive. --> + <py:when test="nodeconst.NODE_INACTIVE"> + <td class="icon"> + <img src="${tg.url('/images/exclamation.png')}" alt="Node has a problem." /> + </td> + <td class="fdom_tmembers_name"><span class="entity_fail">${node}</span></td> + </py:when> + <!--! 3) Status of the node is unknown. --> + <py:when test="nodeconst.NODE_UNKNOWN"> + <td class="icon"> + <img src="${tg.url('/images/question.png')}" alt="Status of the node is unknown." /> + </td> + <td class="fdom_tmembers_name"><span class="entity_unknown">${node}</span></td> + </py:when> + </py:choose> + <!--! Branch according to whether the current node is a member + of this failover domain. --> + <py:choose test="details['nodes'].has_key(node)" py:with="identifier=string2id(node)"> + <!--! 1) Yes. --> <py:when test="True"> - <!--! Yes. --> <td class="fdom_tmembers_member"> - <input type="checkbox" checked="checked" /> + <input id="${identifier+'.check'}" name="${identifier+'.check'}" type="checkbox" checked="checked" onchange="onCheckMember(this.id)"/> </td> <td class="fdom_tmembers_priority"> - <input type="text" value="${details[4][node]}" maxlength="3" class="input_priority"/> + <input id="${identifier + '.priority'}" name="${identifier + '.priority'}" type="text" value="${details['nodes'][node]}" maxlength="3" class="input_priority"/> </td> </py:when> + <!--! 2) No. --> py:otherwise - <!--! No. --> <td class="fdom_tmembers_member"> - <input type="checkbox" /> + <input id="${identifier + '.check'}" name="${identifier+'.check'}" type="checkbox" onchange="onCheckMember(this.id)"/> </td> <td class="fdom_tmembers_priority"> - <input type="text" value="0" maxlength="3" class="input_priority"/> + <input id="${identifier + '.priority'}" name="${identifier + '.priority'}" type="text" value="0" maxlength="3" class="input_priority_disabled"/> </td> </py:otherwise> </py:choose> diff --git a/luci/templates/fence.html b/luci/templates/fence.html index ef45f38..7fbd9fe 100644 --- a/luci/templates/fence.html +++ b/luci/templates/fence.html @@ -20,7 +20,7 @@ <a href="${tg.url('/fence_add')}" class="toolbar_button" id="tb_add">add</a> </div>
- <!--! Overview section. --> + <!--! OVERVIEW SECTION. --> <div id="overview"> <table id="fence_tlist"> <thead> @@ -40,13 +40,24 @@ <tbody> <!--! List all the shared fences. --> <tr py:for="i, fence in enumerate(fences)" - py:attrs="fence[0]==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"> - <td class="checkbox"><input type="checkbox"/></td> + py:attrs="fence[0]==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)" + py:with="identifier=string2id(fence[0])"> + <td class="checkbox"><input type="checkbox" name="${identifier}"/></td> + <!--! If the fence is shared, display appropriate icon. --> <td class="icon"><img py:if="type(fence[2]) is not tuple" src="${tg.url('/images/global_11x11_black.png')}" alt="shared fence"/></td> <td class="main_id"><a href="${tg.url('/fence?name=' + fence[0])}">${fence[0]}</a></td> <td class="fence_tlist_type">${fence[1]}</td> + <!--! Branch according to whether the fence is local or shared. --> <py:choose test="type(fence[2]) is tuple"> - <td py:when="True" class="fence_tlist_members"><span py:attrs="fence[2][1].get('status', nodeconst.NODE_UNKNOWN) == nodeconst.NODE_ACTIVE and {'class': 'entity_ok'} or {'class': 'entity_fail'}">${fence[2][0]}</span></td> + <!--! 1) Local. --> + <td py:when="True" class="fence_tlist_members"> + <py:choose test="fence[2][1].get('status', nodeconst.NODE_UNKNOWN)"> + <span py:when="nodeconst.NODE_ACTIVE" class="entity_ok">${fence[2][0]}</span> + <span py:when="nodeconst.NODE_INACTIVE" class="entity_fail">${fence[2][0]}</span> + <span py:when="nodeconst.NODE_UNKNOWN" class="entity_unknown">${fence[2][0]}</span> + </py:choose> + </td> + <!--! 2) Shared. --> <td py:otherwise="" class="fence_tlist_members">${fence[2]}</td> </py:choose> <td class="fence_tlist_ip">${fence[3]}</td> @@ -57,7 +68,7 @@ </div> </form>
- <!--! Details section. --> + <!--! DETAILS SECTION. --> <div id="details" py:choose="details">
<py:when test="None"> @@ -70,26 +81,26 @@
- <!--! Details - Header. --> + <!--! DETAILS - header section. --> <div id="details_header"> <h3 py:content="name">Fence A</h3> <div id="details_header_buttons"> - <a href="${tg.url('/fence_delete')}" id="dh_delete" title="delete"><span class="hide">delete</span></a> + <a href="${tg.url('/fence_delete?name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a> <a href="${tg.url(request.path_qs)}" id="dh_update" title="refresh"><span class="hide">refresh</span></a> </div> - type <span id="fence_details_type">${details[0]}</span> + type <span id="fence_details_type">${details['type']}</span> </div>
- <!--! Details - Hostname/IP/port section. --> + <!--! DETAILS - hostname/IP/port section. --> <div class="details_section"> <ul id="fence_details_list"> - <li><span class="fence_details_label">Hostname</span>${details[1]}</li> - <li><span class="fence_details_label">IP Address</span>${details[2]}</li> - <li><span class="fence_details_label">Port</span>${details[3]}</li> + <li><span class="fence_details_label">Hostname</span>${details['host']}</li> + <li><span class="fence_details_label">IP Address</span>${details['ip']}</li> + <li><span class="fence_details_label">Port</span>${details['port']}</li> </ul> </div>
- <!--! Details - Nodes section. --> + <!--! DETAILS - nodes section. --> <div class="details_section"> <a href="${tg.url('/fence_manage')}" class="float_button">Manage Nodes</a> <h4>Nodes</h4> @@ -106,14 +117,36 @@ </thead> <tbody> <!--! List all the nodes connected with the current shared fence. --> - <!--! Test whether the current node is connected with the fence. --> - <tr py:for="node, node_dict in nodes.iteritems()" class="grid_row" py:if="details[4].has_key(node)"> + <tr py:for="node, node_dict in nodes.iteritems()" class="grid_row" py:if="details['nodes'].has_key(node)"> + <!--! Branch according to the status of the node. --> + <py:choose test="node_dict.get('status', nodeconst.NODE_UNKNOWN)"> + <!--! 1) Node is active. --> + <py:when test="nodeconst.NODE_ACTIVE"> + <td class="icon"></td> + <td class="fence_tnodes_name"><span class="entity_ok">${node}</span></td> + <td class="fence_tnodes_status">${_('OK')}</td> + </py:when> + <!--! 2) Node is inactive. --> + <py:when test="nodeconst.NODE_INACTIVE"> <td class="icon"> - <img py:if="node_dict['status'] != nodeconst.NODE_ACTIVE" src="${tg.url('/images/exclamation.png')}" alt="Node has a problem." /> + <img src="${tg.url('/images/exclamation.png')}" alt="Node has a problem." /> </td> - <td class="fence_tnodes_name"><span py:attrs="node_dict['status'] == nodeconst.NODE_ACTIVE and {'class': 'entity_ok'} or {'class': 'entity_fail'}">${node}</span></td> - <td class="fence_tnodes_status">${node_dict['status'] != nodeconst.NODE_ACTIVE and node_dict['msg'] or _('OK')}</td> - <td class="fence_tnodes_level">${details[4][node] == 1 and _('Primary Fence') or _('Secondary Fence')}</td> + <td class="fence_tnodes_name"><span class="entity_fail">${node}</span></td> + <td class="fence_tnodes_status">${node_dict.get('msg', _('Uknown problem'))}</td> + </py:when> + <!--! 3) Status of the node is unknown. --> + <py:when test="nodeconst.NODE_UNKNOWN"> + <td class="icon"> + <img src="${tg.url('/images/question.png')}" alt="Status of the node is unknown." /> + </td> + <td class="fence_tnodes_name"><span class="entity_unknown">${node}</span></td> + <td class="fence_tnodes_status">${node_dict.get('msg', _('Status uknown'))}</td> + </py:when> + </py:choose> + <py:choose test="details['nodes'][node]"> + <td py:when="1" class="fence_tnodes_level">${_('Primary Fence')}</td> + <td py:when="2" class="fence_tnodes_level">${_('Secondary Fence')}</td> + </py:choose> <td class="table_space"></td> </tr> </tbody> diff --git a/luci/templates/master.html b/luci/templates/master.html index 24c9377..6ec3715 100644 --- a/luci/templates/master.html +++ b/luci/templates/master.html @@ -18,6 +18,8 @@ ${header()} <ul id="mainmenu"> <li><a href="${tg.url('/about')}" class="${('', 'active')[defined('page') and page==page=='about']}">About</a></li> + <li><a href="${tg.url('/fdom')}">Failovers</a></li> + <li><a href="${tg.url('/fence')}">Fences</a></li> <span py:if="tg.auth_stack_enabled" py:strip="True"> <li py:if="not request.identity" id="login" class="loginlogout"><a href="${tg.url('/login')}">Login</a></li> <li py:if="request.identity" id="login" class="loginlogout"><a href="${tg.url('/logout_handler')}">Logout</a></li>
luci-commits@lists.fedorahosted.org