[luci] luci: Sync up the master branch with current dev branch/upstream
by Ryan McCabe
commit 387d2bf38dbf7e43acca418479b7f98a00d112b9
Author: Ryan McCabe <ryan(a)chipotle.numb.lan>
Date: Wed Feb 3 14:19:29 2010 -0500
luci: Sync up the master branch with current dev branch/upstream
config/Makefile | 16 ++
config/cacert.config | 15 ++
config/luci.ini | 106 ++++++++
init.d/Makefile | 10 +
init.d/luci | 172 +++++++++++++
luci.spec | 88 +++++++
luci/controllers/async.py | 23 ++
luci/controllers/cluster.py | 372 +++++++++++++++--------------
luci/controllers/cluster_part/__init__.py | 13 +
luci/controllers/cluster_part/failover.py | 299 +++++++++++++++++++++++
luci/controllers/cluster_part/fence.py | 232 ++++++++++++++++++
luci/controllers/cluster_part/node.py | 273 +++++++++++++++++++++
luci/controllers/cluster_part/service.py | 240 +++++++++++++++++++
luci/controllers/decorators.py | 88 +++++---
luci/controllers/global_res.py | 216 +++++++++++++++++
luci/controllers/root.py | 8 +-
luci/controllers/scheme.py | 113 +++++++++
luci/lib/ClusterConf/ClusterNode.py | 7 +
luci/lib/ClusterConf/FailoverDomain.py | 10 +
luci/lib/ClusterConf/ModelBuilder.py | 27 ++
luci/lib/app_globals.py | 6 +-
luci/lib/app_strings.py | 84 +++++++
luci/lib/async_helpers.py | 31 +++
luci/lib/base.py | 137 ++++++++----
luci/lib/cluster_conf_helpers.py | 8 +
luci/lib/cluster_status.py | 64 +++---
luci/lib/db_helpers.py | 52 ++++-
luci/lib/demo_data.py | 214 ++++++++++++++---
luci/lib/form_utils.py | 99 ++++++---
luci/lib/helpers.py | 45 +++-
luci/lib/ricci_communicator.py | 2 +-
luci/model/objects.py | 2 +-
luci/public/css/global_res.css | 22 ++
luci/public/js/shared.js | 39 +++
luci/templates/cluster_forms.html | 31 +++
luci/templates/cluster_list.html | 22 +-
luci/templates/cluster_part/__init__.py | 2 +
luci/templates/cluster_part/failover.html | 234 ++++++++++++++++++
luci/templates/cluster_part/fence.html | 176 ++++++++++++++
luci/templates/cluster_part/node.html | 244 +++++++++++++++++++
luci/templates/cluster_part/service.html | 167 +++++++++++++
luci/templates/configure.html | 61 ++++-
luci/templates/failover.html | 55 ++---
luci/templates/fence.html | 45 ++--
luci/templates/global_res.html | 87 +++++++
luci/templates/homebase.html | 31 +--
luci/templates/node.html | 80 ++++---
luci/templates/service.html | 50 ++--
48 files changed, 3906 insertions(+), 512 deletions(-)
---
diff --git a/config/Makefile b/config/Makefile
new file mode 100644
index 0000000..cf37f3a
--- /dev/null
+++ b/config/Makefile
@@ -0,0 +1,16 @@
+localstatedir ?= ${DESTDIR}/var
+luci_dir = ${localstatedir}/lib/luci
+
+all:
+
+install:
+ /usr/bin/install -d -m 700 ${luci_dir}
+ /usr/bin/install -d -m 700 ${luci_dir}/data/
+ /usr/bin/install -d -m 700 ${luci_dir}/log/
+ /usr/bin/install -d -m 700 ${luci_dir}/etc/
+ /usr/bin/install -d -m 700 ${luci_dir}/certs/
+
+ /usr/bin/install -m 600 luci.ini ${luci_dir}/etc/
+ /usr/bin/install -m 600 cacert.config ${luci_dir}/etc/
+
+clean:
diff --git a/config/cacert.config b/config/cacert.config
new file mode 100644
index 0000000..1f2238a
--- /dev/null
+++ b/config/cacert.config
@@ -0,0 +1,15 @@
+[ req ]
+distinguished_name = req_distinguished_name
+attributes = req_attributes
+prompt = no
+
+[ req_distinguished_name ]
+C = US
+ST = State or Province
+L = Luci
+O = Luci
+OU = Luci
+CN = luci cluster management server
+emailAddress = luci@localhost
+
+[ req_attributes ]
diff --git a/config/luci.ini b/config/luci.ini
new file mode 100644
index 0000000..86b8e16
--- /dev/null
+++ b/config/luci.ini
@@ -0,0 +1,106 @@
+# luci - Pylons environment configuration
+#
+# The %(here)s variable will be replaced with the parent directory of this file
+#
+# This file is for deployment specific config options -- other configuration
+# that is always required for the app is done in the config directory,
+# and generally should not be modified by end users.
+
+[DEFAULT]
+debug = true
+# Uncomment and replace with the address which should receive any error reports
+#email_to = you(a)yourdomain.com
+#smtp_server = localhost
+#error_email_from = paste@localhost
+
+[server:main]
+use = egg:Paste#http
+host = 127.0.0.1
+port = 8080
+
+[app:main]
+use = egg:luci
+full_stack = true
+cache_dir = /var/lib/luci/data
+beaker.session.key = luci
+beaker.session.secret = somesecret
+
+# If you'd like to fine-tune the individual locations of the cache data dirs
+# for the Cache data, or the Session saves, un-comment the desired settings
+# here:
+beaker.cache.data_dir = /var/lib/luci/data/cache
+beaker.session.data_dir = /var/lib/luci/data/sessions
+
+sqlalchemy.url = sqlite:////var/lib/luci/data/luci.db
+sqlalchemy.echo = false
+sqlalchemy.echo_pool = false
+sqlalchemy.pool_recycle = 3600
+
+templating.mako.reloadfromdisk = false
+
+# the compiled template dir is a directory that must be readable by your
+# webserver. It will be used to store the resulting templates once compiled
+# by the TemplateLookup system.
+# During development you generally don't need this option since paste's HTTP
+# server will have access to you development directories, but in production
+# you'll most certainly want to have apache or nginx to write in a directory
+# that does not contain any source code in any form for obvious security reasons.
+#
+#templating.mako.compiled_templates_dir = /some/dir/where/webserver/has/access
+
+# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
+# Debug mode will enable the interactive debugging tool, allowing ANYONE to
+# execute malicious code after an exception is raised.
+set debug = false
+
+# Logging configuration
+# Add additional loggers, handlers, formatters here
+# Uses python's logging config file format
+# http://docs.python.org/lib/logging-config-fileformat.html
+
+[loggers]
+keys = root, luci, sqlalchemy, auth
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+# If you create additional loggers, add them as a key to [loggers]
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_luci]
+level = DEBUG
+handlers =
+qualname = luci
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+
+# A logger for authentication, identification and authorization -- this is
+# repoze.who and repoze.what:
+[logger_auth]
+level = WARN
+handlers =
+qualname = auth
+
+# If you create additional handlers, add them as a key to [handlers]
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+# If you create additional formatters, add them as a key to [formatters]
+[formatter_generic]
+format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/init.d/Makefile b/init.d/Makefile
new file mode 100644
index 0000000..0fdd546
--- /dev/null
+++ b/init.d/Makefile
@@ -0,0 +1,10 @@
+TARGET = luci
+sysconfdir ?= ${DESTDIR}/etc
+
+all:
+
+install:
+ /usr/bin/install -d ${sysconfdir}/rc.d/init.d
+ /usr/bin/install ${TARGET} ${sysconfdir}/rc.d/init.d
+
+clean:
diff --git a/init.d/luci b/init.d/luci
new file mode 100755
index 0000000..4c6e7c6
--- /dev/null
+++ b/init.d/luci
@@ -0,0 +1,172 @@
+#!/bin/bash
+#
+# luci - Luci cluster management application init script
+#
+# chkconfig: - 25 78
+# description: Starts and stops luci
+#
+#
+### BEGIN INIT INFO
+# Provides: luci
+# Required-Start: $network $time
+# Required-Stop: $network $time
+# Default-Start:
+# Default-Stop:
+# Short-Description: Starts and stops luci
+# Description: Starts and stops the luci cluster management application
+### END INIT INFO
+
+PATH="/bin:/usr/bin:/sbin:/usr/sbin:/usr/sbin"
+
+# Defaults for luci. These can be overridden by the contents of
+# /etc/sysconfig/luci (or /etc/default/luci on deb-based distributions)
+LUCI_USER=luci
+LUCI_GROUP=luci
+
+LUCI_DATA_DIR=/var/lib/luci
+LUCI_CONF_DIR=/var/lib/luci/etc
+LUCI_LOG_DIR="$LUCI_DATA_DIR/log"
+LUCI_CERT_DIR="$LUCI_DATA_DIR/certs"
+
+LUCI_CONFIG_FILE="$LUCI_CONF_DIR/luci.ini"
+LUCI_DB_FILE="$LUCI_DATA_DIR/data/luci.db"
+LUCI_PID_FILE="$LUCI_DATA_DIR/data/luci.pid"
+LUCI_PASTER_LOG="$LUCI_LOG_DIR/luci.log"
+
+LUCI_CERT_CONFIG="$LUCI_CONF_DIR/cacert.config"
+LUCI_CERT_PRIV="$LUCI_CERT_DIR/privkey.pem"
+LUCI_CERT_PUB="$LUCI_CERT_DIR/cacert.pem"
+LUCI_CERT_KEY_LIFE_DAYS='1825'
+LUCI_CERT_KEY_BITS='2048'
+
+if [ -d /etc/sysconfig ]; then
+ [ -f /etc/sysconfig/luci ] && . /etc/sysconfig/luci
+elif [ -d /etc/default ]; then
+ [ -f /etc/default/luci ] && . /etc/default/luci
+fi
+
+# Create the luci database if luci has not previously run (or the database
+# has disappeared).
+luci_init() {
+ if [ ! -f "$LUCI_CERT_PRIV" ]; then
+ /usr/bin/openssl genrsa -out "$LUCI_CERT_PRIV" "$LUCI_CERT_KEY_BITS" >&/dev/null
+ if [ $? -ne 0 ] ; then
+ rm -f -- "$LUCI_CERT_PRIV" >& /dev/null
+ echo "Unable to generate the luci private certificate file."
+ return 1
+ fi
+ chmod 600 "$LUCI_CERT_PRIV"
+ chown $LUCI_USER:$LUCI_GROUP "$LUCI_CERT_PRIV"
+ if [ $? -ne 0 ]; then
+ echo "Unable to change ownership of the luci private certificate file."
+ rm -f -- "$LUCI_CERT_PRIV" >& /dev/null
+ return 1
+ fi
+ fi
+
+ if [ ! -f "$LUCI_CERT_PUB" ]; then
+ /usr/bin/openssl req -new -x509 -key "$LUCI_CERT_PRIV" -out "$LUCI_CERT_PUB" -days "$LUCI_CERT_KEY_LIFE_DAYS" -set_serial "$(/bin/date +%s)" -config "$LUCI_CERT_CONFIG"
+ if [ $? -ne 0 ]; then
+ rm -f -- "$LUCI_CERT_PUB" >& /dev/null
+ echo "Unable to generate the luci public certificate file."
+ return 1
+ fi
+ chmod 600 "$LUCI_CERT_PUB"
+ chown $LUCI_USER:$LUCI_GROUP "$LUCI_CERT_PUB"
+ if [ $? -ne 0 ]; then
+ rm -f -- "$LUCI_CERT_PUB" >& /dev/null
+ echo "Unable to change ownership of the luci public certificate file."
+ return 1
+ fi
+ fi
+
+ if [ ! -f "$LUCI_DB_FILE" ]; then
+ paster setup-app "$LUCI_CONFIG_FILE" >& /dev/null
+ if [ $? -ne 0 ]; then
+ echo "Unable to create the luci database file."
+ return 1
+ fi
+ chown $LUCI_USER:$LUCI_GROUP "$LUCI_DB_FILE"
+ if [ $? -ne 0 ]; then
+ echo "Unable to change ownership of the luci database file."
+ return 1
+ fi
+ fi
+ return 0
+}
+
+luci_start() {
+ luci_status >& /dev/null
+ if [ $? -eq 0 ]; then
+ # echo already started
+ return 0
+ fi
+
+ luci_init
+ if [ $? -ne 0 ]; then
+ return $?
+ fi
+ /usr/bin/paster serve --daemon --user "$LUCI_USER" --group "$LUCI_GROUP" "$LUCI_CONFIG_FILE" --log-file="$LUCI_PASTER_LOG" --pid-file="$LUCI_PID_FILE" >/dev/null
+ return $?
+}
+
+luci_stop() {
+ luci_status >& /dev/null
+ if [ $? -ne 0 ]; then
+ # already stopped
+ return 0
+ else
+ /usr/bin/paster serve --stop-daemon --daemon --user "$LUCI_USER" --group "$LUCI_GROUP" "$LUCI_CONFIG_FILE" --log-file="$LUCI_PASTER_LOG" --pid-file="$LUCI_PID_FILE" >/dev/null
+ return $?
+ fi
+}
+
+luci_restart() {
+ luci_status >& /dev/null
+ if [ $? -ne 0 ]; then
+ luci_stop || return 1
+ fi
+ luci_start
+ return $?
+}
+
+luci_status() {
+ out=`/usr/bin/paster serve --status --daemon --user "$LUCI_USER" --group "$LUCI_GROUP" "$LUCI_CONFIG_FILE" --log-file="$LUCI_PASTER_LOG" --pid-file="$LUCI_PID_FILE"`
+ ret=$?
+ echo "$out" | tail -1
+ return $ret
+}
+
+ret=0
+
+case "$1" in
+start)
+ luci_start
+ ret=$?
+;;
+stop)
+ luci_stop
+ ret=$?
+;;
+restart|reload|force-reload)
+ luci_restart
+ ret=$?
+;;
+condrestart)
+ luci_status >& /dev/null
+ if [ $? -eq 0 ]; then
+ luci_restart
+ ret=$?
+ fi
+;;
+status)
+ luci_status
+ ret=$?
+;;
+*)
+ echo "Usage: $0 {start|stop|reload|restart|status}"
+ ret=3
+;;
+esac
+
+exit $ret
diff --git a/luci.spec b/luci.spec
new file mode 100644
index 0000000..8537eb0
--- /dev/null
+++ b/luci.spec
@@ -0,0 +1,88 @@
+%{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
+%{!?pyver: %define pyver %(%{__python} -c "import sys ; print sys.version[:3]")}
+
+Name: luci
+Version: 0.21.1
+Release: 1%{?dist}
+Summary: Web-based cluster administration application
+Group: Applications/System
+License: GPLv2
+URL: http://sources.redhat.com/cluster/conga
+Source0: http://people.redhat.com/rmccabe/luci/luci-0.21.0.tar.bz2
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+BuildArch: noarch
+BuildRequires: python-devel python-setuptools python-paste python-paste-script
+Requires: TurboGears2 python-tw-jquery python-repoze-what-quickstart python-catwalk openssl
+Requires(post): chkconfig initscripts
+Requires(preun): chkconfig initscripts
+Requires(postun): initscripts
+
+%description
+Luci is a web-based cluster administration application built on the
+TurboGears 2 framework.
+
+%prep
+%setup -q
+
+%build
+python setup.py build
+
+%install
+rm -rf %{buildroot}
+python setup.py install --skip-build --root %{buildroot}
+cd init.d && make DESTDIR=%{buildroot} install;cd ..
+cd config && make DESTDIR=%{buildroot} install;cd ..
+
+%clean
+rm -rf %{buildroot}
+
+%files
+%defattr(-,root,root,-)
+%doc README.txt COPYING
+%{python_sitelib}/%{name}-%{version}-py%{pyver}.egg-info/
+%{python_sitelib}/luci
+
+%config(noreplace) %{_localstatedir}/lib/luci/etc/luci.ini
+%attr(0600,luci,luci) %{_localstatedir}/lib/luci/etc/luci.ini
+%config(noreplace) %{_localstatedir}/lib/luci/etc/cacert.config
+%attr(0600,luci,luci) %{_localstatedir}/lib/luci/etc/cacert.config
+%attr(0750,luci,luci) %{_localstatedir}/lib/luci
+%config(noreplace) %{_sysconfdir}/rc.d/init.d/luci
+
+
+%pre
+getent group luci >/dev/null || groupadd -r luci
+getent passwd luci >/dev/null || useradd -r -g luci -d /var/lib/luci -s /sbin/nologin -c "luci user" luci
+exit 0
+
+%post
+/sbin/chkconfig --add luci
+exit 0
+
+%preun
+if [ "$1" == "0" ]; then
+ /sbin/service luci stop >&/dev/null
+ /sbin/chkconfig --del luci
+fi
+exit 0
+
+%postun
+if [ "$1" == "1" ]; then
+ /sbin/service luci condrestart >&/dev/null
+fi
+exit 0
+
+%changelog
+* Wed Nov 04 2009 Ryan McCabe <rmccabe(a)redhat.com> - 0.21.0-4
+- And again.
+
+* Wed Nov 04 2009 Ryan McCabe <rmccabe(a)redhat.com> - 0.21.0-2
+- Fix missing build dep.
+
+* Tue Nov 03 2009 Ryan McCabe <rmccabe(a)redhat.com> - 0.21.0-1
+- Add init script.
+- Run as the luci user, not root.
+- Turn off debugging.
+
+* Fri Sep 25 2009 Ryan McCabe <rmccabe(a)redhat.com> - 0.20.0-1
+- Initial build.
diff --git a/luci/controllers/async.py b/luci/controllers/async.py
new file mode 100644
index 0000000..8249d90
--- /dev/null
+++ b/luci/controllers/async.py
@@ -0,0 +1,23 @@
+from tg import flash, request, require, redirect, expose, tmpl_context, app_globals, validate
+from pylons.i18n import ugettext as _, lazy_ugettext as l_
+from repoze.what.predicates import not_anonymous
+
+from luci.lib.base import BaseController
+
+__all__ = ['AsyncController']
+
+class AsyncController(BaseController):
+ allow_only = not_anonymous()
+
+ @expose("json")
+ def get_cluster_nodes(self, **kw):
+ from luci.lib.async_helpers import get_node_list
+
+ host = kw.get('host')
+ if not host:
+ return { 'errors': _('No host was given') }
+ passwd = kw.get('passwd')
+ if not passwd:
+ return { 'errors': _('No password was given for host "%s"') % host }
+
+ return get_node_list(kw.get('host'), kw.get('passwd'))
diff --git a/luci/controllers/cluster.py b/luci/controllers/cluster.py
index 18ac876..104024c 100644
--- a/luci/controllers/cluster.py
+++ b/luci/controllers/cluster.py
@@ -7,16 +7,17 @@ from tg import expose, redirect, validate, flash, app_globals, validate, tmpl_co
# third party imports
#from pylons.i18n import ugettext as _
#from repoze.what import predicates
+from repoze.what.predicates import not_anonymous
# project specific imports
from luci.lib.base import BaseController
-#from luci.model import DBSession, metadata
+from luci.lib.db_helpers import get_cluster_list, get_model_for_cluster, get_status_for_cluster
data = app_globals.data
class ClusterController(BaseController):
#Uncomment this line if your controller requires an authenticated user
- #allow_only = authorize.not_anonymous()
+ allow_only = not_anonymous()
@expose('luci.templates.cluster_list')
def index(self):
@@ -26,15 +27,17 @@ class ClusterController(BaseController):
def lookup(self, name, *args):
print name
print args
- if name in data.clusters.iterkeys():
- icc = IndividualClusterController(name,data)
- return icc, args
- cc = ClusterController
- status = 'error'
- msg = "Bad cluster name"
- flash (msg, status=status)
- redirect('/cluster')
- return (cc,args)
+ cluster_list = get_cluster_list()
+ for i in cluster_list:
+ if i.name == name:
+ icc = IndividualClusterController(name, data)
+ return icc, args
+ cc = ClusterController
+ status = 'error'
+ msg = "No cluster named \"%s\" is managed by luci" % name
+ flash (msg, status=status)
+ redirect('/cluster')
+ return (cc,args)
class IndividualClusterController(BaseController):
@@ -45,205 +48,222 @@ class IndividualClusterController(BaseController):
@expose("luci.templates.node")
def default(self):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/'
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/'
return dict(page='nodes',name=self.nodename, base_url = '/nodes')
@expose("luci.templates.node")
def nodes(self):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/'
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/'
return dict(page='nodes',name=self.nodename, base_url = '/nodes')
# This processes all of the commands that we can apply to a node
@expose("luci.templates.node")
def nodes_cmd(self, command=None, **kw):
print kw
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/'
- # If we're using the checkboxes, we control multiple nodes at once
- if "MultiAction" in kw:
- print "MAction!"
- kw["name"] = ""
- nodelist = ""
- command = kw["MultiAction"]
- for key,element in kw.items():
- if element == "on":
- nodelist = nodelist + " " + key
- if command == 'Reboot':
- flash ("Rebooting: " + nodelist)
- if command == 'Fence':
- flash ("Fencing: " + nodelist)
- if command == 'Leave Cluster':
- flash ("Removing from Cluster: " + nodelist)
- if command == 'Delete':
- flash ("Deleting: " + nodelist)
- else:
- if command == 'add':
- flash ("Adding node on " + tmpl_context.cluster_name)
- if command == 'Delete':
- flash ("Deleting: " + kw["name"] + " (but not really)")
- if command == 'Fence':
- flash ("Fencing: " + kw["name"] + " (but not really)")
- if command == 'Leave':
- flash ("Leaving: " + kw["name"] + " (but not really)")
- if command == 'Reboot':
- flash ("Rebooting: " + kw["name"] + " (but not really)")
- redirect (tmpl_context.cluster_url + kw["name"])
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/'
+ # If we're using the checkboxes, we control multiple nodes at once
+ if "MultiAction" in kw:
+ print "MAction!"
+ kw["name"] = ""
+ nodelist = ""
+ command = kw["MultiAction"]
+ for key,element in kw.items():
+ if element == "on":
+ nodelist = nodelist + " " + key
+ if command == 'Reboot':
+ flash ("Rebooting: " + nodelist)
+ elif command == 'Fence':
+ flash ("Fencing: " + nodelist)
+ elif command == 'Leave Cluster':
+ flash ("Removing from Cluster: " + nodelist)
+ elif command == 'Delete':
+ flash ("Deleting: " + nodelist)
+ else:
+ if command == 'add':
+ flash ("Adding node on " + tmpl_context.cluster_name)
+ elif command == 'Delete':
+ flash ("Deleting: " + kw["name"])
+ elif command == 'Fence':
+ flash ("Fencing: " + kw["name"])
+ elif command == 'Leave':
+ flash ("Leaving: " + kw["name"])
+ elif command == 'Reboot':
+ flash ("Rebooting: " + kw["name"])
+ redirect (tmpl_context.cluster_url + kw["name"])
@expose("luci.templates.service")
def services(self,*args):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/'
- if (len(args) == 1):
- servicename = args[0]
- print "Servicename: " + servicename
- else:
- servicename = None
- print "No service name"
- base_url = '/cluster/' + self.name + '/services'
- services_cmd = '/cluster/' + self.name + '/services_cmd'
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/'
+
+ if (len(args) == 1):
+ servicename = args[0]
+ print "Servicename: " + servicename
+ else:
+ servicename = None
+ print "No service name"
+
+ base_url = '/cluster/' + self.name + '/services'
+ services_cmd = '/cluster/' + self.name + '/services_cmd'
return dict(page='nodes',name=servicename, base_url = base_url, services_cmd = services_cmd)
@expose("luci.templates.service")
def services_cmd(self,command=None,**kw):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/services'
- # If we're using the checkboxes, we control multiple nodes at once
- if "MultiAction" in kw:
- print "MAction!"
- kw["name"] = ""
- nodelist = ""
- command = kw["MultiAction"]
- for key,element in kw.items():
- if element == "on":
- nodelist = nodelist + " " + key
- if command == 'Reboot':
- flash ("Rebooting Service: " + nodelist)
- if command == 'Delete':
- flash ("Deleting Service: " + nodelist)
- redirect (tmpl_context.cluster_url)
- else:
- if command == 'Delete':
- flash ("Deleting: " + kw["name"] + " (but not really)")
- if command == 'Reboot':
- flash ("Rebooting: " + kw["name"] + " (but not really)")
- redirect (tmpl_context.cluster_url + '/' + kw["name"])
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/services'
+
+ # If we're using the checkboxes, we control multiple nodes at once
+ if "MultiAction" in kw:
+ print "MAction!"
+ kw["name"] = ""
+ nodelist = ""
+ command = kw["MultiAction"]
+ for key,element in kw.items():
+ if element == "on":
+ nodelist = nodelist + " " + key
+
+ if command == 'Reboot':
+ flash ("Rebooting Service: " + nodelist)
+ elif command == 'Delete':
+ flash ("Deleting Service: " + nodelist)
+ redirect (tmpl_context.cluster_url)
+ else:
+ if command == 'Delete':
+ flash ("Deleting: " + kw["name"])
+ elif command == 'Reboot':
+ flash ("Rebooting: " + kw["name"])
+ redirect (tmpl_context.cluster_url + '/' + kw["name"])
@expose("luci.templates.failover")
def failovers(self,*args):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/'
- if (len(args) == 1):
- failovername = args[0]
- else:
- failovername = None
- failovers_cmd = '/cluster/' + self.name + '/failovers_cmd'
- return dict(page='nodes',name=failovername, base_url = '/cluster/' + self.name + '/failovers',
- failovers_cmd = failovers_cmd)
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/'
+ if len(args) == 1:
+ failovername = args[0]
+ else:
+ failovername = None
+ failovers_cmd = '/cluster/' + self.name + '/failovers_cmd'
+ return dict(page='nodes',name=failovername, base_url = '/cluster/' + self.name + '/failovers', failovers_cmd = failovers_cmd)
@expose("luci.templates.failover")
def failovers_cmd(self,command=None,**kw):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/failovers'
- # If we're using the checkboxes, we control multiple nodes at once
- if "MultiAction" in kw:
- print "MAction!"
- kw["name"] = ""
- nodelist = ""
- command = kw["MultiAction"]
- for key,element in kw.items():
- if element == "on":
- nodelist = nodelist + " " + key
- if command == 'Delete':
- flash ("Deleting Failover: " + nodelist)
- redirect (tmpl_context.cluster_url)
- else:
- if command == 'Delete':
- flash ("Deleting: " + kw["name"] + " (but not really)")
- if command == 'update_properties':
- flash ("Updating Properties...")
- redirect (tmpl_context.cluster_url + '/' + kw["name"])
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/failovers'
+ # If we're using the checkboxes, we control multiple nodes at once
+
+ if "MultiAction" in kw:
+ print "MAction!"
+ kw["name"] = ""
+ nodelist = ""
+ command = kw["MultiAction"]
+
+ for key,element in kw.items():
+ if element == "on":
+ nodelist = nodelist + " " + key
+
+ if command == 'Delete':
+ flash ("Deleting Failover: " + nodelist)
+ redirect (tmpl_context.cluster_url)
+ else:
+ if command == 'Delete':
+ flash ("Deleting: " + kw["name"])
+ elif command == 'update_properties':
+ flash ("Updating Properties...")
+ redirect (tmpl_context.cluster_url + '/' + kw["name"])
@expose("luci.templates.fence")
def fences(self,*args):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/'
- if (len(args) == 1):
- fencename = args[0]
- else:
- fencename = None
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/'
+
+ if len(args) == 1:
+ fencename = args[0]
+ else:
+ fencename = None
- fences_cmd = '/cluster/' + self.name + '/fences_cmd'
- return dict(page='nodes',name=fencename, base_url = '/cluster/' + self.name + '/fences',
- fences_cmd = fences_cmd)
+ fences_cmd = '/cluster/' + self.name + '/fences_cmd'
+ return dict(page='nodes',name=fencename, base_url = '/cluster/' + self.name + '/fences', fences_cmd = fences_cmd)
@expose("luci.templates.fence")
def fences_cmd(self,command=None,**kw):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/fences'
- # If we're using the checkboxes, we control multiple nodes at once
- if "MultiAction" in kw:
- print "MAction!"
- kw["name"] = ""
- nodelist = ""
- command = kw["MultiAction"]
- for key,element in kw.items():
- if element == "on":
- nodelist = nodelist + " " + key
- if command == 'Delete':
- flash ("Deleting Fences: " + nodelist)
- if command == 'Update':
- flash ("Updating Fences: " + nodelist)
- redirect (tmpl_context.cluster_url)
- else:
- if command == 'Delete':
- flash ("Deleting: " + kw["name"] + " (but not really)")
- if command == 'Update':
- flash ("Updating: " + kw["name"] + " (but not really)")
- redirect (tmpl_context.cluster_url + '/' + kw["name"])
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/fences'
+ # If we're using the checkboxes, we control multiple nodes at once
+ if "MultiAction" in kw:
+ print "MAction!"
+ kw["name"] = ""
+ nodelist = ""
+ command = kw["MultiAction"]
+ for key,element in kw.items():
+ if element == "on":
+ nodelist = nodelist + " " + key
+ if command == 'Delete':
+ flash ("Deleting Fences: " + nodelist)
+ elif command == 'Update':
+ flash ("Updating Fences: " + nodelist)
+ redirect (tmpl_context.cluster_url)
+ else:
+ if command == 'Delete':
+ flash ("Deleting: " + kw["name"])
+ elif command == 'Update':
+ flash ("Updating: " + kw["name"])
+ redirect (tmpl_context.cluster_url + '/' + kw["name"])
@expose("luci.templates.configure")
- def configure(self,*args):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/'
- configure_cmd = '/cluster/' + self.name + '/configure_cmd'
- return dict(page='nodes',name='configure', base_url = '/cluster/' + self.name + '/configure',
- configure_cmd = configure_cmd)
+ def configure(self,*args):
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/'
+ configure_cmd = '/cluster/' + self.name + '/configure_cmd'
+ return dict(page='nodes',name='configure', base_url = '/cluster/' + self.name + '/configure', configure_cmd = configure_cmd)
@expose("luci.templates.configure")
def configure_cmd(self,command=None,*args,**kw):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/configure'
- print command
- print kw
- print args
- print "Configure Command"
- flash ("Applying Settings: (but not really)")
- if command == 'Update':
- flash ("Updating: (but not really)")
- redirect (tmpl_context.cluster_url)
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/configure'
+ print command
+ print kw
+ print args
+ print "Configure Command"
+ flash ("Applying Settings:")
+ if command == 'Update':
+ flash ("Updating:")
+ redirect (tmpl_context.cluster_url)
@expose()
def lookup(self, nodename, *args):
clustername = self.name
- tmpl_context.cluster_data=data.clusters[clustername]
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
tmpl_context.cluster_name = clustername
- tmpl_context.cluster_url = "/cluster/" + clustername + '/'
- inc = IndividualNodeController(clustername,self.data,nodename)
- return inc, args
+ tmpl_context.cluster_url = "/cluster/" + clustername + '/'
+ inc = IndividualNodeController(clustername,self.data,nodename)
+ return inc, args
class IndividualNodeController(BaseController):
def __init__(self,name,data,nodename):
@@ -253,15 +273,17 @@ class IndividualNodeController(BaseController):
@expose("luci.templates.node")
def default(self):
- tmpl_context.cluster_data=data.clusters[self.name]
- tmpl_context.cluster_name = self.name
- tmpl_context.cluster_url = "/cluster/" + self.name + '/'
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
+ tmpl_context.cluster_name = self.name
+ tmpl_context.cluster_url = "/cluster/" + self.name + '/'
return dict(page='nodes',name=self.nodename, base_url = '/nodes')
@expose()
def lookup(self, nodename, *args):
clustername = self.name
- tmpl_context.cluster_data=data.clusters[clustername]
+ tmpl_context.cluster_data = get_model_for_cluster(self.name)
+ tmpl_context.cluster_status = get_status_for_cluster(self.name)
tmpl_context.cluster_name = clustername
- icc = IndividualClusterController(clustername,self.data,nodename)
- return icc, args
+ icc = IndividualClusterController(clustername,self.data,nodename)
+ return icc, args
diff --git a/luci/controllers/cluster_part/__init__.py b/luci/controllers/cluster_part/__init__.py
new file mode 100644
index 0000000..f1c4df0
--- /dev/null
+++ b/luci/controllers/cluster_part/__init__.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+"""Subcontrollers for `clusters' part."""
+
+from node import NodeController, NodeCmdController
+from service import ServiceController, ServiceCmdController
+from failover import FailoverController, FailoverCmdController
+from fence import FenceController, FenceCmdController
+
+__all__ = ['NodeController', 'NodeCmdController',
+ 'ServiceController', 'ServiceCmdController',
+ 'FailoverController', 'FailoverCmdController',
+ 'FenceController', 'FenceCmdController']
diff --git a/luci/controllers/cluster_part/failover.py b/luci/controllers/cluster_part/failover.py
new file mode 100644
index 0000000..6a8072f
--- /dev/null
+++ b/luci/controllers/cluster_part/failover.py
@@ -0,0 +1,299 @@
+# -*- coding: utf-8 -*-
+
+"""Controller API for `failovers of certain cluster' part.
+
+ Functionality description:
+
+ Let's have the main subcontroller (FailoverController) available through
+ 'failovers' identifier of the `clusters' controller and the subcontroller
+ handling related commands (FailoverCmdController) through 'failovers_cmd'
+ identifier.
+
+ Main page of certain cluster (`ClusterOne') is accessible via
+ http://example.com/clusters/ClusterOne. Failovers in the cluster are
+ Failover1 and Failover2.
+
+ Accessing following URLs has following consequences (some cases omitted):
+
+ 1) <http://example.com/clusters/ClusterOne/failovers>
+ OR <http://example.com/clusters/ClusterOne/failovers/>
+ - displaying a list of failovers with some information about them.
+
+ 2) <http://example.com/clusters/ClusterOne/failovers/Failover1>
+ OR <http://example.com/clusters/ClusterOne/failovers/Failover1/>
+ - the same list as in case 1) is displayed, but also details to failover
+ `Failover1' are shown in the lower part of the page.
+
+ 3) <http://example.com/clusters/ClusterOne/failovers/NonExistingFailover>
+ OR <http://example.com/clusters/ClusterOne/failovers/NonExistingFailover/>
+ OR <http://example.com/clusters/ClusterOne/failovers/NonExistingFailover/some...>
+ - redirection back to base `failovers' page and displaying information
+ about the request of non-existing failover.
+
+ ---
+
+ 4) <http://example.com/clusters/ClusterOne/failovers_cmd/add>
+ - display form for a failover addition.
+
+ 5) <http://example.com/clusters/ClusterOne/failovers_cmd/update_properties>
+ - apply updating properties of certain failover.
+
+ 6) <http://example.com/clusters/ClusterOne/failovers_cmd/update_members>
+ - apply updating members of certain failover.
+
+ 7) <http://example.com/clusters/ClusterOne/failovers_cmd/apply>
+ OR <http://example.com/clusters/ClusterOne/failovers_cmd/apply?CMDID...>
+ - apply a command on given failover(s); both GET and POST method
+ supported.
+
+"""
+from tg import flash, redirect, expose, tmpl_context, app_globals
+from pylons.i18n import ugettext as _, lazy_ugettext as l_
+from repoze.what import predicates
+
+from luci.lib.base import SubController, SubControllerApplyMixin, \
+ ApplyCommands, setApplyCommands
+from luci.controllers.decorators import *
+from luci.lib.app_strings import Title, FlashMsg
+from luci.lib.helpers import relativeUrlList2Str
+
+__all__ = ['FailoverController', 'FailoverCmdController']
+
+
+# Imports into module's namespace.
+form_utils = app_globals.form_utils
+cluster_scheme = app_globals.scheme.Cluster
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+#
+# ITEMS LISTING.
+#
+
+class FailoverController(SubController):
+ """Subcontroller handling basic requests related with `failovers' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.failover')
+ def default(self):
+ """Handle simple failovers listing.
+
+ E.g. <http://example.com/clusters/ClusterOne/failovers> for situation
+ described in the module's doc.
+
+ """
+ Title.usePart('FAILOVERS')
+
+ base_url_str = relativeUrlList2Str(tmpl_context.cluster_url,
+ cluster_scheme.FAILOVERS)
+ cmd_url_str = relativeUrlList2Str(tmpl_context.cluster_url,
+ base2CmdCtrl(cluster_scheme.FAILOVERS))
+ service_cmd_url_str = \
+ relativeUrlList2Str(tmpl_context.cluster_url,
+ base2CmdCtrl(cluster_scheme.SERVICES))
+
+ return dict(name=None, page='nodes',
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ service_cmd_url=service_cmd_url_str,
+ apply_cmds=FailoverApplyCommands)
+
+
+ @expose()
+ def lookup(self, name, *args):
+ """Handle failovers listing with details of certain failover.
+
+ E.g. <http://example.com/clusters/ClusterOne/failovers/Failover1>
+ for situation described in the module's doc.
+
+ The request is dynamically delegated to _CertainFailoverController
+ object.
+
+ Keyword arguments:
+ name Name of the failover to be displayed in more detail.
+
+ """
+ # Check whether required failover exists.
+ if tmpl_context.cluster_data.failovers.has_key(name):
+ dynamic_ctrl = _CertainFailoverController(entity_name=name)
+ return dynamic_ctrl, args
+ else:
+ msg = l_('Bad node name.')
+ status = 'error'
+
+ flash(msg, status=status)
+ redirect(relativeUrlList2Str(tmpl_context.cluster_url,
+ cluster_scheme.FAILOVERS))
+
+
+class _CertainFailoverController(SubController):
+ """Subcontroller handling failovers listing with details of certain failover."""
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.failover')
+ def default(self):
+ """Handle failovers listing with details of certain failover.
+
+ E.g. <http://example.com/clusters/ClusterOne/failovers/Failover1>
+ for situation described in the module's doc.
+
+ """
+ Title.usePart('CERTAIN_FAILOVER', self.entity_name)
+
+ base_url_str = relativeUrlList2Str(tmpl_context.cluster_url,
+ cluster_scheme.FAILOVERS)
+ cmd_url_str = relativeUrlList2Str(tmpl_context.cluster_url,
+ base2CmdCtrl(cluster_scheme.FAILOVERS))
+ service_cmd_url_str = \
+ relativeUrlList2Str(tmpl_context.cluster_url,
+ base2CmdCtrl(cluster_scheme.SERVICES))
+
+ return dict(name=self.entity_name, page='nodes',
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ service_cmd_url=service_cmd_url_str,
+ apply_cmds=FailoverApplyCommands)
+
+
+ @expose()
+ def lookup(self, *args):
+ """Handle wrong URL (longer than expected) connected with certain failover.
+
+ E.g. <http://example.com/clusters/ClusterOne/failovers/Failover1/something>
+ for situation described in the module's doc.
+
+ """
+ redirect(relativeUrlList2Str(tmpl_context.cluster_url,
+ cluster_scheme.FAILOVERS,
+ self.entity_name))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class FailoverApplyCommands(ApplyCommands):
+ """Encapsulation of available commands to apply over selected failover(s)."""
+
+ DELETE = 'cmd_delete'
+
+
+ def _delete(which):
+ failovers = tmpl_context.cluster_data.failovers
+ msg = l_('Deleting selected failover(s)... %s') \
+ % (', '.join(which))
+ status = 'info'
+ for failover in which:
+ if failovers.has_key(failover):
+ del failovers[failover]
+ else:
+ # If Luci's data are consistent, this should never happen.
+ msg, status = FlashMsg.INTERNAL_ERROR
+ break
+ return msg, status, False
+
+
+ # Mapping.
+ cmds = {DELETE: _delete}
+
+ # Set methods as static (but only after they are referenced
+ # in 'cmds' dict).
+ _delete = staticmethod(_delete)
+
+
+@setApplyCommands(FailoverApplyCommands)
+class FailoverCmdController(SubController, SubControllerApplyMixin):
+ """Subcontroller handling commands related with `failovers' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def default(self, *args, **kwargs):
+ """Handle requests not connected with any of defined method."""
+
+ flash(*FlashMsg.BAD_COMMAND_REQUEST)
+
+ base_url_str = relativeUrlList2Str(tmpl_context.cluster_url, self.base_name)
+ return dict(redir_target=base_url_str)
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def add(self):
+ """Handle creation of a new failover domain."""
+
+ flash('It should be a dialog to add new failover domain here instead, '
+ 'but not implemented yet...', status='info')
+ # following code is only my experiment, how to continue:
+ #tmpl_context.form = create_fdom_add_form
+ #return dict()
+
+
+ @forImmediateRedirect(allowed_methods = 'POST')
+ def update_properties(self, name=None, **kwargs):
+ """Handle changes in a failover domain -- properties.
+
+ Keyword arguments:
+ name Name of the failover, which attributes are being changed.
+ kwargs Dict of attributes that are active.
+
+ """
+ if not name:
+ flash(*FlashMsg.INTERNAL_ERROR)
+ else:
+ failovers = tmpl_context.cluster_data.failovers
+ failover = form_utils.id2String(name)
+ if failovers.has_key(failover):
+ for attr in ['prioritized', 'restricted', 'failback']:
+ if kwargs.has_key(attr):
+ failovers[failover][attr] = True
+ else:
+ failovers[failover][attr] = False
+ flash(_('Updating properties for %s') % failover, status='info')
+ else:
+ # If the Luci's data are consistent this should never happen.
+ flash(*FlashMsg.INTERNAL_ERROR)
+
+
+ @forImmediateRedirect(allowed_methods = 'POST')
+ def update_members(self, name=None, **kwargs):
+ """Handle changes in a failover domain -- member nodes.
+
+ Keyword arguments:
+ name Name of the failover, which members are being changed.
+ kwargs Dict of values of members.
+
+ """
+ if not name:
+ flash(*FlashMsg.INTERNAL_ERROR)
+ else:
+ failovers = tmpl_context.cluster_data.failovers
+ failover = form_utils.id2String(name)
+ if failovers.has_key(failover):
+ # Clear the dictionary of member nodes and start from scratch.
+ failovers[failover]['nodes'].clear()
+ nodes = \
+ map(lambda node_id: (node_id, form_utils.id2String(node_id)),
+ map(lambda node_id: node_id.replace(u'.check', u''),
+ filter(lambda param: param.endswith(u'.check'),
+ kwargs.iterkeys())))
+ flash(_('Updating members for %s') % failover, status='info')
+ for node_id, node in nodes:
+ if tmpl_context.cluster_data.nodes.has_key(node):
+ failovers[failover]['nodes'][node] = \
+ kwargs.get(node_id + '.priority', 0)
+ else:
+ # If the Luci's data are consistent this should never
+ # happen.
+ flash(*FlashMsg.INTERNAL_ERROR)
+
+
diff --git a/luci/controllers/cluster_part/fence.py b/luci/controllers/cluster_part/fence.py
new file mode 100644
index 0000000..35a0a4a
--- /dev/null
+++ b/luci/controllers/cluster_part/fence.py
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+
+"""Controller API for `fences of certain cluster' part.
+
+ Functionality description:
+
+ Let's have the main subcontroller (FenceController) available through
+ 'fences' identifier of the `clusters' controller and the subcontroller
+ handling related commands (FenceCmdController) through 'fences_cmd'
+ identifier.
+
+ Main page of certain cluster (`ClusterOne') is accessible via
+ http://example.com/clusters/ClusterOne. Fences in the cluster are
+ FenceA and FenceB.
+
+ Accessing following URLs has following consequences (some cases omitted):
+
+ 1) <http://example.com/clusters/ClusterOne/fences>
+ OR <http://example.com/clusters/ClusterOne/fences/>
+ - displaying a list of fences with some information about them.
+
+ 2) <http://example.com/clusters/ClusterOne/fences/FenceA>
+ OR <http://example.com/clusters/ClusterOne/fences/FenceA/>
+ - the same list as in case 1) is displayed, but also details to fence
+ `FenceA' are shown in the lower part of the page.
+
+ 3) <http://example.com/clusters/ClusterOne/fences/NonExistingFence>
+ OR <http://example.com/clusters/ClusterOne/fences/NonExistingFence/>
+ OR <http://example.com/clusters/ClusterOne/fences/NonExistingFence/something_...>
+ - redirection back to base `fences' page and displaying information
+ about the request of non-existing fence.
+
+ ---
+
+ 4) <http://example.com/clusters/ClusterOne/fences_cmd/add>
+ - display form for a fence addition.
+
+ 5) <http://example.com/clusters/ClusterOne/fences_cmd/apply>
+ OR <http://example.com/clusters/ClusterOne/fences_cmd/apply?CMDID...>
+ - apply a command on given node(s); both GET and POST method supported.
+
+"""
+from tg import flash, redirect, expose, tmpl_context, app_globals
+from pylons.i18n import ugettext as _, lazy_ugettext as l_
+from repoze.what import predicates
+
+from luci.lib.base import SubController, SubControllerApplyMixin, \
+ ApplyCommands, setApplyCommands
+from luci.controllers.decorators import *
+from luci.lib.app_strings import Title, FlashMsg
+from luci.lib.helpers import relativeUrlList2Str
+
+
+__all__ = ['FenceController', 'FenceCmdController']
+
+
+# Imports into module's namespace.
+form_utils = app_globals.form_utils
+cluster_scheme = app_globals.scheme.Cluster
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+#
+# ITEMS LISTING.
+#
+
+class FenceController(SubController):
+ """Subcontroller handling basic requests related with `fences' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.fence')
+ def default(self):
+ """Handle simple fences listing.
+
+ E.g. <http://example.com/clusters/ClusterOne/fences> for situation
+ described in the module's doc.
+
+ """
+ Title.usePart('FENCES')
+
+ cluster_url = tmpl_context.cluster_url + u'/'
+
+ return dict(name=None, page='nodes',
+ base_url=cluster_url + cluster_scheme.FENCES,
+ cmd_url=cluster_url + base2CmdCtrl(cluster_scheme.FENCES),
+ node_base_url=cluster_url + cluster_scheme.NODES,
+ apply_cmds=FenceApplyCommands)
+
+
+ @expose()
+ def lookup(self, name, *args):
+ """Handle fences listing with details of certain one.
+
+ E.g. <http://example.com/clusters/ClusterOne/fences/FenceA>
+ for situation described in the module's doc.
+
+ The request is dynamically delegated to _CertainFenceController object.
+
+ Keyword arguments:
+ name Name of the fence to be displayed in more detail.
+
+ """
+ # Check whether required fence exists.
+ if tmpl_context.cluster_data.fences.has_key(name):
+ dynamic_ctrl = _CertainFenceController(entity_name=name)
+ return dynamic_ctrl, args
+ else:
+ msg = l_('Bad fence name.')
+ status = 'error'
+
+ flash(msg, status=status)
+ redirect(relativeUrlList2Str(tmpl_context.cluster_url,
+ cluster_scheme.FENCES))
+
+
+class _CertainFenceController(SubController):
+ """Subcontroller handling fences listing with details of certain one."""
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.fence')
+ def default(self):
+ """Handle fences listing with details of certain one.
+
+ E.g. <http://example.com/clusters/ClusterOne/fences/FenceA>
+ for situation described in the module's doc.
+
+ """
+ Title.usePart('CERTAIN_FENCE', self.entity_name)
+
+ cluster_url = tmpl_context.cluster_url + u'/'
+
+ return dict(name=self.entity_name, page='nodes',
+ base_url=cluster_url + cluster_scheme.FENCES,
+ cmd_url=cluster_url + base2CmdCtrl(cluster_scheme.FENCES),
+ node_base_url=cluster_url + cluster_scheme.NODES,
+ apply_cmds=FenceApplyCommands)
+
+
+ @expose()
+ def lookup(self, *args):
+ """Handle wrong URL (longer than expected) connected with certain fence.
+
+ E.g. <http://example.com/clusters/ClusterOne/nodes/NodeAlpha/something>
+ for situation described in the module's doc.
+
+ """
+ redirect(relativeUrlList2Str(tmpl_context.cluster_url, cluster_scheme.FENCES,
+ self.entity_name))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class FenceApplyCommands(ApplyCommands):
+ """Encapsulation of available commands to apply over selected fence(s)."""
+
+ UPDATE = 'cmd_update'
+ DELETE = 'cmd_delete'
+
+
+ def _update(which):
+ msg = l_('Updating selected fence(s)... %s') \
+ % (", ".join(which))
+ status = 'info'
+ return msg, status
+
+
+ def _delete(which):
+ fences = tmpl_context.cluster_data.fences
+ msg = l_('Deleting selected fence(s)... %s') \
+ % (', '.join(which))
+ status = 'info'
+ for fence in which:
+ if fences.has_key(fence):
+ del fences[fence]
+ else:
+ # If Luci's data are consistent, this should never happen.
+ msg, status = FlashMsg.INTERNAL_ERROR
+ break
+ return msg, status, False
+
+
+ # Mapping.
+ cmds = {UPDATE: _update,
+ DELETE: _delete}
+
+ # Set methods as static (but only after they are referenced
+ # in 'cmds' dict).
+ _update = staticmethod(_update)
+ _delete = staticmethod(_delete)
+
+
+@setApplyCommands(FenceApplyCommands)
+class FenceCmdController(SubController, SubControllerApplyMixin):
+ """Subcontroller handling commands related with `fences' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def default(self, *args, **kwargs):
+ """Handle requests not connected with any of defined method."""
+
+ flash(*FlashMsg.BAD_COMMAND_REQUEST)
+
+ base_url_str = relativeUrlList2Str(tmpl_context.cluster_url, self.base_name)
+ return dict(redir_target=base_url_str)
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def add(self):
+ """Handle creation of a new fence."""
+
+ flash('It should be a dialog to add new fence here instead, but not'
+ ' implemented yet...', status='info')
+
+
+# @forImmediateRedirect(allowed_methods = 'GET')
+# def manage(self):
+# """Handle the nodes management."""
+#
+# flash('Demo of managing nodes...', status='info')
diff --git a/luci/controllers/cluster_part/node.py b/luci/controllers/cluster_part/node.py
new file mode 100644
index 0000000..556d2a2
--- /dev/null
+++ b/luci/controllers/cluster_part/node.py
@@ -0,0 +1,273 @@
+# -*- coding: utf-8 -*-
+
+"""Controller API for `nodes of certain cluster' part.
+
+ Functionality description:
+
+ Let's have the main subcontroller (NodeController) available through
+ 'nodes' identifier of the `clusters' controller and the subcontroller
+ handling related commands (NodeCmdController) through 'nodes_cmd'
+ identifier.
+
+ Main page of certain cluster (`ClusterOne') is accessible via
+ http://example.com/clusters/ClusterOne. Nodes in the cluster are
+ NodeAlpha, NodeBeta and NodeGamma.
+
+ Accessing following URLs has following consequences (some cases omitted):
+
+ 1) <http://example.com/clusters/ClusterOne/nodes>
+ OR <http://example.com/clusters/ClusterOne/nodes/>
+ - displaying a list of nodes with some information about them.
+
+ 2) <http://example.com/clusters/ClusterOne/nodes/NodeAlpha>
+ OR <http://example.com/clusters/ClusterOne/nodes/NodeAlpha/>
+ - the same list as in case 1) is displayed, but also details to node
+ `NodeAlpha' are shown in the lower part of the page.
+
+ 3) <http://example.com/clusters/ClusterOne/nodes/NonExistingNode>
+ OR <http://example.com/clusters/ClusterOne/nodes/NonExistingNode/>
+ OR <http://example.com/clusters/ClusterOne/nodes/NonExistingNode/something_more>
+ - redirection back to base `nodes' page and displaying information
+ about the request of non-existing node.
+
+ ---
+
+ 4) <http://example.com/clusters/ClusterOne/nodes_cmd/add>
+ - display form for a node addition to cluster.
+
+ 5) <http://example.com/clusters/ClusterOne/nodes_cmd/apply>
+ OR <http://example.com/clusters/ClusterOne/nodes_cmd/apply?CMDID...>
+ - apply a command on given node(s); both GET and POST method supported.
+
+"""
+from tg import flash, redirect, expose, tmpl_context, app_globals
+from pylons.i18n import ugettext as _, lazy_ugettext as l_
+from repoze.what import predicates
+
+from luci.lib.base import SubController, SubControllerApplyMixin, \
+ ApplyCommands, setApplyCommands
+from luci.controllers.decorators import *
+from luci.lib.app_strings import Title, FlashMsg
+from luci.lib.helpers import relativeUrlList2Str
+
+__all__ = ['NodeController', 'NodeCmdController']
+
+
+# Imports into module's namespace.
+form_utils = app_globals.form_utils
+cluster_scheme = app_globals.scheme.Cluster
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+#
+# ITEMS LISTING.
+#
+
+class NodeController(SubController):
+ """Subcontroller handling basic requests related with `nodes' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.node')
+ def default(self):
+ """Handle simple nodes listing.
+
+ E.g. <http://example.com/clusters/ClusterOne/nodes> for situation
+ described in the module's doc.
+
+ """
+ Title.usePart('NODES')
+
+ cluster_url = tmpl_context.cluster_url + u'/'
+
+ return dict(page='nodes',name=None,
+ base_url=cluster_url + cluster_scheme.NODES,
+ cmd_url= \
+ cluster_url + base2CmdCtrl(cluster_scheme.NODES),
+ service_cmd_url= \
+ cluster_url + base2CmdCtrl(cluster_scheme.SERVICES),
+ apply_cmds=NodeApplyCommands)
+
+
+ @expose()
+ def lookup(self, name, *args):
+ """Handle nodes listing with details of certain one.
+
+ E.g. <http://example.com/clusters/ClusterOne/nodes/NodeAlpha>
+ for situation described in the module's doc.
+
+ The request is dynamically delegated to _CertainNodeController object.
+
+ Keyword arguments:
+ name Name of the node to be displayed in more detail.
+
+ """
+ # Check whether required node exists.
+ if tmpl_context.cluster_data.nodes.has_key(name):
+ dynamic_ctrl = _CertainNodeController(entity_name=name)
+ return dynamic_ctrl, args
+ else:
+ msg = l_('Bad node name: %s') % name
+ status = 'error'
+
+ flash(msg, status=status)
+ redirect(relativeUrlList2Str(tmpl_context.cluster_url, cluster_scheme.NODES))
+
+
+class _CertainNodeController(SubController):
+ """Subcontroller handling nodes listing with details of certain one."""
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.node')
+ def default(self):
+ """Handle nodes listing with details of certain one.
+
+ E.g. <http://example.com/clusters/ClusterOne/nodes/NodeAlpha>
+ for situation described in the module's doc.
+
+ """
+ Title.usePart('CERTAIN_NODE', self.entity_name)
+
+ cluster_url = tmpl_context.cluster_url + u'/'
+
+ return dict(page='nodes', name=self.entity_name,
+ base_url=cluster_url + cluster_scheme.NODES,
+ cmd_url= \
+ cluster_url + base2CmdCtrl(cluster_scheme.NODES),
+ service_cmd_url= \
+ cluster_url + base2CmdCtrl(cluster_scheme.SERVICES),
+ apply_cmds=NodeApplyCommands)
+
+
+ @expose()
+ def lookup(self, *args):
+ """Handle wrong URL (longer than expected) connected with certain node.
+
+ E.g. <http://example.com/clusters/ClusterOne/nodes/NodeAlpha/something>
+ for situation described in the module's doc.
+
+ """
+ redirect(relativeUrlList2Str(tmpl_context.cluster_url, cluster_scheme.NODES,
+ self.entity_name))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class NodeApplyCommands(ApplyCommands):
+ """Encapsulation of available commands to apply over selected node(s)."""
+
+ REBOOT = 'cmd_reboot'
+ FENCE = 'cmd_fence'
+ LEAVE = 'cmd_leave'
+ DELETE = 'cmd_delete'
+
+
+ def _reboot(which):
+ msg = l_('Rebooting selected node(s)... %s') \
+ % (", ".join(which))
+ status = 'info'
+ return msg, status
+
+
+ def _fence(which):
+ msg = l_('Fencing selected node(s)... %s') \
+ % (", ".join(which))
+ status = 'info'
+ return msg, status
+
+
+ def _leave(which):
+ msg = l_('Removing selected node(s) from the cluster... %s') \
+ % (", ".join(which))
+ status = 'info'
+ return msg, status
+
+
+ def _delete(which):
+ nodes = tmpl_context.cluster_data.nodes
+ msg = l_('Deleting selected node(s)... %s') \
+ % (', '.join(which))
+ status = 'info'
+ for node in which:
+ if nodes.has_key(node):
+ del nodes[node]
+ else:
+ # If Luci's data are consistent, this should never happen.
+ msg, status = FlashMsg.INTERNAL_ERROR
+ break
+ return msg, status, False
+
+
+ # Mapping.
+ cmds = {REBOOT: _reboot,
+ FENCE: _fence,
+ LEAVE: _leave,
+ DELETE: _delete}
+
+ # Set methods as static (but only after they are referenced
+ # in 'cmds' dict).
+ _reboot = staticmethod(_reboot)
+ _fence = staticmethod(_fence)
+ _leave = staticmethod(_leave)
+ _delete = staticmethod(_delete)
+
+
+@setApplyCommands(NodeApplyCommands)
+class NodeCmdController(SubController, SubControllerApplyMixin):
+ """Subcontroller handling commands related with `nodes' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def default(self, *args, **kwargs):
+ """Handle requests not connected with any of defined method."""
+
+ flash(*FlashMsg.BAD_COMMAND_REQUEST)
+
+ base_url_str = relativeUrlList2Str(tmpl_context.cluster_url, self.base_name)
+ return dict(redir_target=base_url_str)
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def add(self):
+ """Handle creation of a new node."""
+
+ flash('It should be a dialog to add new node here instead, '
+ 'but not implemented yet...', status='info')
+ # following code is only my experiment, how to continue:
+ #tmpl_context.form = create_fdom_add_form
+ #return dict()
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def update_settings(self, name=None):
+ """Handle updating of settings."""
+
+ flash('Demo of updating settings for node %s...' % name,
+ status='info')
+
+ base_url_str = relativeUrlList2Str(tmpl_context.cluster_url, self.base_name)
+ return dict(redir_target=base_url_str)
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def update_properties(self, name=None):
+ """Handle updating of properties."""
+
+ flash('Demo of updating properties for node %s...' % name,
+ status='info')
+
+ base_url_str = relativeUrlList2Str(tmpl_context.cluster_url, self.base_name)
+ return dict(redir_target=base_url_str)
+
diff --git a/luci/controllers/cluster_part/service.py b/luci/controllers/cluster_part/service.py
new file mode 100644
index 0000000..2f5b94f
--- /dev/null
+++ b/luci/controllers/cluster_part/service.py
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+
+"""Controller API for `services of certain cluster' part.
+
+ Functionality description:
+
+ Let's have the main subcontroller (ServiceController) available through
+ 'services' identifier of the `clusters' controller and the subcontroller
+ handling related commands (ServiceCmdController) through 'services_cmd'
+ identifier.
+
+ Main page of certain cluster (`ClusterOne') is accessible via
+ http://example.com/clusters/ClusterOne. Services in the cluster are
+ ServiceX, ServiceY and ServiceZ.
+
+ Accessing following URLs has following consequences (some cases omitted):
+
+ 1) <http://example.com/clusters/ClusterOne/services>
+ OR <http://example.com/clusters/ClusterOne/services/>
+ - displaying a list of services with some information about them.
+
+ 2) <http://example.com/clusters/ClusterOne/services/ServiceX>
+ OR <http://example.com/clusters/ClusterOne/services/ServiceX/>
+ - the same list as in case 1) is displayed, but also details to service
+ `ServiceX' are shown in the lower part of the page.
+
+ 3) <http://example.com/clusters/ClusterOne/services/NonExistingService>
+ OR <http://example.com/clusters/ClusterOne/services/NonExistingService/>
+ OR <http://example.com/clusters/ClusterOne/services/NonExistingService/someth...>
+ - redirection back to base `services' page and displaying information
+ about the request of non-existing service.
+
+ ---
+
+ 4) <http://example.com/clusters/ClusterOne/services_cmd/add>
+ - display form for a node addition to cluster.
+
+ 5) <http://example.com/clusters/ClusterOne/services_cmd/apply>
+ OR <http://example.com/clusters/ClusterOne/services_cmd/apply?CMDID...>
+ - apply a command on given service(s); both GET and POST method supported.
+
+"""
+from tg import flash, redirect, expose, tmpl_context, app_globals
+from pylons.i18n import ugettext as _, lazy_ugettext as l_
+from repoze.what import predicates
+
+from luci.lib.base import SubController, SubControllerApplyMixin, \
+ ApplyCommands, setApplyCommands
+from luci.controllers.decorators import *
+from luci.lib.app_strings import Title, FlashMsg
+from luci.lib.helpers import relativeUrlList2Str
+
+__all__ = ['ServiceController', 'ServiceCmdController']
+
+
+# Imports into module's namespace.
+form_utils = app_globals.form_utils
+scheme = app_globals.scheme
+cluster_scheme = scheme.Cluster
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+#
+# ITEMS LISTING.
+#
+
+class ServiceController(SubController):
+ """Subcontroller handling basic requests related with `services' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.service')
+ def default(self):
+ """Handle simple services listing.
+
+ E.g. <http://example.com/clusters/ClusterOne/services> for situation
+ described in the module's doc.
+
+ """
+ Title.usePart('SERVICES')
+
+ cluster_url = tmpl_context.cluster_url + u'/'
+
+ return dict(name=None, page='nodes',
+ base_url=cluster_url + cluster_scheme.SERVICES,
+ cmd_url=cluster_url + base2CmdCtrl(cluster_scheme.SERVICES),
+ globalres_url=relativeUrlList2Str(scheme.APP_PREFIX,
+ scheme.GLOBAL_RES),
+ apply_cmds=ServiceApplyCommands)
+
+
+ @expose()
+ def lookup(self, name, *args):
+ """Handle service listing with details of certain one.
+
+ E.g. <http://example.com/clusters/ClusterOne/services/ServiceX>
+ for situation described in the module's doc.
+
+ The request is dynamically delegated to _CertainServiceController
+ object.
+
+ Keyword arguments:
+ name Name of the service to be displayed in more detail.
+
+ """
+ # Check whether required service exists.
+ if tmpl_context.cluster_data.services.has_key(name):
+ dynamic_ctrl = _CertainServiceController(entity_name=name)
+ return dynamic_ctrl, args
+ else:
+ msg = l_('Bad service name.')
+ status = 'error'
+
+ flash(msg, status=status)
+ redirect(relativeUrlList2Str(tmpl_context.cluster_url, cluster_scheme.SERVICES))
+
+
+class _CertainServiceController(SubController):
+ """Subcontroller handling services listing with details of certain one."""
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.service')
+ def default(self):
+ """Handle services listing with details of certain one.
+
+ E.g. <http://example.com/clusters/ClusterOne/services/ServiceX>
+ for situation described in the module's doc.
+
+ """
+ Title.usePart('CERTAIN_SERVICE', self.entity_name)
+
+ cluster_url = tmpl_context.cluster_url + u'/'
+
+ return dict(name=self.entity_name, page='nodes',
+ base_url=cluster_url + cluster_scheme.SERVICES,
+ cmd_url=cluster_url + base2CmdCtrl(cluster_scheme.SERVICES),
+ globalres_url=relativeUrlList2Str(scheme.APP_PREFIX,
+ scheme.GLOBAL_RES),
+ apply_cmds=ServiceApplyCommands)
+
+
+ @expose()
+ def lookup(self, *args):
+ """Handle wrong URL (longer than expected) connected with certain service.
+
+ E.g. <http://example.com/clusters/ClusterOne/services/ServiceX/something>
+ for situation described in the module's doc.
+
+ """
+ redirect(relativeUrlList2Str(tmpl_context.cluster_url,
+ cluster_scheme.SERVICES, self.node_name))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class ServiceApplyCommands(ApplyCommands):
+ """Encapsulation of available commands to apply over selected service(s)."""
+
+ REBOOT = 'cmd_reboot'
+ DELETE = 'cmd_delete'
+
+
+ def _reboot(which):
+ msg = l_('Rebooting selected service(s)... %s') \
+ % (", ".join(which))
+ status = 'info'
+ return msg, status
+
+
+ def _delete(which):
+ services = tmpl_context.cluster_data.services
+ msg = l_('Deleting selected service(s)... %s') \
+ % (', '.join(which))
+ status = 'info'
+ for service in which:
+ if services.has_key(service):
+ del services[service]
+ else:
+ # If Luci's data are consistent, this should never happen.
+ msg, status = FlashMsg.INTERNAL_ERROR
+ break
+ return msg, status, False
+
+
+ # Mapping.
+ cmds = {REBOOT: _reboot,
+ DELETE: _delete}
+
+ # Set methods as static (but only after they are referenced
+ # in 'cmds' dict).
+ _reboot = staticmethod(_reboot)
+ _delete = staticmethod(_delete)
+
+
+@setApplyCommands(ServiceApplyCommands)
+class ServiceCmdController(SubController, SubControllerApplyMixin):
+ """Subcontroller handling commands related with `services' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def default(self, *args, **kwargs):
+ """Handle requests not connected with any of defined method."""
+
+ flash(*FlashMsg.BAD_COMMAND_REQUEST)
+
+ base_url_str = relativeUrlList2Str(tmpl_context.cluster_url, self.base_name)
+ return dict(redir_target=base_url_str)
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def add(self, node=None, failover=None):
+ """Handle creation of a new service."""
+
+ msg = 'DEMO: Adding a service'
+ if node!= None:
+ msg += ', for node %s' % node
+ if failover != None:
+ msg += ', for failover %s' % failover
+
+ flash(msg, status='info')
+
+ # following code is only my experiment, how to continue:
+ #tmpl_context.form = create_fdom_add_form
+ #return dict()
+
+ base_url_str = relativeUrlList2Str(tmpl_context.cluster_url, self.base_name)
+ return dict(redir_target=base_url_str)
+
diff --git a/luci/controllers/decorators.py b/luci/controllers/decorators.py
index 76ef1c6..cfc46ab 100644
--- a/luci/controllers/decorators.py
+++ b/luci/controllers/decorators.py
@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
-"""Decorators used within the application's controllers."""
+"""Decorators used for the application's controllers and theirs methods."""
from tg import url, redirect, request, flash, expose, app_globals
from pylons.i18n import ugettext as _, lazy_ugettext as l_
-from luci.lib.helpers import urlList2String
+from luci.lib.helpers import relativeUrlList2Str
+from luci.lib.app_strings import FlashMsg
+# Imported, but not in module's header: luci.lib.base (SubController)
__all__ = ['mountSubControllers',
'forPageShowWithUrlCorrection',
@@ -14,30 +16,36 @@ __all__ = ['mountSubControllers',
# Imports into module's namespace.
-APP_PREFIX = '/'
-base2CmdCtrl = '/'
+APP_PREFIX = app_globals.scheme.APP_PREFIX
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
def mountSubControllers(ctrls_dict=None, **ctrls_kwargs):
"""Class decorator serving as an elegant way to `mount' subcontrollers.
The key of each keyword argument (or alternatively taken from 'ctrls_dict'
- argument, which can be used to pass the same in the form of single dict)
- is used as an identifier where to `mount' such object of the subcontroller.
+ argument, which can be used to pass the same, but in the form of a single
+ dict) is used as an identifier where to `mount' such subcontroller's object.
A bit more advanced use of this subcontroller -- let's have a pair of
- related subcontrollers, base one for general items listing (e.g. ItemController)
- and the other for applying command over set of selected items and similar
- actions (e.g. ItemCmdController). It is useful to let them `mounted'
- on related identifiers.
-
- Notice: subcontrollers mounted in this fashion should be inherited from
- 'Subcontroller' class as it is prepared to deal with arguments
- passed on object creation (see luci.lib.base).
-
- Let's have this example of use:
+ related subcontrollers, base one for general items listing
+ (e.g. ItemController) and the other for applying command over a set
+ of selected items and similar actions (e.g. ItemCmdController). It is useful
+ to let them `mounted' on mutually related (similar) identifiers.
+
+ Notice: subcontrollers mounted in this fashion should be derived from
+ 'SubController' class as it is prepared to deal with keyword
+ arguments passed on object creation -- they are stored as object
+ attributes (see luci.lib.base).
+ This is not necessary, but if `commands controller' does derive from
+ 'SubController', it gets 'base_name' argument of its creation
+ containing the name of `base controller' (at the same time, it can
+ be considered as 'sibling' last part of URL for this `base
+ controller').
+
+ Let's have following example of use:
---
- @mountLuciSubControllers(items=(ItemController, ItemCmdController))
+ @mountSubControllers(items=(ItemController, ItemCmdController))
class RootController(BaseController):
...
---
@@ -57,8 +65,11 @@ def mountSubControllers(ctrls_dict=None, **ctrls_kwargs):
ctrls_kwargs Definition of subcontrollers, see examples above.
"""
+
def decorator(cls):
"""Class binding."""
+ from luci.lib.base import SubController
+
# Take all specified subcontrollers and `mount' them to the class.
if ctrls_dict and type(ctrls_dict is dict):
ctrls_kwargs.update(ctrls_dict)
@@ -69,7 +80,13 @@ def mountSubControllers(ctrls_dict=None, **ctrls_kwargs):
# 1) Pair of controllers.
cmd_name = base2CmdCtrl(base_name)
setattr(cls, base_name, controller[0]())
- setattr(cls, cmd_name, controller[1](base_name=base_name))
+ # If `command subcontroller' derives from SubController,
+ # pass the name of related `base controller'.
+ if (issubclass(controller[1], SubController)):
+ cmd_ctrl = controller[1](base_name=base_name)
+ else:
+ cmd_ctrl = controller[1]()
+ setattr(cls, cmd_name, cmd_ctrl)
else:
# 2) Single controller.
if type(controller) in (tuple, list):
@@ -79,16 +96,18 @@ def mountSubControllers(ctrls_dict=None, **ctrls_kwargs):
# Decorator's retval is the value returned by the wrapper.
return cls
- # Retval is the value returned by the decorator.
return decorator
+#-------------------------------------------------------------------------------
+# Customized decorators for controller's methods, based on the basic TG's one:
+# expose.
ATTR_REDIR_TARGET = 'REDIR_TARGET'
-def forPageShowOnly(template='', allowed_methods=('GET', 'POST'),
- redirect='', use_referrer=True):
+def forPageShowOnly(template='', allowed_methods=('GET'),
+ redir_target='', use_referrer=True):
"""Decorator to show page incl. check of the request's method.
Keyword arguments:
@@ -98,10 +117,11 @@ def forPageShowOnly(template='', allowed_methods=('GET', 'POST'),
still be specified through 'REDIR_TARGET' attribute
(ATTR_REDIR_TARGET) of the object that has bound
decorated method. If nothing specified at all,
- LUCI_APP_PREFIX is used.
+ APP_PREFIX is used.
use_referrer On redirect call, try to use referrer first.
"""
+
if type(allowed_methods) not in (tuple, list):
allowed_methods = [allowed_methods]
@@ -111,13 +131,15 @@ def forPageShowOnly(template='', allowed_methods=('GET', 'POST'),
def wrapper(self, *args, **kwargs):
"""Arguments binding, decorated function (method) call."""
if request.method not in allowed_methods:
- flash(_('Unsupported method of the request.'), status='error')
+ flash_msg = list(FlashMsg.UNSUPPORTED_METHOD)
+ flash_msg[0] %= request.method
+ flash(*flash_msg)
redir_url = redir_target
if use_referrer and request.referrer:
redir_url = request.referrer
elif not redir_url:
redir_url = getattr(fn.__self__, ATTR_REDIR_TARGET,
- LUCI_APP_PREFIX)
+ relativeUrlList2Str(APP_PREFIX))
redirect(redir_url)
else:
return fn(self, *args, **kwargs)
@@ -125,7 +147,6 @@ def forPageShowOnly(template='', allowed_methods=('GET', 'POST'),
# Decorator's retval is the value returned by the wrapper.
return wrapper
- # Retval is the value returned by the decorator.
return decorator
@@ -138,13 +159,16 @@ def forPageShowWithUrlCorrection(template=''):
template Which template to internally use with @expose().
"""
+
def decorator(fn):
"""Function (method) binding"""
@expose(template)
def wrapper(self, *args, **kwargs):
"""Arguments binding, decorated function (method) call."""
if request.method != 'GET':
- flash(_('Unsupported method of the request.'), status='error')
+ flash_msg = list(FlashMsg.UNSUPPORTED_METHOD)
+ flash_msg[0] %= request.method
+ flash(*flash_msg)
redirect(request.path, **kwargs)
else:
redir = False
@@ -165,7 +189,6 @@ def forPageShowWithUrlCorrection(template=''):
# Decorator's retval is the value returned by the wrapper.
return wrapper
- # Retval is the value returned by the decorator.
return decorator
@@ -181,7 +204,7 @@ def forImmediateRedirect(**outer_kwargs):
specified through 'REDIR_TARGET' attribute
(ATTR_REDIR_TARGET) of the object that has bound
decorated method. If nothing specified at all,
- LUCI_APP_PREFIX is used.
+ APP_PREFIX is used.
Default: ''
use_referrer On redirect call, try to use referrer first.
Default: True
@@ -191,6 +214,7 @@ def forImmediateRedirect(**outer_kwargs):
has to be dictionary (e.g. 'dict(use_referrer=False, redir_target='/foo')').
"""
+
allowed_methods = outer_kwargs.get('allowed_methods', ('GET', 'POST'))
if type(allowed_methods) not in (tuple, list):
allowed_methods = [allowed_methods]
@@ -202,7 +226,9 @@ def forImmediateRedirect(**outer_kwargs):
"""Arguments binding, decorated function (method) call."""
inner_kwargs = outer_kwargs.copy()
if request.method not in allowed_methods:
- flash(_('Unsupported method of the request.'), status='error')
+ flash_msg = list(FlashMsg.UNSUPPORTED_METHOD)
+ flash_msg[0] %= request.method
+ flash(*flash_msg)
else:
override_kwargs = fn(self, *args, **kwargs)
# If retval of decorated function is dict, override original
@@ -213,12 +239,12 @@ def forImmediateRedirect(**outer_kwargs):
if inner_kwargs.get('use_referrer', True) and request.referrer:
redir_url = request.referrer
elif not redir_url:
- redir_url = getattr(self, ATTR_REDIR_TARGET, LUCI_APP_PREFIX)
+ redir_url = getattr(self, ATTR_REDIR_TARGET,
+ relativeUrlList2Str(APP_PREFIX))
redirect(redir_url)
# Decorator's retval is the value returned by the wrapper (no retval).
return wrapper
- # Retval is the value returned by the decorator.
return decorator
diff --git a/luci/controllers/global_res.py b/luci/controllers/global_res.py
new file mode 100644
index 0000000..a8f01e9
--- /dev/null
+++ b/luci/controllers/global_res.py
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+
+"""Controller API for `global resources' part.
+
+ Functionality description:
+
+ Let's have the main subcontroller (GlobalResController) available through
+ 'global-resources' identifier of the root controller and the subcontroller
+ handling related commands (GlobalResCmdController) through
+ 'global-resources_cmd' identifier.
+
+ Luci application is accessible via http://example.com. Global resources
+ in the system are Resource 1, Resource 2 and Resource 3.
+
+ Accessing following URLs has following consequences (some cases omitted):
+
+ 1) <http://example.com/global-resources>
+ OR <http://example.com/global-resources/>
+ - displaying a list of clusters with some information about them.
+
+ 2) <http://example.com/global-resources/Resource%201>
+ OR <http://example.com/global-resources/Resource%201/>
+ - the same list as in case 1) is displayed, but also details to global
+ resource `Resource 1' are shown in the lower part of the page.
+
+ 3) <http://example.com/global-resources/NonExistingResource>
+ OR <http://example.com/clusters/NonExistingResource/>
+ OR <http://example.com/clusters/NonExistingResource/something_more>
+ - redirection back to base `global resources' page and displaying
+ information about the request of non-existing global resource.
+
+ ---
+
+ 4) <http://example.com/global-resources_cmd/create>
+ - display form for cluster creation.
+
+"""
+
+from tg import flash, request, redirect, expose, tmpl_context, app_globals
+from pylons.i18n import ugettext as _, lazy_ugettext as l_
+from repoze.what import predicates
+
+from luci.lib.base import SubController, SubControllerApplyMixin, \
+ ApplyCommands, setApplyCommands
+from luci.controllers.decorators import *
+from luci.lib.app_strings import Title, FlashMsg
+from luci.lib.helpers import relativeUrlList2Str
+
+__all__ = ['GlobalResController', 'GlobalResCmdController']
+
+
+# Imports into module's namespace.
+scheme = app_globals.scheme
+cluster_scheme = app_globals.scheme.Cluster
+data = app_globals.data
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+#
+# ITEMS LISTING.
+#
+
+class GlobalResController(SubController):
+ """Subcontroller handling basic requests related with `global resources' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+
+ @forPageShowWithUrlCorrection('luci.templates.global_res')
+ def default(self):
+ """Handle simple global resources listing.
+
+ E.g. <http://example.com/global-resources> for situation described
+ in the module's doc.
+
+ """
+ Title.usePart('GLOBAL_RES')
+
+ base_url_str = relativeUrlList2Str(scheme.APP_PREFIX, scheme.GLOBAL_RES)
+ cmd_url_str = relativeUrlList2Str(scheme.APP_PREFIX,
+ base2CmdCtrl(scheme.GLOBAL_RES))
+
+ return dict(name=None,
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ apply_cmds=GlobalResApplyCommands)
+
+
+ @expose()
+ def lookup(self, name, *args):
+ """Handle global resources listing with details of certain one.
+
+ E.g. <http://example.com/global-resources/Resource%201>
+ for situation described in the module's doc.
+
+ The request is dynamically delegated to _CertainGlobalResController
+ object.
+
+ Keyword arguments:
+ name Name of the global resource to be displayed in more detail.
+
+ """
+ # Check whether required global resource.
+ if data.global_res.has_key(name):
+ dynamic_ctrl = _CertainGlobalResController(entity_name=name)
+ return dynamic_ctrl, args
+ else:
+ msg = l_('Bad global resource name: %s') % name
+ status = 'error'
+
+ flash(msg, status=status)
+ redirect(relativeUrlList2Str(tmpl_context.cluster_url,
+ cluster_scheme.GLOBAL_RES))
+
+
+class _CertainGlobalResController(SubController):
+ """Subcontroller handling global resources listing with details of certain one."""
+
+ @forPageShowWithUrlCorrection('luci.templates.global_res')
+ def default(self):
+ """Handle global resources listing with details of certain one.
+
+ E.g. <http://example.com/global-resources/Resource%201>
+ for situation described in the module's doc.
+
+ """
+ Title.usePart('CERTAIN_GLOBAL_RES', self.entity_name)
+
+ base_url_str = relativeUrlList2Str(scheme.APP_PREFIX, scheme.GLOBAL_RES)
+ cmd_url_str = relativeUrlList2Str(scheme.APP_PREFIX,
+ base2CmdCtrl(scheme.GLOBAL_RES))
+
+ return dict(name=self.entity_name,
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ apply_cmds=GlobalResApplyCommands)
+
+
+ @expose()
+ def lookup(self, *args):
+ """Handle wrong URL connected with certain global resource.
+
+ E.g. <http://example.com/global-resources/Resource%201/something>
+ for situation described in the module's doc.
+
+ """
+ redirect(relativeUrlList2Str(scheme.APP_PREFIX, scheme.GLOBAL_RES,
+ self.entity_name))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class GlobalResApplyCommands(ApplyCommands):
+ """Encapsulation of available commands to apply over selected global resource(s)."""
+
+ DELETE = 'cmd_delete'
+
+ def _delete(which):
+ global_res = data.global_res
+ msg = l_('Deleting selected global resource(s)... %s') \
+ % (', '.join(which))
+ status = 'info'
+ for global_resource in which:
+ if global_res.has_key(global_resource):
+ del global_res[global_resource]
+ else:
+ # If Luci's data are consistent, this should never happen.
+ msg, status = FlashMsg.INTERNAL_ERROR
+ break
+ return msg, status, False
+
+
+ # Mapping.
+ cmds = {DELETE: _delete}
+
+ # Set methods as static (but only after they are referenced
+ # in 'cmds' dict).
+ _delete = staticmethod(_delete)
+
+
+@setApplyCommands(GlobalResApplyCommands)
+class GlobalResCmdController(SubController, SubControllerApplyMixin):
+ """Subcontroller handling commands related with `global resources' part."""
+
+ # TODO: Uncomment this (or replace with more suitable predicate)
+ # to apply access control.
+ # For some reason this won't work for me, similar problem discussed at:
+ # <http://groups.google.com.ar/group/turbogears/browse_thread/thread/28a8b85...>
+ # allow_only = predicates.not_anonymous()
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def default(self, *args, **kwargs):
+ """Handle requests not connected with any of defined method."""
+
+ flash(*FlashMsg.BAD_COMMAND_REQUEST)
+
+ base_url_str = relativeUrlList2Str(scheme.APP_PREFIX, self.base_name)
+ return dict(redir_target=base_url_str)
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def add(self):
+ """Handle creation of a new node."""
+
+ flash('It should be a dialog to add new node here instead, '
+ 'but not implemented yet...', status='info')
+ # following code is only my experiment, how to continue:
+ #tmpl_context.form = create_fdom_add_form
+ #return dict()
+
diff --git a/luci/controllers/root.py b/luci/controllers/root.py
index 7bda783..f4b38cf 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 repoze.what.predicates import not_anonymous
from luci.controllers.decorators import *
from luci.lib.base import BaseController
@@ -73,6 +74,7 @@ class RootController(BaseController):
return dict(page='about')
@expose('luci.templates.homebase')
+ @require(predicates.not_anonymous())
def homebase(self, homebasepage='homebasepage', **args):
if homebasepage == 'addsystem':
tmpl_context.form = create_add_system_form
@@ -94,12 +96,6 @@ class RootController(BaseController):
def clusters(self, **args):
redirect ('/cluster/')
- @expose('luci.templates.storage')
- def storage(self,storagepage='systemlist'):
- return dict(page='storage', storagepage=storagepage)
-
- # TODO: Automatically generated/experimental methods? Perhaps clean needed.
-
@expose('luci.templates.authentication')
def auth(self):
"""Display some information about auth* on this application."""
diff --git a/luci/controllers/scheme.py b/luci/controllers/scheme.py
new file mode 100644
index 0000000..64bdcf6
--- /dev/null
+++ b/luci/controllers/scheme.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+
+"""Constants and functions regarding the (logical and URL) scheme of Luci.
+
+Here is also a place, where strings forming certain parts of URL across
+Luci can be adjusted.
+
+"""
+
+from luci.lib.helpers import singleton
+
+__all__ = ['LuciScheme']
+
+
+class Scheme():
+ """Base abstract class for scheme structure."""
+
+ def __getattr__(self, name):
+ if name=='mounts':
+ return self.getMounts()
+ elif name=='getMounts':
+ raise RuntimeError, \
+ '\'getMounts\' method has to be defined in derived class'
+
+
+@singleton
+class LuciScheme(Scheme):
+ """Encapsulation of logical and URL scheme of Luci.
+
+ The root of recursively nested classes (`singletons') deriving from
+ the same base class as this one.
+
+ """
+
+ # Static URL prefix (excluding host part and slashes both at the beginning
+ # and at the end). E.g. 'luci' for Luci application set to be running at
+ # <http://example.com/luci/> (notice: appropriate changes in controllers
+ # schema have to be made to reach this behavior).
+ APP_PREFIX = u''
+
+ # First level of the scheme.
+
+ CLUSTERS = u'clusters'
+ GLOBAL_RES = u'global-resources'
+ STORAGE = u'storage'
+ USERS = u'users'
+ SYSTEMS = u'systems'
+
+ @classmethod
+ def getMounts(cls):
+ """Defined as a method to avoid circular dependency."""
+
+ from luci.controllers.cluster import ClusterController, \
+ ClusterCmdController
+ from luci.controllers.global_res import GlobalResController, \
+ GlobalResCmdController
+# from luci.controllers.storage import StorageController, \
+# StorageCmdController
+
+ return {
+ cls.CLUSTERS: (ClusterController, ClusterCmdController),
+ cls.GLOBAL_RES: (GlobalResController, GlobalResCmdController)
+# cls.USERS: (UsersController, UsersCmdController],
+# cls.STORAGE: (StorageController, StorageCmdController),
+# cls.SYSTEMS: (SystemsController, SystemsCmdController)
+ }
+
+ # Second level of the scheme -- `Clusters'.
+
+ @singleton
+ class Cluster(Scheme):
+ NODES = u'nodes'
+ SERVICES = u'services'
+ FAILOVERS = u'failovers'
+ FENCES = u'fences'
+
+ @classmethod
+ def getMounts(cls):
+ """Defined as a method to avoid circular dependency."""
+ from cluster_part.node import NodeController, NodeCmdController
+ from cluster_part.service import ServiceController, ServiceCmdController
+ from cluster_part.failover import FailoverController, FailoverCmdController
+ from cluster_part.fence import FenceController, FenceCmdController
+
+ return {
+ cls.NODES: (NodeController, NodeCmdController),
+ cls.SERVICES: (ServiceController, ServiceCmdController),
+ cls.FAILOVERS: (FailoverController, FailoverCmdController),
+ cls.FENCES: (FenceController, FenceCmdController)
+ }
+
+
+ #---------------------------------------------------------------------------
+ # Part related to `base controller' - `commands controller' correspondence.
+
+ """Suffix which makes identifier of base subcontroller unique.
+
+ Such unique identifier can be then used as an identifier of `commands
+ subcontroller'.
+
+ """
+ CTRL_CMD_SUFFIX = u'_cmd'
+
+ @classmethod
+ def base2CmdCtrl(cls, base):
+ """Convert `base controller' -- `command subcontroller' name."""
+ return base + cls.CTRL_CMD_SUFFIX
+
+ @classmethod
+ def cmd2BaseCtrl(cls, cmd):
+ """Convert `command subcontroller' -- `base controller' name."""
+ return cmd[:cmd.rfind(cls.CTRL_CMD_SUFFIX)]
+
diff --git a/luci/lib/ClusterConf/ClusterNode.py b/luci/lib/ClusterConf/ClusterNode.py
index 7a11504..42ca19d 100644
--- a/luci/lib/ClusterConf/ClusterNode.py
+++ b/luci/lib/ClusterConf/ClusterNode.py
@@ -74,3 +74,10 @@ class ClusterNode(TagObject):
return "1"
except:
return None
+
+ def getID(self):
+ try:
+ return self.getAttribute('nodeid')
+ except Exception, e:
+ pass
+ return "UNKNOWN"
diff --git a/luci/lib/ClusterConf/FailoverDomain.py b/luci/lib/ClusterConf/FailoverDomain.py
index c7b6245..8f30621 100644
--- a/luci/lib/ClusterConf/FailoverDomain.py
+++ b/luci/lib/ClusterConf/FailoverDomain.py
@@ -13,3 +13,13 @@ class FailoverDomain(TagObject):
def __init__(self):
TagObject.__init__(self)
self.TAG_NAME = TAG_NAME
+
+ def get_member_node(self, nodename):
+ member_list = self.children
+ for i in member_list:
+ if i.getName() == nodename:
+ return i
+ return None
+
+ def has_member_node(self, nodename):
+ return self.get_member_node(nodename) != None
diff --git a/luci/lib/ClusterConf/ModelBuilder.py b/luci/lib/ClusterConf/ModelBuilder.py
index 97f0e70..6c688d9 100644
--- a/luci/lib/ClusterConf/ModelBuilder.py
+++ b/luci/lib/ClusterConf/ModelBuilder.py
@@ -537,6 +537,9 @@ class ModelBuilder:
def getNodeNameById(self, node_id):
return filter(lambda x: x.getAttribute('nodeid') == node_id, self.clusternodes_ptr.getChildren())[0].getName()
+ def getNodeByName(self, node_name):
+ return filter(lambda x: x.getAttribute('name') == node_name, self.clusternodes_ptr.getChildren())[0]
+
def addNode(self, clusternode):
self.clusternodes_ptr.addChild(clusternode)
if self.usesMulticast is True:
@@ -600,6 +603,10 @@ class ModelBuilder:
raise KeyError, 'Couldn\'t find service name %s in current list' % name
+ def getServicesForFdom(self, name):
+ svc_list = filter(lambda x: x.getAttribute('domain') == name, self.getServices())
+ return svc_list
+
def retrieveVMsByName(self, name):
vms = self.getVMs()
for v in vms:
@@ -650,6 +657,23 @@ class ModelBuilder:
else:
return self.fencedevices_ptr.getChildren()
+ def getNodesUsingFence(self, fence_name):
+ nodes_using = []
+ nodes = self.getNodes()
+ for i in nodes:
+ added_node = False
+ levels = i.getFenceLevels()
+ for l in levels:
+ lc = l.getChildren()
+ for dev in lc:
+ if dev.getName() == fence_name:
+ nodes_using.append(i)
+ added_node = True
+ break
+ if added_node != False:
+ break
+ return nodes_using
+
def getFenceDevicePtr(self):
return self.fencedevices_ptr
@@ -843,6 +867,9 @@ class ModelBuilder:
def getClusterName(self):
return self.getClusterPtr().getName()
+ def getClusterConfigVersion(self):
+ return self.getClusterPtr().getConfigVersion()
+
def getClusterAlias(self):
return self.getClusterPtr().getAlias()
diff --git a/luci/lib/app_globals.py b/luci/lib/app_globals.py
index b464e11..5793a0b 100644
--- a/luci/lib/app_globals.py
+++ b/luci/lib/app_globals.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
+"""The application's Globals object."""
-"""The application's Globals object"""
-
+from luci.controllers.scheme import LuciScheme
from luci.lib.form_utils import FormUtils
from luci.lib.demo_data import ClusterData
@@ -19,4 +19,6 @@ class Globals(object):
def __init__(self):
"""Prepare some common static functions, constants etc."""
self.form_utils = FormUtils
+ self.scheme = LuciScheme
self.data = ClusterData()
+
diff --git a/luci/lib/app_strings.py b/luci/lib/app_strings.py
new file mode 100644
index 0000000..e8869b3
--- /dev/null
+++ b/luci/lib/app_strings.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""Separated encapsulations of common strings and short sentences."""
+
+from pylons.i18n import ugettext as _
+# Imported, but not in module's header: tg (tmpl_context)
+
+__all__ = ['Title', 'FlashMsg']
+
+
+class Title:
+ """Strings used in the title of the page.
+
+ Final form of the title adjusted by 'luci.templates.title'.
+
+ """
+
+ @classmethod
+ def usePart(cls, what, *args):
+ """Adds/updates strings + theirs args to be used within page's title."""
+
+ from tg import tmpl_context
+
+ title_part = getattr(cls, what, ('', ))
+ if type(title_part) is not tuple:
+ if issubclass(type(title_part), basestring):
+ title_part = (title_part,)
+ else:
+ title_part = ('', )
+
+ tmpl_context.title_list.append(title_part[0])
+
+ min_len = len(args)
+ if min_len > len(title_part)-1:
+ min_len = len(title_part)-1
+
+ for i in xrange(1, min_len+1):
+ tmpl_context.title_args[unicode(title_part[i])] = args[i-1]
+
+
+ # Following pairs are used for general items listing or certain entity
+ # details displaying (thus, the '{word}' is alias for the name of such
+ # entity, 'word' itself is the next item in corresponding tuple and
+ # is used as a key for storing/retrieving such string parameters
+ # to separated dict respectively.
+
+ CLUSTERS = _('clusters')
+ CERTAIN_CLUSTER = _('cluster {cluster}'), 'cluster'
+
+ CLUSTER_CREATE = _('create cluster')
+ CLUSTER_ADD_EXISTING = _('add an existing cluster')
+
+ GLOBAL_RES = _('resources')
+ CERTAIN_GLOBAL_RES = _('resource {global_res}'), 'global_res'
+
+ NODES = _('nodes')
+ CERTAIN_NODE = _('node {node}'), 'node'
+
+ SERVICES = _('services')
+ CERTAIN_SERVICE = _('service {service}'), 'service'
+
+ FAILOVERS = _('failovers')
+ CERTAIN_FAILOVER = _('failover {failover}'), 'failover'
+
+ FENCES = _('fences')
+ CERTAIN_FENCE = _('fence {fence}'), 'fence'
+
+
+class FlashMsg:
+ """Strings used for error/warning/notice reporting to the user."""
+
+ STYLE_WARNING = 'warning'
+ STYLE_ERROR = 'error'
+
+ # Pairs of message and the message status to be used by tg.flash method.
+
+ # Connected with
+ BAD_COMMAND_REQUEST = _('Bad command request.'), STYLE_WARNING
+ INTERNAL_ERROR = _('Internal error.'), STYLE_ERROR
+ NOTHING_CHOSEN = _('Nothing was chosen.'), STYLE_WARNING
+
+ # Connected with decorators.
+
+ UNSUPPORTED_METHOD = _('%s method not supported in this context.'), STYLE_ERROR
+
diff --git a/luci/lib/async_helpers.py b/luci/lib/async_helpers.py
new file mode 100644
index 0000000..ab0832a
--- /dev/null
+++ b/luci/lib/async_helpers.py
@@ -0,0 +1,31 @@
+from pylons.i18n import ugettext as _
+
+from luci.lib.ricci_communicator import RicciCommunicator
+import luci.lib.ricci_queries as rq
+
+from luci.lib.cluster_conf_helpers import get_cluster_conf_nodes
+
+
+def get_node_list(host, passwd):
+ node_names = None
+ try:
+ rc = RicciCommunicator(host, enforce_trust=False)
+ rc.trust()
+ rc.auth(passwd)
+ if not rc.authed():
+ return { 'errors': _('Authentication to host "%s" failed') % host }
+
+ cluster_name = rc.cluster_info()[0]
+ if not cluster_name:
+ return { 'errors': _('Host "%s" is not a member of a cluster') % host }
+
+ conf_xml = rq.getClusterConf(rc)
+ if conf_xml:
+ node_names = get_cluster_conf_nodes(conf_xml)
+
+ if not node_names or len(node_names) < 1:
+ return { 'errors': _('Unable to retrieve the list of cluster nodes from "%s"') % host }
+ except Exception, e:
+ # log this
+ return { 'errors': _('Unable to connect to host "%s": %s') % (host, str(e)) }
+ return { 'nodes': node_names }
diff --git a/luci/lib/base.py b/luci/lib/base.py
index e48c964..bfe68cb 100644
--- a/luci/lib/base.py
+++ b/luci/lib/base.py
@@ -1,36 +1,30 @@
# -*- coding: utf-8 -*-
-
"""The base Controller API."""
-from tg import TGController, tmpl_context, flash, app_globals
+from tg import TGController, request, flash, app_globals, tmpl_context
+# TODO: Remove following lines, it comes from original TG's app template (?).
#from tg.render import render
-from tg import request
#from pylons.i18n import _, ungettext, N_
#from tw.api import WidgetBunch
-import luci.model as model
from luci.controllers.decorators import forImmediateRedirect
-from luci.lib.helpers import urlList2String
-from luci.lib.strings import VerboseStrings
-
-__all__ = ['BaseController', 'Subcontroller', 'SubcontrollerApplyMixin']
-
+from luci.lib.app_strings import FlashMsg
+from luci.lib.helpers import relativeUrlList2Str
+import luci.model as model
-# Imports into module's namespace.
-form_utils = app_globals.form_utils
+__all__ = ['BaseController', 'SubController',
+ 'SubControllerApplyMixin', 'ApplyCommands', 'setApplyCommands']
class BaseController(TGController):
- """
- Base class for the controllers in the application.
+ """Base class for the root controller.
- Your web application should have one of these. The root of
- your application is used to compute URLs used by your app.
+ The root of the application is used to compute URLs used by this app.
"""
def __call__(self, environ, start_response):
- """Invoke the Controller"""
+ """Invoke the Controller."""
# TGController.__call__ dispatches to the Controller method
# the request is routed to. This routing information is
# available in environ['pylons.routes_dict']
@@ -40,7 +34,7 @@ class BaseController(TGController):
return TGController.__call__(self, environ, start_response)
-class Subcontroller(object):
+class SubController(object):
"""Base class for subcontrollers.
Keyword arguments passed on object creation are automatically moved
@@ -49,32 +43,50 @@ class Subcontroller(object):
"""
def __new__(cls, *args, **kwargs):
- # If the class that invoked this method is directly inherited from this
- # class, set new attributes according to passed keyword arguments
+ # If the class that invoked this method is directly inherited from
+ # this class, set new attributes according to passed keyword arguments
# and delegate this invocation upwards (omitting the level
- # of Subcontroller class).
- # Otherwise, delegate the invocation directly upwards.
- if cls.__base__.__name__ == 'Subcontroller':
+ # of SubController class).
+ # Then delegate the invocation to superclass.
+
+ if cls.__base__.__name__ == 'SubController':
for kw, v in kwargs.iteritems():
setattr(cls, kw, v)
- return BaseController.__new__(cls, *args)
- else:
- return cls.__base__.__new__(cls, *args, **kwargs)
+ kwargs.clear
+ return super(type(cls), cls).__new__(cls, *args)
-class SubcontrollerApplyMixin:
- """Mixin that adds 'apply' method to the derived controller."""
+#-------------------------------------------------------------------------------
+# Mini-framework for definition and usage of `apply command over selected
+# entitities' feature.
- @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
- def apply(self, name=None, **kwargs):
- """Handle applying a command over selected node(s).
+# Name of the attribute in `commands controller' to contain encapsulation
+# of related `commands to apply' (derived from ApplyCommands).
+# This attribute should be only set with 'setApplyCommands' class decorator,
+# the other uses of this attribute belong to the framework.
+ATTR_APPLY_CMD = 'APPLY_CMD'
+
+
+class ApplyCommands:
+ """Base class for object encapsulation of related commands `to apply'.
+
+ See doc-string for SubControllerApplyMixin.
+ """
- Class using this mixin has to define attribute '_apply_cmds' that
- contains object that defines attribute 'cmds', which is a dictionary.
+ # Default empty mapping.
+ cmds = {}
- Example of definition of such object:
+
+class SubControllerApplyMixin:
+ """Mixin that adds 'apply' method to the derived controller.
+
+ Class using this mixin has to be decorated with 'setApplyCommands'
+ decorator setting the appropriate encapsulation of related commands.
+ This encapsulation should be derived from 'ApplyCommands' class.
+
+ Example definition of such encapsulation of related commands:
---
- class ApplyCommands:
+ class EntityApplyCommands(ApplyCommands):
DELETE = 'cmd_delete'
def _delete(which):
@@ -87,7 +99,7 @@ class SubcontrollerApplyMixin:
_delete = staticmethod(_delete)
---
- This method takes all keyword arguments (taken from URL) and try
+ The 'apply' method takes all keyword arguments (taken from URL) and try
to find a command identifier first (e.g. 'cmd_delete' from example
above, but there can be more of them).
Then it prepares a list of identifiers (sort of entity depends
@@ -107,13 +119,28 @@ class SubcontrollerApplyMixin:
use_referrer Whether to allow use referrer (if available)
for redirection target.
+ """
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def apply(self, name=None, **kwargs):
+ """Handle applying a command over selected entity(-ies).
+
Keyword arguments:
name Name of the entity to be used (if GET method is used).
kwargs Dict of entities to be used (if POST method is used).
+ Return value(s):
+ Dict as expected by 'forImmediateRedirect' decorator.
+
"""
+ form_utils = app_globals.form_utils
+
cmd_set = False
- cmds_used = set(kwargs).intersection(self._apply_cmds.cmds)
+ # Get intersection of keys of request's parameters and
+ # commands defined within corresponding encapsulation of commands.
+ # This intersection is expected to have only one member.
+ apply_commands = getattr(self, ATTR_APPLY_CMD, ApplyCommands)
+ cmds_used = set(kwargs).intersection(apply_commands.cmds)
if len(cmds_used) == 1:
cmd_set = True
cmd = cmds_used.pop()
@@ -122,8 +149,7 @@ class SubcontrollerApplyMixin:
use_referrer = True
if not cmd_set:
# If Luci's templates are consistent, this should never happen.
- msg = VerboseStrings.INTERNAL_ERROR
- status = 'error'
+ msg, status = FlashMsg.INTERNAL_ERROR
else:
if name:
which = [name]
@@ -131,14 +157,39 @@ class SubcontrollerApplyMixin:
which = [form_utils.id2String(s) for s in kwargs.iterkeys()]
if not which:
- msg = VerboseStrings.NOTHING_CHOSEN
- status ='warning'
+ msg, status = FlashMsg.NOTHING_CHOSEN
else:
- retval = self._apply_cmds.cmds[cmd](which)
+ retval = apply_commands.cmds[cmd](which)
unpacker = lambda msg, status, use_referrer=True: \
(msg, status, use_referrer)
msg, status, use_referrer = unpacker(*retval)
flash(msg, status=status)
- base_url_str = urlList2String(tmpl_context.cluster_url, self.base_name)
- return dict(use_referrer=use_referrer, redir_target=base_url_str)
+ base_url_str = \
+ relativeUrlList2Str(tmpl_context.cluster_url, self.base_name)
+
+ return dict(use_referrer=use_referrer,
+ redir_target=base_url_str)
+
+
+def setApplyCommands(apply_commands):
+ """Class decorator to set the encapsulation of related `commands to apply'.
+
+ To be used for decoration of `commands controller' that derives from
+ SubControllerApplyMixin.
+
+ Keyword arguments:
+ apply_commands Encapsulation of related commands `to apply'.
+
+ """
+
+ def decorator(cls):
+ """Class binding."""
+ # Take and set given encapsulation of related commands as an attribute
+ # of the class.
+ if issubclass(apply_commands, ApplyCommands):
+ setattr(cls, ATTR_APPLY_CMD, apply_commands)
+ return cls
+
+ return decorator
+
diff --git a/luci/lib/cluster_conf_helpers.py b/luci/lib/cluster_conf_helpers.py
new file mode 100644
index 0000000..4ff8a94
--- /dev/null
+++ b/luci/lib/cluster_conf_helpers.py
@@ -0,0 +1,8 @@
+def get_cluster_conf_nodes(conf_xml):
+ try:
+ cluster_nodes = conf_xml.getElementsByTagName('clusternode')
+ return map(lambda x: str(x.getAttribute('name')), cluster_nodes)
+ except Exception, e:
+ pass
+ # log this
+ return None
diff --git a/luci/lib/cluster_status.py b/luci/lib/cluster_status.py
index 1a66a3d..3c21971 100644
--- a/luci/lib/cluster_status.py
+++ b/luci/lib/cluster_status.py
@@ -2,39 +2,43 @@ from luci.model import metadata, DBSession
from luci.model.objects import Node,Cluster,Task
from sqlalchemy.orm.exc import NoResultFound
-from luci.lib.ricci_communicator import RicciCommunicator
-import luci.lib.ricci_queries as rq
-
class NodeStatus:
- def __init__(self, node_xml):
- self.name = node_xml.getAttribute('name')
- self.clustered = node_xml.getAttribute('clustered')
- self.online = node_xml.getAttribute('online')
- self.uptime = node_xml.getAttribute('uptime')
- self.votes = node_xml.getAttribute('votes')
+ def __init__(self, node_xml):
+ self.name = node_xml.getAttribute('name')
+ self.clustered = node_xml.getAttribute('clustered')
+ self.online = node_xml.getAttribute('online')
+ self.uptime = node_xml.getAttribute('uptime')
+ self.votes = node_xml.getAttribute('votes')
+ self.services = {}
class ServiceStatus:
- def __init__(self, svc_xml):
- self.type = 'service'
- self.name = svc_xml.getAttribute('name')
- self.nodename = svc_xml.getAttribute('nodename')
- self.running = svc_xml.getAttribute('running')
- self.failed = svc_xml.getAttribute('failed')
- self.autostart = svc_xml.getAttribute('autostart')
- self.is_vm = svc_xml.getAttribute('vm').lower() == 'true'
+ def __init__(self, svc_xml):
+ self.type = 'service'
+ self.name = svc_xml.getAttribute('name')
+ self.nodename = svc_xml.getAttribute('nodename')
+ self.running = svc_xml.getAttribute('running')
+ self.failed = svc_xml.getAttribute('failed')
+ self.autostart = svc_xml.getAttribute('autostart').lower() == 'true'
+ self.is_vm = svc_xml.getAttribute('vm').lower() == 'true'
class ClusterStatus:
- def __init__(self, status_xml):
- self.alias = status_xml.firstChild.getAttribute('alias')
- self.name = status_xml.firstChild.getAttribute('name')
- self.quorate = status_xml.firstChild.getAttribute('quorate')
- self.votes = status_xml.firstChild.getAttribute('votes')
- self.minQuorum = status_xml.firstChild.getAttribute('minQuorum')
- self.nodes = []
- self.services = []
+ def __init__(self, status_xml):
+ self.alias = status_xml.firstChild.getAttribute('alias')
+ self.name = status_xml.firstChild.getAttribute('name')
+ self.quorate = status_xml.firstChild.getAttribute('quorate')
+ self.votes = status_xml.firstChild.getAttribute('votes')
+ self.minQuorum = status_xml.firstChild.getAttribute('minQuorum')
+ self.nodes = {}
+ self.services = {}
+
+ for node in status_xml.firstChild.childNodes:
+ if node.nodeName == 'node':
+ ns = NodeStatus(node)
+ self.nodes[ns.name] = ns
- for node in status_xml.firstChild.childNodes:
- if node.nodeName == 'node':
- self.nodes.append(NodeStatus(node))
- elif node.nodeName == 'service':
- self.services.append(ServiceStatus(node))
+ for node in status_xml.firstChild.childNodes:
+ if node.nodeName == 'service':
+ ss = ServiceStatus(node)
+ self.services[ss.name] = ss
+ if ss.nodename:
+ self.nodes[ss.nodename][ss.name] = ss
diff --git a/luci/lib/db_helpers.py b/luci/lib/db_helpers.py
index 9f1174c..77dcf9c 100644
--- a/luci/lib/db_helpers.py
+++ b/luci/lib/db_helpers.py
@@ -2,6 +2,7 @@
from luci.model import metadata, DBSession
from luci.model.objects import Node,Cluster,Task
+from luci.lib.cluster_status import ClusterStatus
from sqlalchemy.orm.exc import NoResultFound
from luci.lib.ricci_communicator import RicciCommunicator
@@ -18,6 +19,15 @@ def get_cluster_db_obj(cluster_name):
pass
return db_obj
+def get_cluster_node(cluster_name, node_name):
+ db_obj = get_cluster_db_obj(cluster_name)
+ if db_obj == None:
+ return None
+ for i in db_obj.nodes:
+ if i.node_name == node_name:
+ return i
+ return None
+
def get_agent_for_cluster(cluster_name):
db_obj = get_cluster_db_obj(cluster_name)
if db_obj is None:
@@ -51,7 +61,7 @@ def get_model_for_cluster(cluster_name, rc=None):
model = ModelBuilder(None, conf, rc.os())
return model
except Exception, e:
- # log this
+ print "Error getting conf %s" % e
pass
# Couldn't get the conf from any nodes
@@ -61,10 +71,29 @@ def get_cluster_status(rc):
try:
doc = rq.getClusterStatusBatch(rc)
except Exception, e:
- # log this
return None
return doc
+def get_status_for_cluster(name, rc=None):
+ if rc is None:
+ rc = get_agent_for_cluster(name)
+ if rc is None:
+ print "No ricci agent"
+ # log this
+ return None
+
+ status_xml = get_cluster_status(rc)
+ if status_xml is None:
+ print "no status xml"
+ # log this
+ return None
+ try:
+ return ClusterStatus(status_xml)
+ except Exception, e:
+ print "parsing xml: %s" % str(e)
+ # log this
+ return None
+
def get_cluster_list():
db_obj = None
try:
@@ -77,13 +106,30 @@ def get_cluster_list():
return db_obj
+def get_cluster_list_full():
+ cluster_list = {}
+
+ db_objs = get_cluster_list()
+ for i in db_objs:
+ cluster_name = i.name
+ rc = get_agent_for_cluster(cluster_name)
+ if rc is None:
+ print "Unable to find a ricci agent for cluster %s" % cluster_name
+ # log this
+ continue
+ cluster_list[cluster_name] = {
+ 'model': get_model_for_cluster(cluster_name, rc),
+ 'status': get_status_for_cluster(cluster_name)
+ }
+ return cluster_list
+
def get_cluster_task_status(cluster_name):
db_obj = get_cluster_db_obj(cluster_name)
if db_obj is None:
return None
tasks = db_obj.tasks
if len(tasks) > 0:
- pass
# get status
+ pass
return False
diff --git a/luci/lib/demo_data.py b/luci/lib/demo_data.py
index 7eea305..aa3cbf8 100644
--- a/luci/lib/demo_data.py
+++ b/luci/lib/demo_data.py
@@ -1,18 +1,28 @@
# -*- coding: utf-8 -*-
"""Static demo data."""
+from pylons.i18n import lazy_ugettext as l_
+from luci.lib import db_helpers
+
__all__ = ['ClusterData']
class ClusterData:
"""Class representation of overall demo data."""
+
def __init__(self):
- self.clusters = {'ClusterOne': ClusterOne(),
- 'ClusterTwo': ClusterTwo(),
- 'ClusterThree': ClusterThree()}
+ clu = {}
+ clu_list = db_helpers.get_cluster_list()
+ if clu_list:
+ for i in clu_list:
+ clu[i.display_name] = db_helpers.get_model_for_cluster(i.display_name)
+ self.clusters = clu
self.storage = {'storage_a': StorageA(),
'storage_b': StorageB(),
'storage_c': StorageC()}
+ self.global_res = {'Resource 1': Resource_1(),
+ 'Resource 2': Resource_2(),
+ 'Resource 3': Resource_3()}
# Certain clusters.
@@ -31,6 +41,7 @@ class Cluster:
class ClusterOne(Cluster):
"""Data for 'ClusterOne'."""
+
def __init__(self):
self.status = self.CLUSTER_OK
self.cluster_votes = 2
@@ -41,33 +52,42 @@ class ClusterOne(Cluster):
'serviceload': 10,
'status': self.NODE_ACTIVE,
'services': ('Service W'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'ricci': {'running': True, 'autostart': True}}},
'NodeBeta': {'ip': '144.92.235.12',
'serviceload': 20,
'status': self.NODE_ACTIVE,
'services': ('Service X', 'Service Y', 'Service Z'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeDelta': {'ip': '144.92.235.13',
'serviceload': 30,
'status': self.NODE_INACTIVE,
'msg': 'Something terrible',
'services': ('Service X'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeGamma': {'ip': '144.92.235.14',
'serviceload': 40,
'status': self.NODE_ACTIVE,
'services': ('Service Y'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeEpsilon': {'ip': '144.92.235.15',
'serviceload': 50,
'status': self.NODE_UNKNOWN,
'services': ('Service Z'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}}}
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}}}
self.services = \
{'Service W': {'running': True,
@@ -166,6 +186,7 @@ class ClusterOne(Cluster):
class ClusterTwo(Cluster):
"""Data for 'ClusterTwo'."""
+
def __init__(self):
self.status = self.CLUSTER_OK
self.cluster_votes = 2
@@ -176,33 +197,43 @@ class ClusterTwo(Cluster):
'serviceload': 10,
'status': self.NODE_ACTIVE,
'services': ('Service W 2'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeBeta 2': {'ip': '144.92.235.12',
'serviceload': 20,
'status': self.NODE_ACTIVE,
'services': ('Service X 2', 'Service Y 2', 'Service Z 2'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeDelta 2': {'ip': '144.92.235.13',
'serviceload': 30,
'status': self.NODE_INACTIVE,
'msg': 'Something terrible',
'services': ('Service X 2'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeGamma 2': {'ip': '144.92.235.14',
'serviceload': 40,
'status': self.NODE_ACTIVE,
'services': ('Service Y 2'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeEpsilon 2': {'ip': '144.92.235.15',
'serviceload': 50,
'status': self.NODE_UNKNOWN,
'services': ('Service Z 2'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}}}
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}}}
self.services = \
{'Service W 2': {'running': True,
@@ -301,6 +332,7 @@ class ClusterTwo(Cluster):
class ClusterThree(Cluster):
"""Data for 'ClusterThree'."""
+
def __init__(self):
self.status = self.CLUSTER_OK
@@ -312,33 +344,43 @@ class ClusterThree(Cluster):
'serviceload': 10,
'status': self.NODE_ACTIVE,
'services': ('Service W 3'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeBeta 3': {'ip': '144.92.235.12',
'serviceload': 20,
'status': self.NODE_ACTIVE,
'services': ('Service X 3', 'Service Y 3', 'Service Z 3'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeDelta 3': {'ip': '144.92.235.13',
'serviceload': 30,
'status': self.NODE_INACTIVE,
'msg': 'Something terrible',
'services': ('Service X 3'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeGamma 3': {'ip': '144.92.235.14',
'serviceload': 40,
'status': self.NODE_ACTIVE,
'services': ('Service Y 3'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}},
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}},
'NodeEpsilon 3': {'ip': '144.92.235.15',
'serviceload': 50,
'status': self.NODE_UNKNOWN,
'services': ('Service Z 3'),
- 'cman': {'running': True, 'autostart': True},
- 'rgmanager': {'running': True, 'autostart': True}}}
+ 'daemons': { 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'running': True, 'autostart': True},
+ 'corosync': {'running': True, 'autostart': True},
+ 'openais': {'running': True, 'autostart': True}}}}
self.services = \
{'Service W 3': {'running': True,
@@ -435,24 +477,128 @@ class ClusterThree(Cluster):
'NodeGamma 3': 2}}}
-
# Certain storage.
class StorageA:
"""Data for 'storage_a'."""
+
def __init__(self):
self.total = 500
self.drives = 2
class StorageB:
"""Data for 'storage_a'."""
+
def __init__(self):
self.total = 1500
self.drives = 3
class StorageC:
"""Data for 'storage_a'."""
+
def __init__(self):
self.total = 400
self.drives = 1
+
+# Certain global resources.
+
+class Resource:
+ """Base class for resources."""
+
+ TYPE_NONE = -1
+ TYPE_IP = 0
+ TYPE_FS = 1
+ TYPE_GFS = 2
+ TYPE_NFSM = 3
+ TYPE_NFSC = 4
+ TYPE_NFSX = 5
+ TYPE_SCR = 6
+ TYPE_SMB = 7
+
+ TYPE_APACHE = 8
+ TYPE_LVM = 9
+ TYPE_MYSQL = 10
+ TYPE_OPENLDAP = 11
+ TYPE_POSTGRES8 = 12
+ TYPE_TOMCAT5 = 13
+ TYPE_SAPDB = 14
+ TYPE_SAPI = 15
+ TYPE_SYBASEA = 16
+ TYPE_ORACLEDB = 17
+
+ # type ... Name of the resource type to display.
+ # main_data ... Which attribute to use as a 'main data item' to display.
+ TYPES = {TYPE_NONE: {'type': l_('no title'),
+ 'main_data': 'empty'},
+ TYPE_IP: {'type': l_('IP'),
+ 'main_data': 'ip_address'},
+ TYPE_FS: {'type': l_('File System'),
+ 'main_data': 'name'},
+ TYPE_GFS: {'type': l_('GFS'),
+ 'main_data': 'name'},
+ TYPE_NFSM: {'type': l_('NFS Mount'),
+ 'main_data': 'name'},
+ TYPE_NFSC: {'type': 'NFS Client',
+ 'main_data': 'name'},
+ TYPE_NFSX: {'type': l_('NFS Export'),
+ 'main_data': 'name'},
+ TYPE_SCR: {'type': l_('Script'),
+ 'main_data': 'name'},
+ TYPE_SMB: {'type': l_('Samba Server'),
+ 'main_data': 'name'},
+ TYPE_APACHE: {'type': l_('Apache'),
+ 'main_data': 'name'},
+ TYPE_LVM: {'type': l_('LVM'),
+ 'main_data': 'name'},
+ TYPE_MYSQL: {'type': l_('MySQL'),
+ 'main_data': 'name'},
+ TYPE_OPENLDAP: {'type': l_('Open LDAP'),
+ 'main_data': 'name'},
+ TYPE_POSTGRES8: {'type': l_('PostgreSQL8'),
+ 'main_data': 'name'},
+ TYPE_TOMCAT5: {'type': l_('Tomcat 5'),
+ 'main_data': 'name'},
+ TYPE_SAPDB: {'type': l_('SAP Database'),
+ 'main_data': 'name'},
+ TYPE_SAPI: {'type': l_('SAP Instance'),
+ 'main_data': 'name'},
+ TYPE_SYBASEA: {'type': l_('Sybase ASE Failover Instance'),
+ 'main_data': 'name'},
+ TYPE_ORACLEDB: {'type': l_('Oracle 10g Failover Instance'),
+ 'main_data': 'name'}}
+
+
+ def getTypeString(self):
+ return self.TYPES[getattr(self, 'type', self.TYPE_NONE)]['type']
+
+ def getMainData(self):
+ return \
+ getattr(self,
+ self.TYPES[getattr(self, 'type', self.TYPE_NONE)]['main_data'],
+ u'')
+
+
+class Resource_1(Resource):
+ """Data for 'Resource 1'."""
+
+ def __init__(self):
+ self.type = self.TYPE_IP
+ self.ip_address = '192.168.1.100'
+ self.monitor_link = True
+
+
+class Resource_2(Resource):
+ """Data for 'Resource 2'."""
+
+ def __init__(self):
+ self.type = self.TYPE_APACHE
+ self.name = 'exampleapache.com'
+
+class Resource_3(Resource):
+ """Data for 'Resource 3'."""
+
+ def __init__(self):
+ self.type = self.TYPE_FS
+ self.name = 'mirror file system'
+
diff --git a/luci/lib/form_utils.py b/luci/lib/form_utils.py
index d3ab9c1..b19143d 100644
--- a/luci/lib/form_utils.py
+++ b/luci/lib/form_utils.py
@@ -1,16 +1,45 @@
# -*- coding: utf-8 -*-
+"""Utilities used in connection with HTML forms."""
-"""Utilities used in connection to HTML forms."""
-
-from base64 import b64encode, b64decode
from string import maketrans
+# Imported, but not in module's header: base64
__all__ = ['FormUtils']
class FormUtils:
+ """Methods useful for HTML forms, especially `values conversion mechanism'.
+
+ `Values conversion mechanism' for HTML forms:
+
+ (X)HTML forms uses 'name' attribute of some nested elements to
+ use its value within triggered request. This value has to conform
+ the lexical requirements that do not allow some characters within it.
+
+ This situation is handled by a pair of function providing `values
+ conversion mechanism':
+ 1) the original value is encoded in a safe way before it is used
+ as the value of mentioned attribute
+ 2) in the request triggered by corresponding HTML form, these
+ values are converted back to the original form
+
+ The implementation uses base64 encoding with some other additions;
+ references:
+ [1] http://www.w3.org/TR/xhtml1/#C_8
+ [2] http://www.w3.org/TR/html4/types.html#h-6.2
+ [3] http://tools.ietf.org/html/rfc3548.html
+
+ """
- STARTING_CHAR = u'L' # First character has to be [A-Za-z].
+ #---------------------------------------------------------------------------
+ # Values conversion mechanism.
+
+ # First character has to be [A-Za-z].
+ # Note: it's better when `straight' keys (i.e. not converted by following
+ # methods) of the request triggered by the HTML form do not start
+ # with this letter if this conversion mechanism is also used in that
+ # HTML form.
+ STARTING_CHAR = u'L'
B64_ORIG_CHARS = u'=+/'
B64_CONV_CHARS = u'-_:'
@@ -20,52 +49,62 @@ class FormUtils:
@classmethod
- def string2Id(cls, 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.
+ def string2Id(cls, orig_string):
+ """Convert original string to the form usable within HTML form.
- 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
+ See doc-string for the class.
Keyword arguments:
- s String to convert.
+ orig_string Original string to convert.
+
+ Return value(s):
+ Encoded string.
"""
- encoded = b64encode(s.encode('utf-8'))
- return cls.STARTING_CHAR + encoded.translate(cls.TRANS_TABLE_TO_CONV)
+ from base64 import b64encode
+
+ b64_string = b64encode(orig_string.encode('utf-8'))
+ return cls.STARTING_CHAR + b64_string.translate(cls.TRANS_TABLE_TO_CONV)
@classmethod
- def id2String(cls, s):
- """Receive the string previously encoded by string2id().
+ def id2String(cls, id_string):
+ """Receive the original string previously encoded by string2Id().
+
+ See doc-string for the class.
Keyword arguments:
- s String to convert back to human readable format.
+ id_string String to convert back to human readable format.
+
+ Return value(s):
+ String converted back from its encoded form.
+
"""
- back = b64decode(str(s)[1:].translate(cls.TRANS_TABLE_FROM_CONV))
- return back.decode('utf-8')
+ from base64 import b64decode
+
+ b64_string = str(id_string)[1:].translate(cls.TRANS_TABLE_FROM_CONV)
+ return b64decode(b64_string).decode('utf-8')
+ #---------------------------------------------------------------------------
+ # Other useful methods.
+
@staticmethod
- def compactString(s, maxlong, append=u'...'):
- """For string longer than maxlong, shorten it and append e.g. '...'.
+ def compactString(long_string, max_length, append=u'...'):
+ """For string longer than max_length, shorten it and append e.g. '...'.
Keyword arguments:
- s String to be `compacted'.
- maxlong Maximum tolerated length of the input string.
+ long_string String to be `compacted'.
+ max_length Maximum tolerated length of the input string.
append What to append to the `compacted' string.
Return value:
- `Compacted' string if the input one was longer than maxlong,
+ `Compacted' string if the input one was longer than max_length,
original string otherwise.
"""
- if len(s) > maxlong:
- return s[:maxlong] + append
+ if len(long_string) > max_length:
+ return long_string[:max_length] + append
else:
- return s
+ return long_string
+
diff --git a/luci/lib/helpers.py b/luci/lib/helpers.py
index a2ab731..61427bf 100644
--- a/luci/lib/helpers.py
+++ b/luci/lib/helpers.py
@@ -1,21 +1,46 @@
# -*- coding: utf-8 -*-
+"""Helpers used in Luci."""
-"""Helpers used in luci."""
-
+# TODO: Remove following line, it comes from original TG's app template (?).
#from webhelpers import date, feedgenerator, html, number, misc, text
+__all__ = ['singleton',
+ 'relativeUrlSlashPrefix', 'relativeUrlList2Str']
+
+
+def singleton(cls):
+ """Trivial class decorator that replace the class with an instance of it."""
+ return cls()
+
+
+#-------------------------------------------------------------------------------
+# Functions related to URL.
+
+def relativeUrlSlashPrefix(url_string):
+ """Ensures that given URL (as a string) starts with a slash.
+
+ Keyword arguments:
+ url_string URL (as a string).
+
+ Return value(s):
+ 'url_string' argument surely starting with a slash.
+
+ """
+ if not url_string.startswith(u'/'):
+ url_string = u'/' + url_string
+ return url_string
-__all__ = ['urlList2String']
-def urlList2String(*url_list):
- """Converts list of URL's parts to string begging with '/'.
+def relativeUrlList2Str(*url_parts):
+ """Converts list of URL parts to string beginning with '/'.
Keyword arguments:
- url_list List of URL's parts.
+ url_parts List of URL parts.
+
+ Return value(s):
+ All URL parts joined with a slash, the whole such string starts also
+ with a slash.
"""
+ return relativeUrlSlashPrefix(u'/'.join(url_parts))
- retval = u'/'.join(url_list)
- if not retval.startswith(u'/'):
- retval = u'/' + retval
- return retval
diff --git a/luci/lib/ricci_communicator.py b/luci/lib/ricci_communicator.py
index fbf61c4..391c03c 100644
--- a/luci/lib/ricci_communicator.py
+++ b/luci/lib/ricci_communicator.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2006-2009 Red Hat, Inc.
+# Copyright (C) 2006-2010 Red Hat, Inc.
#
# This program is free software; you can redistribute
# it and/or modify it under the terms of version 2 of the
diff --git a/luci/model/objects.py b/luci/model/objects.py
index ec520fe..2e78ee1 100644
--- a/luci/model/objects.py
+++ b/luci/model/objects.py
@@ -46,7 +46,7 @@ class Node(DeclarativeBase):
#{ Columns
node_id = Column(Integer, autoincrement=True, primary_key=True)
- node_name = Column(Unicode(16), unique=True, nullable=False)
+ node_name = Column(Unicode(255), unique=False, nullable=False)
display_name = Column(Unicode(255))
hostname = Column(Unicode(255))
ipaddr = Column(String(16))
diff --git a/luci/public/css/global_res.css b/luci/public/css/global_res.css
new file mode 100644
index 0000000..87963a6
--- /dev/null
+++ b/luci/public/css/global_res.css
@@ -0,0 +1,22 @@
+/* ===================== G L O B A L R E S O U R C E S ==================== */
+
+/* -------------------------------- overview -------------------------------- */
+
+.globalres_tlist_type {
+ text-align: left;
+}
+
+th.globalres_tlist_type {
+ width: 15%;
+ min-width: 15%;
+}
+
+.globalres_tlist_main {
+ text-align: left;
+}
+
+th.globalres_tlist_main {
+ width: 45%;
+ min-width: 45%;
+}
+
diff --git a/luci/public/js/shared.js b/luci/public/js/shared.js
new file mode 100644
index 0000000..5ef6e0e
--- /dev/null
+++ b/luci/public/js/shared.js
@@ -0,0 +1,39 @@
+/*
+ * Shared JS functions.
+ *
+ */
+
+
+/**
+ * Handle request for an overview collapse.
+ */
+function onOverviewCollapse()
+ {
+ var tb_shown = document.getElementById('toolbar_shown');
+ var tb_collapsed = document.getElementById('toolbar_collapsed');
+ var overview = document.getElementById('overview');
+ if (tb_shown && tb_collapsed)
+ {
+ tb_shown.style.display = 'none';
+ tb_collapsed.style.display = 'inherit';
+ overview.style.display = 'none';
+ }
+ }
+
+
+/**
+ * Handle request for an overview show.
+ */
+function onOverviewShow()
+ {
+ var tb_shown = document.getElementById('toolbar_shown');
+ var tb_collapsed = document.getElementById('toolbar_collapsed');
+ var overview = document.getElementById('overview');
+ if (tb_shown && tb_collapsed)
+ {
+ tb_shown.style.display = 'inherit';
+ tb_collapsed.style.display = 'none';
+ overview.style.display = 'inherit';
+ }
+ }
+
diff --git a/luci/templates/cluster_forms.html b/luci/templates/cluster_forms.html
new file mode 100644
index 0000000..61d84c8
--- /dev/null
+++ b/luci/templates/cluster_forms.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<xi:include href="master.html" />
+
+<head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
+ <title py:content="''">Template: Forms connected with clusters</title>
+</head>
+
+<body>
+ <div class="mainpage"
+ py:choose="page">
+
+ <div py:when="'add_existing'">
+ <h2>Add an Existing Cluster</h2>
+ <p>Enter credentials for a node from the cluster you wish to add to the cluster management interface.</p>
+ <div py:replace="tmpl_context.form()">Input Form</div>
+ </div>
+
+ <div py:when="'create_cluster'">
+ <h2>Create a New Cluster</h2>
+ <div py:replace="tmpl_context.form()">Input Form</div>
+ </div>
+
+ </div>
+</body>
+</html>
diff --git a/luci/templates/cluster_list.html b/luci/templates/cluster_list.html
index 6ea5142..ac0b70c 100644
--- a/luci/templates/cluster_list.html
+++ b/luci/templates/cluster_list.html
@@ -6,12 +6,16 @@
<xi:include href="master.html" />
+<?python
+ from luci.lib import db_helpers
+?>
+
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title>${title()}</title>
</head>
-<body py:with="clusters = app_globals.data.clusters;
+<body py:with="clusters = db_helpers.get_cluster_list_full();
form_utils = app_globals.form_utils">
<form action="${tg.url(page + '/apply')}" method="post">
@@ -44,19 +48,19 @@
py:with="identifier = form_utils.string2Id(entity_name)">
<td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
<!--! Branch according to the status of the cluster. -->
- <py:choose test="cluster_data['status']">
+ <py:choose test="cluster_data['status'].quorate">
<!--! 1) Cluster is OK. -->
- <py:when test="cluster_data.CLUSTER_OK">
+ <py:when test="'true'">
<td class="icon"></td>
<td class="main_id">
<a href="${tg.url('/' + page + '/' + entity_name) + '/'}">
<span class="entity_ok">${entity_name}</span>
</a>
</td>
- <td class="cluster_tlist_status">OK</td>
+ <td class="cluster_tlist_status">Quorate</td>
</py:when>
<!--! 2) Cluster is not OK. -->
- <py:when test="cluster_data.CLUSTER_NOT_OK">
+ <py:when test="'false'">
<td class="icon">
<img src="${tg.url('/images/exclamation.png')}" alt="Cluster is not OK." />
</td>
@@ -65,10 +69,10 @@
<span class="entity_fail">${entity_name}</span>
</a>
</td>
- <td class="cluster_tlist_status">Not OK</td>
+ <td class="cluster_tlist_status">Not Quorate</td>
</py:when>
<!--! 3) Status of the cluster is unknown. -->
- <py:when test="cluster_data.CLUSTER_UNKNOWN">
+ <py:when test="">
<td class="icon">
<img src="${tg.url('/images/question.png')}" alt="Status of the cluster is unknown." />
</td>
@@ -80,8 +84,8 @@
<td class="cluster_tlist_status">Status uknown</td>
</py:when>
</py:choose>
- <td class="cluster_tlist_votes">${cluster_data['cluster_votes']}</td>
- <td class="cluster_tlist_quorum">${cluster_data['required_quorum']}</td>
+ <td class="cluster_tlist_votes">${cluster_data['status'].votes}</td>
+ <td class="cluster_tlist_quorum">${cluster_data['status'].minQuorum}</td>
<td class="table_space"></td>
</tr>
</tbody>
diff --git a/luci/templates/cluster_part/__init__.py b/luci/templates/cluster_part/__init__.py
new file mode 100644
index 0000000..d06d133
--- /dev/null
+++ b/luci/templates/cluster_part/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+"""Templates package for pages connected with certain cluster."""
diff --git a/luci/templates/cluster_part/failover.html b/luci/templates/cluster_part/failover.html
new file mode 100644
index 0000000..37def84
--- /dev/null
+++ b/luci/templates/cluster_part/failover.html
@@ -0,0 +1,234 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<xi:include href="master.html" />
+
+<head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
+ <title py:content="''">Template: Failovers</title>
+ <script type="text/javascript" src="${tg.url('/js/failover.js')}"></script>
+ <script type="text/javascript" src="${tg.url('/js/shared.js')}"></script>
+ <link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/css/failover.css')}" />
+</head>
+
+<body onload="onLoad()"
+ py:with="cluster_data = tmpl_context.cluster_data;
+ form_utils = app_globals.form_utils">
+
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
+ <div class="sectionblock">
+ <div class="toolbar" id="toolbar_collapsed">
+ <div class="toolbar_view_button" onclick="onOverviewShow()">show</div>
+ </div>
+ <div class="toolbar" id="toolbar_shown">
+ <input type="submit" name="${apply_cmds.DELETE}" value="${_('Delete')}" class="toolbar_button" id="tb_delete"/>
+ <a href="${tg.url(cmd_url + '/add')}" class="toolbar_button" id="tb_add">Add</a>
+ <div class="toolbar_view_button" onclick="onOverviewCollapse()">collapse</div>
+ </div>
+
+ <!--! OVERVIEW SECTION. -->
+ <div id="overview">
+ <table id="fdom_tlist" class="maintable">
+ <thead>
+ <tr>
+ <th class="checkbox"></th>
+ <th class="icon"></th>
+ <th class="main_id">Name</th>
+ <th class="fdom_tlist_enabled">Enabled</th>
+ <th class="fdom_tlist_prioritizied">Prioritized</th>
+ <th class="fdom_tlist_restricted">Restricted</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <!--! List all the failover domains. -->
+ <tr py:for="i, (entity_name, failover_data) in enumerate(cluster_data.failovers.iteritems())"
+ py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
+ py:with="identifier=form_utils.string2Id(entity_name)">
+ <td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
+ <td class="icon"></td>
+ <td class="main_id"><a href="${tg.url(base_url + '/' + entity_name)}">${entity_name}</a></td>
+ <!--! TODO: Is the following column necessary? -->
+ <td class="fdom_tlist_enabled">
+ <input type="checkbox" disabled="true" py:attrs="False and {'checked': 'checked'} or None"/>
+ </td>
+ <td class="fdom_tlist_prioritizied"><img py:if="failover_data['prioritized']" src="${tg.url('/images/dot.png')}" alt="*" /></td>
+ <td class="fdom_tlist_restricted"><img py:if="failover_data['restricted']" src="${tg.url('/images/dot.png')}" alt="*" /></td>
+ <td class="table_space"></td>
+ </tr>
+ <!--! Fix error reported by validator if there is no item at all. -->
+ <tr py:if="len(cluster_data.failovers) == 0"><td colspan="7"></td></tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </form>
+
+ <!--! DETAILS SECTION. -->
+ <div class="sectionblock">
+ <div id="details" py:choose="name">
+
+ <py:when test="None">
+ <div id="details_header">
+ <div id="not_selected" py:choose="len(cluster_data.failovers)">
+ <py:when test="0">No item to display</py:when>
+ <py:otherwise>Select an item to view details</py:otherwise>
+ </div>
+ </div>
+ </py:when>
+
+ <py:otherwise py:with="details = cluster_data.failovers[name]">
+
+ <!--! DETAILS - header section. -->
+ <div id="details_header">
+ <h3 py:content="name">Failover name</h3>
+ <div id="details_header_buttons">
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.DELETE + '&name=' + name)}" id="dh_delete" title="delete"><span class="hide">Delete</span></a>
+ </div>
+ </div>
+
+ <!--! DETAILS - attributes section. -->
+ <div class="details_section">
+ <div class="details_inner">
+ <form action="${tg.url(cmd_url + '/update_properties')}" method="post">
+ <input type="hidden" name="name" value="${form_utils.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" 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">
+ Order the nodes to which services failover.
+ </td>
+ </tr>
+ <tr>
+ <td class="fdom_tattr_checkbox">
+ <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">
+ Kill service when all member nodes fail.
+ </td>
+ </tr>
+ <tr>
+ <td class="fdom_tattr_checkbox">
+ <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">
+ Send service back to 1st priority node when it becomes available again.
+ </td>
+ </tr>
+ </table>
+ </form>
+ </div>
+ </div>
+
+ <!--! DETAILS - services section. -->
+ <div class="details_section">
+ <a href="${tg.url(service_cmd_url + '/add?failover=' + name)}" class="float_button">Add a Service</a>
+ <h4>Services</h4>
+ <div class="details_inner">
+ <table id="fdom_tservices" class="detailstable">
+ <tbody>
+ <!--! List all the services connected with the current failover domain. -->
+ <!--! 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 = cluster_data.services.has_key(service) and (cluster_data.services[service]['running'])">
+ <td class="icon">
+ <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="running and {'class': 'entity_ok'} or {'class': 'entity_fail'}">${service}</span></td>
+ </py:with>
+ <td class="table_space"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <!--! DETAILS - members section. -->
+ <div class="details_section">
+ <form action="${tg.url(cmd_url + '/update_members')}" method="post">
+ <input type="hidden" name="name" value="${form_utils.string2Id(name)}"/>
+ <input type="submit" value="Update Settings" class="float_button"/>
+ <h4>Members</h4>
+ <div class="details_inner">
+ <table id="fdom_tmembers" class="detailstable">
+ <thead>
+ <tr class="grid_row">
+ <th class="icon"></th>
+ <th class="fdom_tmembers_name"></th>
+ <th class="fdom_tmembers_member">Member</th>
+ <th class="fdom_tmembers_priority">Priority</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <!--! List all the nodes (of the current cluster). -->
+ <tr py:for="node, node_dict in cluster_data.nodes.iteritems()" class="grid_row">
+ <!--! Branch according to the status of the node. -->
+ <py:choose test="node_dict.get('status', cluster_data.NODE_UNKNOWN)">
+ <!--! 1) Node is active. -->
+ <py:when test="cluster_data.NODE_ACTIVE">
+ <td class="icon"></td>
+ <td class="fdom_tmembers_name"><span class="entity_ok">${node}</span></td>
+ </py:when>
+ <!--! 2) Node is inactive. -->
+ <py:when test="cluster_data.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="cluster_data.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=form_utils.string2Id(node)">
+ <!--! 1) Yes. -->
+ <py:when test="True">
+ <td class="fdom_tmembers_member">
+ <input id="${identifier+'.check'}" name="${identifier+'.check'}" type="checkbox" checked="checked" onchange="onCheckMember(this.id)"/>
+ </td>
+ <td class="fdom_tmembers_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>
+ <td class="fdom_tmembers_member">
+ <input id="${identifier + '.check'}" name="${identifier+'.check'}" type="checkbox" onchange="onCheckMember(this.id)"/>
+ </td>
+ <td class="fdom_tmembers_priority">
+ <input id="${identifier + '.priority'}" name="${identifier + '.priority'}" type="text" value="0" maxlength="3" class="input_priority_disabled"/>
+ </td>
+ </py:otherwise>
+ </py:choose>
+ <td class="table_space"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </form>
+ </div>
+
+ </py:otherwise>
+
+ </div>
+ </div>
+
+</body>
+</html>
diff --git a/luci/templates/cluster_part/fence.html b/luci/templates/cluster_part/fence.html
new file mode 100644
index 0000000..3e8fbd7
--- /dev/null
+++ b/luci/templates/cluster_part/fence.html
@@ -0,0 +1,176 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<xi:include href="master.html" />
+
+<head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
+ <title py:content="''">Template: Fences</title>
+ <script type="text/javascript" src="${tg.url('/js/shared.js')}"></script>
+ <link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/css/fence.css')}" />
+</head>
+
+<body py:with="cluster_data = tmpl_context.cluster_data;
+ form_utils = app_globals.form_utils">
+
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
+ <div class="sectionblock">
+ <div class="toolbar" id="toolbar_collapsed">
+ <div class="toolbar_view_button" onclick="onOverviewShow()">show</div>
+ </div>
+ <div class="toolbar" id="toolbar_shown">
+ <input type="submit" name="${apply_cmds.UPDATE}" value="${_('Update')}" class="toolbar_button" id="tb_update"/>
+ <input type="submit" name="${apply_cmds.DELETE}" value="${_('Delete')}" class="toolbar_button" id="tb_delete"/>
+ <a href="${tg.url(cmd_url + '/add')}" class="toolbar_button" id="tb_add">Add</a>
+ <div class="toolbar_view_button" onclick="onOverviewCollapse()">collapse</div>
+ </div>
+
+ <!--! OVERVIEW SECTION. -->
+ <div id="overview">
+ <table id="fence_tlist" class="maintable">
+ <thead>
+ <tr>
+ <th class="checkbox"></th>
+ <th class="icon"><img src="${tg.url('/images/global_11x11_black.png')}" alt="shared fences"/></th>
+ <th class="main_id">Name</th>
+ <th class="fence_tlist_type">Fence Type</th>
+ <th class="fence_tlist_members">Member Nodes</th>
+ <th class="fence_tlist_ip">IP Address</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <!--! List all the fences. -->
+ <tr py:for="i, (entity_name, fence_data) in enumerate(cluster_data.fences.iteritems())"
+ py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
+ py:with="identifier = form_utils.string2Id(entity_name);
+ local_fence = len(fence_data['nodes']) == 1">
+ <td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
+ <!--! If the fence is shared, display appropriate icon. -->
+ <td class="icon"><img py:if="not local_fence" src="${tg.url('/images/global_11x11_black.png')}" alt="shared fence"/></td>
+ <td class="main_id"><a href="${tg.url(base_url + '/' + entity_name)}">${entity_name}</a></td>
+ <td class="fence_tlist_type">${fence_data['type']}</td>
+ <!--! Branch according to whether the fence is local or shared. -->
+ <py:choose test="local_fence">
+ <!--! 1) Local. -->
+ <td py:when="True" class="fence_tlist_members"
+ py:with="local_node = fence_data['nodes'].keys()[0]">
+ <py:choose test="cluster_data.nodes.get(local_node, {}).get('status', cluster_data.NODE_UNKNOWN)">
+ <span py:when="cluster_data.NODE_ACTIVE" class="entity_ok">${local_node}</span>
+ <span py:when="cluster_data.NODE_INACTIVE" class="entity_fail">${local_node}</span>
+ <span py:when="cluster_data.NODE_UNKNOWN" class="entity_unknown">${local_node}</span>
+ </py:choose>
+ </td>
+ <!--! 2) Shared. -->
+ <td py:otherwise="" class="fence_tlist_members">${len(fence_data['nodes'])}</td>
+ </py:choose>
+ <td class="fence_tlist_ip">${fence_data['ip']}</td>
+ <td class="table_space"></td>
+ </tr>
+ <!--! Fix error reported by validator if there is no item at all. -->
+ <tr py:if="len(cluster_data.fences) == 0"><td colspan="7"></td></tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </form>
+
+ <!--! DETAILS SECTION. -->
+ <div class="sectionblock">
+ <div id="details" py:choose="name">
+
+ <py:when test="None">
+ <div id="details_header">
+ <div id="not_selected" py:choose="len(cluster_data.fences)">
+ <py:when test="0">No item to display</py:when>
+ <py:otherwise>Select an item to view details</py:otherwise>
+ </div>
+ </div>
+ </py:when>
+
+ <py:otherwise py:with="details = cluster_data.fences[name]">
+
+ <!--! DETAILS - header section. -->
+ <div id="details_header">
+ <h3 py:content="name">Fence A</h3>
+ <div id="details_header_buttons">
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.DELETE + '&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.UPDATE + '&name=' + name)}" id="dh_update" title="update"><span class="hide">update</span></a>
+ <!-- TODO: remove? <a href="${tg.url(request.path_qs)}" id="dh_update" title="refresh"><span class="hide">refresh</span></a> -->
+ </div>
+ <span class="details_header_info_label">Type</span> <span class="details_header_info">${details['type']}</span>
+ </div>
+
+ <!--! DETAILS - hostname/IP/port section. -->
+ <div class="details_section">
+ <ul id="fence_details_list">
+ <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. -->
+ <div class="details_section">
+ <a href="${tg.url(node_base_url)}" class="float_button">Manage Nodes</a>
+ <h4>Nodes</h4>
+ <div class="details_inner">
+ <table id="fence_tnodes" class="detailstable">
+ <thead>
+ <tr class="grid_row">
+ <th class="icon"></th>
+ <th class="fence_tnodes_name"></th>
+ <th class="fence_tnodes_status">status</th>
+ <th class="fence_tnodes_level">level</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <!--! List all the nodes connected with the current (shared) fence. -->
+ <tr py:for="node, node_dict in cluster_data.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', cluster_data.NODE_UNKNOWN)">
+ <!--! 1) Node is active. -->
+ <py:when test="cluster_data.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="cluster_data.NODE_INACTIVE">
+ <td class="icon">
+ <img src="${tg.url('/images/exclamation.png')}" alt="Node has a problem." />
+ </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="cluster_data.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>
+ </table>
+ </div>
+ </div>
+
+ </py:otherwise>
+
+ </div>
+ </div>
+
+</body>
+</html>
diff --git a/luci/templates/cluster_part/node.html b/luci/templates/cluster_part/node.html
new file mode 100644
index 0000000..2292fb2
--- /dev/null
+++ b/luci/templates/cluster_part/node.html
@@ -0,0 +1,244 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<xi:include href="master.html" />
+
+<head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
+ <title py:content="''">Template: Nodes</title>
+ <script type="text/javascript" src="${tg.url('/js/shared.js')}"></script>
+ <link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/css/node.css')}" />
+</head>
+
+<body py:with="cluster_data = tmpl_context.cluster_data;
+ form_utils = app_globals.form_utils">
+
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
+ <div class="sectionblock">
+ <div class="toolbar" id="toolbar_collapsed">
+ <div class="toolbar_view_button" onclick="onOverviewShow()">show</div>
+ </div>
+ <div class="toolbar" id="toolbar_shown">
+ <input type="submit" name="${apply_cmds.REBOOT}" value="${_('Reboot')}" class="toolbar_button" id="tb_reboot" />
+ <input type="submit" name="${apply_cmds.FENCE}" value="${_('Fence')}" class="toolbar_button" id="tb_fence" />
+ <input type="submit" name="${apply_cmds.LEAVE}" value="${_('Leave Cluster')}" class="toolbar_button" id="tb_leave" />
+ <input type="submit" name="${apply_cmds.DELETE}" value="${_('Delete')}" class="toolbar_button" id="tb_delete" />
+ <a href="${tg.url(cmd_url + '/add')}" class="toolbar_button" id="tb_add">Add</a>
+ <div class="toolbar_view_button" onclick="onOverviewCollapse()">collapse</div>
+ </div>
+
+ <!--! OVERVIEW SECTION. -->
+ <div id="overview">
+ <table id="node_tlist" class="maintable">
+ <thead>
+ <tr>
+ <th class="checkbox"></th>
+ <th class="icon">!</th>
+ <th class="main_id">Name</th>
+ <th class="node_tlist_status">Status</th>
+ <th class="node_tlist_load">Service Load</th>
+ <th class="node_tlist_ip">IP Address</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <!--! List all the nodes. -->
+ <tr py:for="i, (entity_name, node_data) in enumerate(cluster_data.nodes.iteritems())"
+ py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
+ py:with="identifier=form_utils.string2Id(entity_name)">
+ <td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
+ <!--! Branch according to the status of the node. -->
+ <py:choose test="node_data['status']">
+ <!--! 1) Node is active. -->
+ <py:when test="cluster_data.NODE_ACTIVE">
+ <td class="icon"></td>
+ <td class="main_id">
+ <a href="${tg.url(base_url + '/' + entity_name)}">
+ <span class="entity_ok">${entity_name}</span>
+ </a>
+ </td>
+ <td class="node_tlist_status">${_('OK')}</td>
+ </py:when>
+ <!--! 2) Node is inactive. -->
+ <py:when test="cluster_data.NODE_INACTIVE">
+ <td class="icon">
+ <img src="${tg.url('/images/exclamation.png')}" alt="Node has a problem." />
+ </td>
+ <td class="main_id">
+ <a href="${tg.url(base_url + '/' + entity_name)}">
+ <span class="entity_fail">${entity_name}</span>
+ </a>
+ </td>
+ <td class="node_tlist_status">${node_data.get('msg', _('Uknown problem'))}</td>
+ </py:when>
+ <!--! 3) Status of the node is unknown. -->
+ <py:when test="cluster_data.NODE_UNKNOWN">
+ <td class="icon">
+ <img src="${tg.url('/images/question.png')}" alt="Status of the node is unknown." />
+ </td>
+ <td class="main_id">
+ <a href="${tg.url(base_url + '/' + entity_name)}">
+ <span class="entity_unknown">${entity_name}</span>
+ </a>
+ </td>
+ <td class="node_tlist_status">${node_data.get('msg', _('Status unknown'))}</td>
+ </py:when>
+ </py:choose>
+ <td class="node_tlist_load">${node_data['serviceload']}</td>
+ <td class="node_tlist_ip">${node_data['ip']}</td>
+ <td class="table_space"></td>
+ </tr>
+ <!--! Fix error reported by validator if there is no item at all. -->
+ <tr py:if="len(cluster_data.nodes) == 0"><td colspan="7"></td></tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </form>
+
+ <!--! DETAILS SECTION. -->
+ <div class="sectionblock">
+ <div id="details" py:choose="name">
+
+ <py:when test="None">
+ <div id="details_header">
+ <div id="not_selected" py:choose="len(cluster_data.nodes)">
+ <py:when test="0">No item to display</py:when>
+ <py:otherwise>Select an item to view details</py:otherwise>
+ </div>
+ </div>
+ </py:when>
+
+ <py:otherwise py:with="details = cluster_data.nodes[name]">
+
+ <!--! DETAILS - header section. -->
+ <div id="details_header">
+ <h3 py:content="name">Node A</h3>
+ <div id="details_header_buttons">
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.DELETE + '&name=' + name)}" id="dh_reboot" title="reboot"><span class="hide">reboot</span></a>
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.FENCE + '&name=' + name)}" id="dh_fence" title="fence"><span class="hide">fence</span></a>
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.LEAVE + '&name=' + name)}" id="dh_leave" title="leave cluster"><span class="hide">leave cluster</span></a>
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.DELETE + '&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
+ </div>
+ <span class="details_header_info_label">Status</span>
+ <span class="details_header_info" py:choose="details['status']">
+ <py:when test="cluster_data.NODE_ACTIVE">${_('OK')}</py:when>
+ <py:when test="cluster_data.NODE_INACTIVE">${details.get('msg', _('Uknown problem'))}</py:when>
+ <py:when test="cluster_data.NODE_UNKNOWN">${details.get('msg', _('Status uknown'))}</py:when>
+ </span>
+ </div>
+
+ <!--! DETAILS - services section. -->
+ <div class="details_section">
+ <a href="${tg.url(service_cmd_url + '/add?node=' + name)}" class="float_button">Add a Service</a>
+ <h4>Services</h4>
+ <div class="details_inner">
+ <table id="node_tservices" class="detailstable">
+ <tbody>
+ <!--! List all the services connected with the current failover domain. -->
+ <!--! 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 = cluster_data.services.has_key(service) and (cluster_data.services[service]['running'])">
+ <td class="icon">
+ <img py:if="not running" src="${tg.url('/images/exclamation.png')}" alt="Service has a problem." />
+ </td>
+ <td class="node_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>
+ </table>
+ </div>
+ </div>
+
+ <!--! DETAILS - failover domains section. -->
+ <div class="details_section">
+ <a href="${tg.url(cmd_url + '/update_settings?name=' + name)}" class="float_button">Update settings</a>
+ <h4>Failover Domains</h4>
+ <div class="details_inner">
+ <table id="node_tfdoms" class="detailstable">
+ <thead>
+ <tr class="grid_row">
+ <th class="icon"></th>
+ <th class="node_tfdoms_name"></th>
+ <th class="node_tfdoms_priority">Priority</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr py:for="fdom, fdom_dict in cluster_data.failovers.iteritems()" class="grid_row"
+ py:if="fdom_dict.has_key('nodes') and name in fdom_dict['nodes'].keys()">
+ <td class="icon"></td>
+ <td class="node_tfdoms_name">${fdom}</td>
+ <td class="node_tfdoms_priority">${fdom_dict['nodes'][name]}</td>
+ <td class="table_space"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <!--! DETAILS - fence devices section. -->
+ <div class="details_section">
+ <a href="${tg.url(cmd_url + '/update_settings?name=' + name)}" class="float_button">Update settings</a>
+ <h4>Fence Devices</h4>
+ <div class="details_inner">
+ <table id="node_tfences" class="detailstable">
+ <thead>
+ <tr class="grid_row">
+ <th class="icon"></th>
+ <th class="node_tfences_name"></th>
+ <th class="node_tfences_type">Type</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr py:for="fence, fence_dict in cluster_data.fences.iteritems()" class="grid_row"
+ py:if="fence_dict.has_key('nodes') and name in fence_dict['nodes'].keys()">
+ <td class="icon"></td>
+ <td class="node_tfences_name">${fence}</td>
+ <td class="node_tfences_type">${fence_dict['type']}</td>
+ <td class="table_space"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <!--! DETAILS - cluster daemons section. -->
+ <div class="details_section">
+ <a href="${tg.url(cmd_url + '/update_properties?name=' + name)}" class="float_button">Update Properties</a>
+ <h4>Cluster Daemons</h4>
+ <div class="details_inner">
+ <table id="node_tdaemons" class="detailstable">
+ <thead>
+ <tr class="grid_row">
+ <th class="icon"></th>
+ <th class="node_tdaemons_name"></th>
+ <th class="node_tdaemons_status">Status</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr py:for="daemon_name, daemon_data in details.daemons.iteritems()" class="grid_row">
+ <td class="icon"></td>
+ <td class="node_tdaemons_name">${daemon_name}</td>
+ <td class="node_tdaemons_status"
+ py:with="running = daemon_data.get('running', None)">${running is None and _('Unknown') or running == True and _('Running') or _('Not running') }</td>
+ <td class="table_space"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ </py:otherwise>
+
+ </div>
+ </div>
+</body>
+</html>
diff --git a/luci/templates/cluster_part/service.html b/luci/templates/cluster_part/service.html
new file mode 100644
index 0000000..a8958cc
--- /dev/null
+++ b/luci/templates/cluster_part/service.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<xi:include href="master.html" />
+
+<head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
+ <title py:content="''">Template: Services</title>
+ <script type="text/javascript" src="${tg.url('/js/shared.js')}"></script>
+ <link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/css/service.css')}" />
+</head>
+
+<body py:with="cluster_data = tmpl_context.cluster_data;
+ form_utils = app_globals.form_utils">
+
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
+ <div class="sectionblock">
+ <div class="toolbar" id="toolbar_collapsed">
+ <div class="toolbar_view_button" onclick="onOverviewShow()">show</div>
+ </div>
+ <div class="toolbar" id="toolbar_shown">
+ <input type="submit" name="${apply_cmds.REBOOT}" value="${_('Reboot')}" class="toolbar_button" id="tb_reboot" />
+ <input type="submit" name="${apply_cmds.DELETE}" value="${_('Delete')}" class="toolbar_button" id="tb_delete" />
+ <a href="${tg.url(cmd_url + '/add')}" class="toolbar_button" id="tb_add">Add</a>
+ <div class="toolbar_view_button" onclick="onOverviewCollapse()">collapse</div>
+ </div>
+
+ <!--! OVERVIEW SECTION. -->
+ <div id="overview">
+ <table id="service_tlist" class="maintable">
+ <thead>
+ <tr>
+ <th class="checkbox"></th>
+ <th class="icon">!</th>
+ <th class="main_id">Name</th>
+ <th class="service_tlist_status">Status</th>
+ <th class="service_tlist_enabled">Enabled</th>
+ <th class="service_tlist_fdom">Failover Domain</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <!--! List all the services. -->
+ <tr py:for="i, (entity_name, service_data) in enumerate(cluster_data.services.iteritems())"
+ py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
+ py:with="identifier=form_utils.string2Id(entity_name)">
+ <td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
+ <!--! Branch according to the status of the service. -->
+ <py:choose test="service_data['running']">
+ <!--! 1) Service is running. -->
+ <py:when test="True">
+ <td class="icon"></td>
+ <td class="main_id">
+ <a href="${tg.url(base_url + '/' + entity_name)}">
+ <span class="entity_ok">${entity_name}</span>
+ </a>
+ </td>
+ <td class="service_tlist_status"
+ py:with="msg = _('Running on %s') % service_data['node']">
+ ${form_utils.compactString(msg, 18)}
+ <!--! TODO: Avoid following nasty hack, if possible !-->
+ <py:if test="name == entity_name">${setattr(tmpl_context, 'current_service_status', msg)}</py:if>
+ </td>
+ </py:when>
+ <!--! 2) Service is not running. -->
+ <py:when test="False">
+ <td class="icon">
+ <img src="${tg.url('/images/exclamation.png')}" alt="Service has a problem." />
+ </td>
+ <td class="main_id">
+ <a href="${tg.url(base_url + '/' + entity_name)}">
+ <span class="entity_fail">${entity_name}</span>
+ </a>
+ </td>
+ <td class="service_tlist_status"
+ py:with="msg = not service_data['autostart'] and _('Autostart not enabled') or
+ (cluster_data.nodes.has_key(service_data['node']) and cluster_data.nodes[service_data['node']].get('msg', _('Unknown problem'))
+ or _('Internal error'))">
+ ${form_utils.compactString(msg, 18)}
+ <!--! TODO: Avoid following nasty hack, if possible !-->
+ <py:if test="name == entity_name">${setattr(tmpl_context, 'current_service_status', msg)}</py:if>
+ </td>
+ </py:when>
+ </py:choose>
+ <td class="service_tlist_enabled"><input type="checkbox" disabled="disabled"/></td>
+ <td class="service_tlist_fdom">${service_data.get('failover', None) or _('No/default failover domain')}</td>
+ <td class="table_space"></td>
+ </tr>
+ <!--! Fix error reported by validator if there is no item at all. -->
+ <tr py:if="len(cluster_data.services) == 0"><td colspan="7"></td></tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </form>
+
+ <!--! DETAILS SECTION. -->
+ <div class="sectionblock">
+ <div id="details" py:choose="name">
+
+ <py:when test="None">
+ <div id="details_header">
+ <div id="not_selected" py:choose="len(cluster_data.services)">
+ <py:when test="0">No item to display</py:when>
+ <py:otherwise>Select an item to view details</py:otherwise>
+ </div>
+ </div>
+ </py:when>
+
+ <py:otherwise py:with="details = cluster_data.services[name]">
+
+ <!--! DETAILS - header section. -->
+ <div id="details_header">
+ <h3 py:content="name">Service A</h3>
+ <div id="details_header_buttons">
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.DELETE + '&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.REBOOT + '&name=' + name)}" id="dh_update" title="reboot"><span class="hide">reboot</span></a>
+ <!-- TODO: remove? <a href="${tg.url(request.path_qs)}" id="dh_update" title="refresh"><span class="hide">refresh</span></a> -->
+ </div>
+ <span class="details_header_info_label">Status</span> <span class="details_header_info">${tmpl_context.current_service_status}</span>
+ <form style="display: inline; padding-left: 24px; ">
+ <select style="font-size: small;">
+ <option value="null">Start on node...</option>
+ <py:for each="node in cluster_data.nodes.iterkeys()" py:if="node != details['node']">
+ <option value="form_utils.string2Id(node)">${node}</option>
+ </py:for>
+ </select>
+ </form>
+ </div>
+
+ <!--! DETAILS - resources. -->
+ <div class="details_section">
+ <a href="${tg.url(globalres_url)}" class="float_button">Manage Global Resources</a>
+ <!--a href="${tg.url(cmd_url + '/service')}" class="float_button">Create New Resource in this Service</a-->
+ <h4>Resources</h4>
+ <div class="details_inner">
+ <table id="service_tresources" class="detailstable">
+ <thead>
+ <tr class="grid_row">
+ <th class="icon"></th>
+ <th class="service_tresources_name"></th>
+ <th class="service_tresources_type">Type</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <tr py:for="resource, res_vals in details['resources'].iteritems()" class="grid_row">
+ <td class="icon"></td>
+ <td class="service_tresources_name">${resource}</td>
+ <td class="service_tresources_type">${res_vals[0]}</td>
+ <td class="table_space"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ </py:otherwise>
+
+ </div>
+ </div>
+
+</body>
+</html>
diff --git a/luci/templates/configure.html b/luci/templates/configure.html
index 5439a5c..2c58aea 100644
--- a/luci/templates/configure.html
+++ b/luci/templates/configure.html
@@ -57,7 +57,8 @@
<input name="ClusterName" type="text" value="${tmpl_context.cluster_name}"/>
</td>
</tr>
- <tr><td>Configuration Version</td><td><input type="text"/></td></tr>
+ <tr><td>Configuration Version</td>
+ <td><input type="text" value="${cluster_data.getClusterConfigVersion()}"/></td></tr>
<tr><td><div id="button"><b><button>Show advanced cluster properties</button></b></div></td></tr>
<tr><td colspan="2">
<div id="advanced">
@@ -79,9 +80,15 @@
<input type="hidden" name="page" value="Fence"/>
<table>
<tr><td><b>Fence Daemon Properties</b></td></tr>
- <tr><td>Post Fail Delay</td><td><input type="text"/></td></tr>
- <tr><td>Post Join Delay</td><td><input type="text"/></td></tr>
- <tr><td>Run XVM fence daemon</td><td><input type="checkbox"/></td></tr>
+ <tr><td>Post Fail Delay</td><td>
+ <input type="text" name="post_fail_delay" value="${cluster_data.getFenceDaemonPtr().getAttribute('post_fail_delay')}"/>
+ </td></tr>
+ <tr><td>Post Join Delay</td><td>
+ <input type="text" name="post_join_delay" value="${cluster_data.getFenceDaemonPtr().getAttribute('post_join_delay')}"/>
+ </td></tr>
+ <tr><td>Run XVM fence daemon</td><td>
+ <input type="checkbox" named="fence_xvmd"
+ py:attrs="cluster_data.hasFenceXVM() and { 'checked': 'checked' } or {}"/></td></tr>
<tr><td></td></tr>
<tr><td><b>XVM fence dameon key distribution</b></td></tr>
<tr><td>Enter a node hostname from the host cluster</td><td><input type="text"/></td></tr>
@@ -96,7 +103,7 @@
<input type="hidden" name="page" value="Multicast"/>
<table>
<tr><td><b>Multicast Configuration</b></td></tr>
- <tr><td><input name="multicast" type="radio"/> Let cluster choose the multicast address</td></tr>
+ <tr><td><input name="multicast" type="radio" checked="checked"/> Let cluster choose the multicast address</td></tr>
<tr><td><input name="multicast" type="radio"/> Specify the multicast address manually</td></tr>
<tr><td>Multicast address</td><td><input type="text"/></td></tr>
<tr><td>Multicast network interface (optional)</td><td><input type="text"/></td></tr>
@@ -109,14 +116,42 @@
<input type="hidden" name="page" value="Quorum Partition"/>
<table>
<tr><td><b>Quorum Partition Configuration</b></td></tr>
- <tr><td><input name="quorum" type="radio"/> Do not use a Quorum Partition</td></tr>
- <tr><td><input name="quorum" type="radio"/> Use a Quorum Partition</td></tr>
- <tr><td>Internal</td><td><input type="text"/></td></tr>
- <tr><td>Votes</td><td><input type="text"/></td></tr>
- <tr><td>TKO</td><td><input type="text"/></td></tr>
- <tr><td>Minimum Score</td><td><input type="text"/></td></tr>
- <tr><td><input name="labeldev" type="radio"/> Label</td></tr>
- <tr><td><input name="labeldev" type="radio"/> Device (deprecated)</td></tr>
+ <tr><td>
+ <input name="quorumd" type="radio"
+ py:attrs="not cluster_data.quorumd_ptr and {'checked': ''} or {}"/>Do not use a Quorum Partition</td></tr>
+ <tr><td>
+ <input name="quorumd" type="radio"
+ py:attrs="cluster_data.quorumd_ptr and {'checked': ''} or {}"/>Use a Quorum Partition</td></tr>
+ <tr><td>Interval</td><td>
+ <input name="interval" type="text"
+ py:attrs="(cluster_data.quorumd_ptr and {'value': cluster_data.quorumd_ptr.getAttribute('interval')or ''}) or {'disabled':'disabled'}" />
+ </td></tr>
+ <tr><td>Votes</td><td>
+ <input name="votes" type="text"
+ py:attrs="(cluster_data.quorumd_ptr and {'value': cluster_data.quorumd_ptr.getAttribute('votes') or ''}) or {'disabled':'disabled'}" />
+ </td></tr>
+ <tr><td>TKO</td><td>
+ <input name="tko" type="text"
+ py:attrs="(cluster_data.quorumd_ptr and {'value': cluster_data.quorumd_ptr.getAttribute('tko') or ''}) or {'disabled':'disabled'}" />
+ </td></tr>
+ <tr><td>Minimum Score</td><td>
+ <input name="min_score" type="text"
+ py:attrs="(cluster_data.quorumd_ptr and {'value': cluster_data.quorumd_ptr.getAttribute('min_score') or ''}) or {'disabled':'disabled'}" />
+ </td></tr>
+ <tr>
+ <td>
+ <input name="labeldev" type="radio"
+ py:attrs="(cluster_data.quorumd_ptr and cluster_data.quorumd_ptr.getAttribute('label')) and {'checked': ''} or {'disabled':'disabled'}" />Label
+ <input name="label" type="text"
+ py:attrs="(cluster_data.quorumd_ptr and {'value': cluster_data.quorumd_ptr.getAttribute('label') or ''}) or {'disabled':'disabled'}" />
+ </td>
+ </tr>
+ <tr><td>
+ <input name="labeldev" type="radio"
+ py:attrs="(cluster_data.quorumd_ptr and cluster_data.quorumd_ptr.getAttribute('device')) and {'checked': ''} or {'disabled':'disabled'}"/>Device (deprecated)
+ <input name="device" type="text"
+ py:attrs="(cluster_data.quorumd_ptr and {'value': cluster_data.quorumd_ptr.getAttribute('device') or ''}) or {'disabled':'disabled'}" />
+ </td></tr>
</table>
<table>
<tr><td><b>Heuristics</b></td></tr>
diff --git a/luci/templates/failover.html b/luci/templates/failover.html
index c148115..be0c784 100644
--- a/luci/templates/failover.html
+++ b/luci/templates/failover.html
@@ -14,6 +14,7 @@
<body onload="onLoad()"
py:with="cluster_data = tmpl_context.cluster_data;
+ cluster_status = tmpl_context.cluster_status;
form_utils = app_globals.form_utils">
<form action="${tg.url(failovers_cmd)}" method="post">
@@ -31,7 +32,6 @@
<th class="checkbox"></th>
<th class="icon"></th>
<th class="main_id">Name</th>
- <th class="fdom_tlist_enabled">Enabled</th>
<th class="fdom_tlist_prioritizied">Prioritized</th>
<th class="fdom_tlist_restricted">Restricted</th>
<td class="table_space"></td>
@@ -39,18 +39,14 @@
</thead>
<tbody>
<!--! List all the failover domains. -->
- <tr py:for="i, (entity_name, failover_data) in enumerate(cluster_data.failovers.iteritems())"
- py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
- py:with="identifier=entity_name">
+ <tr py:for="i, failover_data in enumerate(cluster_data.getFailoverDomains())"
+ py:with="entity_name=failover_data.getName();identifier=entity_name"
+ py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)">
<td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
<td class="icon"></td>
<td class="main_id"><a href="${tg.url(base_url + '/' + entity_name)}">${entity_name}</a></td>
- <!--! TODO: Is the following column necessary? -->
- <td class="fdom_tlist_enabled">
- <input type="checkbox" disabled="true" py:attrs="False and {'checked': 'checked'} or None"/>
- </td>
- <td class="fdom_tlist_prioritizied"><img py:if="failover_data['prioritized']" src="${tg.url('/images/dot.png')}" alt="*" /></td>
- <td class="fdom_tlist_restricted"><img py:if="failover_data['restricted']" src="${tg.url('/images/dot.png')}" alt="*" /></td>
+ <td class="fdom_tlist_prioritizied"><img py:if="failover_data.getAttribute('ordered') != '0'" src="${tg.url('/images/dot.png')}" alt="*" /></td>
+ <td class="fdom_tlist_restricted"><img py:if="failover_data.getAttribute('restricted') != '0'" src="${tg.url('/images/dot.png')}" alt="*" /></td>
<td class="table_space"></td>
</tr>
</tbody>
@@ -65,14 +61,14 @@
<py:when test="None">
<div id="details_header">
- <div id="not_selected" py:choose="len(cluster_data.failovers)">
+ <div id="not_selected" py:choose="len(cluster_data.getFailoverDomains())">
<py:when test="0">No item to display</py:when>
<py:otherwise>Select an item to view details</py:otherwise>
</div>
</div>
</py:when>
- <py:otherwise py:with="details = cluster_data.failovers[name]">
+ <py:otherwise py:with="details = cluster_data.getFailoverDomainByName(name)">
<!--! DETAILS - header section. -->
<div id="details_header">
@@ -91,7 +87,7 @@
<table id="fdom_tattr">
<tr>
<td class="fdom_tattr_checkbox">
- <input type="checkbox" id="prioritized" name="prioritized" py:attrs="details['prioritized'] and {'checked': 'checked'} or None"/>
+ <input type="checkbox" id="prioritized" name="prioritized" py:attrs="details.getAttribute('ordered') != '0' and {'checked': 'checked'} or None"/>
</td><td class="fdom_tattr_caption">
<label for="prioritized">Prioritized</label>
</td><td class="fdom_tattr_hint">
@@ -100,16 +96,16 @@
</tr>
<tr>
<td class="fdom_tattr_checkbox">
- <input type="checkbox" id="restricted" name="restricted" py:attrs="details['restricted'] and {'checked': 'checked'} or None"/>
+ <input type="checkbox" id="restricted" name="restricted" py:attrs="details.getAttribute('restricted') != '0' and {'checked': 'checked'} or None"/>
</td><td class="fdom_tattr_caption">
<label for="restricted">Restricted</label>
</td><td class="fdom_tattr_hint">
- Kill service when all member nodes fail.
+ Service can run only on nodes specified.
</td>
</tr>
<tr>
<td class="fdom_tattr_checkbox">
- <input type="checkbox" id="failback" name="failback" py:attrs="details['failback'] and {'checked': 'checked'} or None"/>
+ <input type="checkbox" id="failback" name="failback" py:attrs="details.getAttribute('failback') and {'checked': 'checked'} or None"/>
</td><td class="fdom_tattr_caption">
<label for="failback">Failback</label>
</td><td class="fdom_tattr_hint">
@@ -129,14 +125,12 @@
<table id="fdom_tservices" class="detailstable">
<tbody>
<!--! List all the services connected with the current failover domain. -->
- <!--! 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 = cluster_data.services.has_key(service) and (cluster_data.services[service]['running'])">
+ <tr py:for="service in cluster_data.getServicesForFdom(name)" class="grid_row">
+ <py:with vars="running = cluster_status.services.has_key(service.getName()) and (cluster_status.services[service.getName()].running == 'true')">
<td class="icon">
- <img py:if="not running" src="${tg.url('/images/exclamation.png')}" alt="Service has a problem." />
+ <img py:if="not running" src="${tg.url('/images/exclamation.png')}" alt="Service is not running." />
</td>
- <td class="fdom_tservices_service"><span py:attrs="running and {'class': 'entity_ok'} or {'class': 'entity_fail'}">${service}</span></td>
+ <td class="fdom_tservices_service"><span py:attrs="running and {'class': 'entity_ok'} or {'class': 'entity_fail'}">${service.getName()}</span></td>
</py:with>
<td class="table_space"></td>
</tr>
@@ -164,23 +158,24 @@
</thead>
<tbody>
<!--! List all the nodes (of the current cluster). -->
- <tr py:for="node, node_dict in cluster_data.nodes.iteritems()" class="grid_row">
+ <tr py:for="node_dict in cluster_data.getNodes()" class="grid_row"
+ py:with="node = node_dict.getName()">
<!--! Branch according to the status of the node. -->
- <py:choose test="node_dict.get('status', cluster_data.NODE_UNKNOWN)">
+ <py:choose test="cluster_status.nodes[node].clustered">
<!--! 1) Node is active. -->
- <py:when test="cluster_data.NODE_ACTIVE">
+ <py:when test="'true'">
<td class="icon"></td>
<td class="fdom_tmembers_name"><span class="entity_ok">${node}</span></td>
</py:when>
<!--! 2) Node is inactive. -->
- <py:when test="cluster_data.NODE_INACTIVE">
+ <py:when test="'false'">
<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="cluster_data.NODE_UNKNOWN">
+ <py:when test="">
<td class="icon">
<img src="${tg.url('/images/question.png')}" alt="Status of the node is unknown." />
</td>
@@ -189,14 +184,15 @@
</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=form_utils.string2Id(node)">
+ <py:with vars="fdom_node = details.get_member_node(node)">
+ <py:choose test="fdom_node != None" py:with="identifier=form_utils.string2Id(node)">
<!--! 1) Yes. -->
<py:when test="True">
<td class="fdom_tmembers_member">
<input id="${identifier+'.check'}" name="${identifier+'.check'}" type="checkbox" checked="checked" onchange="onCheckMember(this.id)"/>
</td>
<td class="fdom_tmembers_priority">
- <input id="${identifier + '.priority'}" name="${identifier + '.priority'}" type="text" value="${details['nodes'][node]}" maxlength="3" class="input_priority"/>
+ <input id="${identifier + '.priority'}" name="${identifier + '.priority'}" type="text" value="${fdom_node.getAttribute('priority')}" maxlength="3" class="input_priority"/>
</td>
</py:when>
<!--! 2) No. -->
@@ -209,6 +205,7 @@
</td>
</py:otherwise>
</py:choose>
+ </py:with>
<td class="table_space"></td>
</tr>
</tbody>
diff --git a/luci/templates/fence.html b/luci/templates/fence.html
index 97693c1..e050e33 100644
--- a/luci/templates/fence.html
+++ b/luci/templates/fence.html
@@ -12,6 +12,7 @@
</head>
<body py:with="cluster_data = tmpl_context.cluster_data;
+ cluster_status = tmpl_context.cluster_status;
form_utils = app_globals.form_utils">
<form action="${tg.url(fences_cmd)}" method="post">
@@ -31,37 +32,39 @@
<th class="icon"><img src="${tg.url('/images/global_11x11_black.png')}" alt="shared fences"/></th>
<th class="main_id">Name</th>
<th class="fence_tlist_type">Fence Type</th>
- <th class="fence_tlist_members">Member Nodes</th>
- <th class="fence_tlist_ip">IP Address</th>
+ <th class="fence_tlist_members">Nodes Using</th>
+ <th class="fence_tlist_ip">Hostname</th>
<td class="table_space"></td>
</tr>
</thead>
<tbody>
<!--! List all the fences. -->
- <tr py:for="i, (entity_name, fence_data) in enumerate(cluster_data.fences.iteritems())"
- py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
- py:with="identifier = entity_name;
- local_fence = len(fence_data['nodes']) == 1">
+ <tr py:for="i, fence_data in enumerate(cluster_data.getFenceDevices())"
+ py:with="entity_name = fence_data.getName();
+ identifier = entity_name;
+ local_fence = fence_data.isShared() != True;
+ nodes_using = cluster_data.getNodesUsingFence(entity_name)"
+ py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)">
<td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
<!--! If the fence is shared, display appropriate icon. -->
<td class="icon"><img py:if="not local_fence" src="${tg.url('/images/global_11x11_black.png')}" alt="shared fence"/></td>
<td class="main_id"><a href="${tg.url(base_url + '/' + entity_name)}">${entity_name}</a></td>
- <td class="fence_tlist_type">${fence_data['type']}</td>
+ <td class="fence_tlist_type">${fence_data.getPrettyName()}</td>
<!--! Branch according to whether the fence is local or shared. -->
<py:choose test="local_fence">
<!--! 1) Local. -->
<td py:when="True" class="fence_tlist_members"
- py:with="local_node = fence_data['nodes'].keys()[0]">
- <py:choose test="cluster_data.nodes.get(local_node, {}).get('status', cluster_data.NODE_UNKNOWN)">
- <span py:when="cluster_data.NODE_ACTIVE" class="entity_ok">${local_node}</span>
- <span py:when="cluster_data.NODE_INACTIVE" class="entity_fail">${local_node}</span>
- <span py:when="cluster_data.NODE_UNKNOWN" class="entity_unknown">${local_node}</span>
+ py:with="local_node = len(nodes_using) > 0 and nodes_using[0] or None">
+ <py:choose test="cluster_status.nodes.get(local_node.getName(), {}).get('clustered', '')">
+ <span py:when="'true'" class="entity_ok">${local_node.getName()}</span>
+ <span py:when="'false'" class="entity_fail">${local_node.getName()}</span>
+ <span py:when="''" class="entity_unknown">${local_node.getName()}</span>
</py:choose>
</td>
<!--! 2) Shared. -->
- <td py:otherwise="" class="fence_tlist_members">${len(fence_data['nodes'])}</td>
+ <td py:otherwise="" class="fence_tlist_members">${len(nodes_using)}</td>
</py:choose>
- <td class="fence_tlist_ip">${fence_data['ip']}</td>
+ <td class="fence_tlist_ip">${fence_data.getAttribute('ipaddr')}</td>
<td class="table_space"></td>
</tr>
</tbody>
@@ -76,14 +79,14 @@
<py:when test="None">
<div id="details_header">
- <div id="not_selected" py:choose="len(cluster_data.fences)">
+ <div id="not_selected" py:choose="len(cluster_data.getFenceDevices())">
<py:when test="0">No item to display</py:when>
<py:otherwise>Select an item to view details</py:otherwise>
</div>
</div>
</py:when>
- <py:otherwise py:with="details = cluster_data.fences[name]">
+ <py:otherwise py:with="details = cluster_data.getFenceDeviceByName(name)">
<!--! DETAILS - header section. -->
<div id="details_header">
@@ -91,23 +94,21 @@
<div id="details_header_buttons">
<a href="${tg.url(fences_cmd + '?command=Delete' + '&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
<a href="${tg.url(fences_cmd + '?command=Update' + '&name=' + name)}" id="dh_update" title="update"><span class="hide">update</span></a>
- <!-- TODO: remove? <a href="${tg.url(request.path_qs)}" id="dh_update" title="refresh"><span class="hide">refresh</span></a> -->
</div>
- <span class="details_header_info_label">Type</span> <span class="details_header_info">${details['type']}</span>
+ <span class="details_header_info_label">Type</span> <span class="details_header_info">${details.getPrettyName()}</span>
</div>
<!--! DETAILS - hostname/IP/port section. -->
<div class="details_section">
<ul id="fence_details_list">
- <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>
+ <li><span class="fence_details_label">Hostname</span>${details.getAttribute('ipaddr')}</li>
+ <li><span class="fence_details_label">Port</span>${details.getAttribute('port')}</li>
</ul>
</div>
<!--! DETAILS - nodes section. -->
<div class="details_section">
- <a href="" class="float_button">Manage Nodes(Fix)</a>
+ <a href="" class="float_button">Manage Nodes</a>
<h4>Nodes</h4>
<div class="details_inner">
<table id="fence_tnodes" class="detailstable">
diff --git a/luci/templates/global_res.html b/luci/templates/global_res.html
new file mode 100644
index 0000000..eb0bf51
--- /dev/null
+++ b/luci/templates/global_res.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude">
+
+<xi:include href="master.html" />
+
+<head>
+ <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
+ <title py:content="''">Template: Global resources</title>
+ <link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/css/global_res.css')}" />
+</head>
+
+<body py:with="global_res = app_globals.data.global_res;
+ form_utils = app_globals.form_utils">
+
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
+ <div class="toolbar">
+ <input type="submit" name="${apply_cmds.DELETE}" value="${_('delete')}" class="toolbar_button" id="tb_delete"/>
+ <a href="${tg.url(cmd_url + '/add')}" class="toolbar_button" id="tb_add">add</a>
+ </div>
+
+ <!--! OVERVIEW SECTION. -->
+ <div id="overview">
+ <table id="clusters_tlist" class="maintable">
+ <thead>
+ <tr>
+ <th class="checkbox"></th>
+ <th class="icon"></th>
+ <th class="main_id">Name</th>
+ <th class="globalres_tlist_type">Type</th>
+ <th class="globalres_tlist_main">Main Data Item</th>
+ <td class="table_space"></td>
+ </tr>
+ </thead>
+ <tbody>
+ <!--! List all the global resources. -->
+ <tr py:for="i, (entity_name, globalres_data) in enumerate(global_res.iteritems())"
+ py:attrs="not i%2 and {'class': 'even'} or None"
+ py:with="identifier = form_utils.string2Id(entity_name)">
+ <td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
+ <td class="icon"></td>
+ <td class="main_id">
+ <a href="${tg.url(base_url + '/' + entity_name)}">${entity_name}</a>
+ </td>
+ <td class="globalres_tlist_type">${globalres_data.getTypeString()}</td>
+ <td class="globalres_tlist_main">${globalres_data.getMainData()}</td>
+ <td class="table_space"></td>
+ </tr>
+ <!--! Fix error reported by validator if there is no item at all. -->
+ <tr py:if="len(global_res) == 0"><td colspan="6"></td></tr>
+ </tbody>
+ </table>
+ </div>
+ </form>
+
+ <!--! DETAILS SECTION. -->
+ <div id="details" py:choose="name">
+
+ <py:when test="None">
+ <div id="details_header">
+ <div id="not_selected" py:choose="len(global_res)">
+ <py:when test="0">No item to display</py:when>
+ <py:otherwise>Select an item to view details</py:otherwise>
+ </div>
+ </div>
+ </py:when>
+
+ <py:otherwise py:with="details = global_res[name]">
+
+ <!--! DETAILS - header section. -->
+ <div id="details_header">
+ <h3 py:content="name">Global resource Resource1</h3>
+ <div id="details_header_buttons">
+ <a href="${tg.url(cmd_url + '/apply?' + apply_cmds.DELETE + '&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
+ </div>
+ </div>
+
+ <!--! DETAILS - XY section. -->
+
+ </py:otherwise>
+
+ </div>
+
+</body>
+</html>
diff --git a/luci/templates/homebase.html b/luci/templates/homebase.html
index a93be9d..859690f 100644
--- a/luci/templates/homebase.html
+++ b/luci/templates/homebase.html
@@ -11,11 +11,15 @@
<title>Login Form</title>
</head>
-<body py:with="clusters = app_globals.data.clusters;form_utils = app_globals.form_utils">
+<?python
+ from luci.lib import db_helpers
+?>
+
+<body py:with="clusters = db_helpers.get_cluster_list_full();
+ form_utils = app_globals.form_utils">
<div class="sidebar">
<h2>Take action</h2>
<div class="actions">
- <a href="/homebase/addsystem">Add a System</a><br/>
<a href="/homebase/addexisting">Add an Existing Cluster</a><br/>
<a href="/homebase/managesystems">Manage Systems</a><br/>
<a href="/homebase/adduser">Add a User</a><br/>
@@ -29,12 +33,6 @@
<h3>Remove clusters and systems</h3>
<p>Check storage systems and clusters to remove from the Luci management interface.</p>
<h3>Clusters</h3>
- <h3>Storage Systems</h3>
- </div>
- <div py:if="homebasepage == 'addsystem'">
- <h2>Add a System</h2>
- <p>Enter credentials for a system you wish to add to the cluster management interface.</p>
- <div py:replace="tmpl_context.form()">Input Form</div>
</div>
<div py:if="homebasepage == 'addexisting'">
<h2>Add an Existing Cluster</h2>
@@ -48,7 +46,6 @@
</div>
<div py:if="homebasepage == 'homebasepage'">
<h2>Problems</h2>
- <p>Table with summary of problems goes here</p>
<h2><a href="${tg.url('/cluster')}" class="${('', 'active')[defined('page') and page==page=='clusters']}">Clusters</a></h2>
<div id="overview">
<table id="clusters_tlist" class="maintable">
@@ -68,21 +65,21 @@
py:attrs="not i%2 and {'class': 'even'} or None"
py:with="identifier = form_utils.string2Id(entity_name)">
<!--! Branch according to the status of the cluster. -->
- <py:choose test="cluster_data['status']">
+ <py:choose test="cluster_data['status'].quorate">
<!--! 1) Cluster is OK. -->
- <py:when test="cluster_data.CLUSTER_OK">
+ <py:when test="'true'">
<td class="icon"></td>
<td class="main_id">
<a href="${tg.url(base_url + '/' + entity_name) + '/'}">
<span class="entity_ok">${entity_name}</span>
</a>
</td>
- <td class="cluster_tlist_status">OK</td>
+ <td class="cluster_tlist_status">Quorate</td>
</py:when>
<!--! 2) Cluster is not OK. -->
- <py:when test="cluster_data.CLUSTER_NOT_OK">
+ <py:when test="'false'">
<td class="icon">
- <img src="${tg.url('/images/exclamation.png')}" alt="Cluster is not OK." />
+ <img src="${tg.url('/images/exclamation.png')}" alt="Cluster is not quorate." />
</td>
<td class="main_id">
<a href="${tg.url(base_url + '/' + entity_name)}">
@@ -92,7 +89,7 @@
<td class="cluster_tlist_status">Not OK</td>
</py:when>
<!--! 3) Status of the cluster is unknown. -->
- <py:when test="cluster_data.CLUSTER_UNKNOWN">
+ <py:when test="">
<td class="icon">
<img src="${tg.url('/images/question.png')}" alt="Status of the cluster is unknown." />
</td>
@@ -104,8 +101,8 @@
<td class="cluster_tlist_status">Status uknown</td>
</py:when>
</py:choose>
- <td class="cluster_tlist_votes">${cluster_data['cluster_votes']}</td>
- <td class="cluster_tlist_quorum">${cluster_data['required_quorum']}</td>
+ <td class="cluster_tlist_votes">${cluster_data['status'].votes}</td>
+ <td class="cluster_tlist_quorum">${cluster_data['status'].minQuorum}</td>
<td class="table_space"></td>
</tr>
</tbody>
diff --git a/luci/templates/node.html b/luci/templates/node.html
index 2ea2a26..0e956b5 100644
--- a/luci/templates/node.html
+++ b/luci/templates/node.html
@@ -11,14 +11,20 @@
<title>${title()}</title>
</head>
-<body py:with="cluster_data = tmpl_context.cluster_data;
- form_utils = app_globals.form_utils">
+<?python
+ from luci.lib import db_helpers
+?>
+
+
+<body py:with=" cluster_data = tmpl_context.cluster_data;
+ cluster_status = tmpl_context.cluster_status;
+ form_utils = app_globals.form_utils">
<form action="${tg.url('nodes_cmd')}" method="post">
<div class="sectionblock">
<div id="toolbar">
<input type="submit" name="MultiAction" value="${_('Reboot')}" class="toolbar_button" id="tb_reboot" />
- <input type="submit" name="MultiAction" value="${_('Fence')}" class="toolbar_button" id="tb_fence" />
+ <input type="submit" name="MultiAction" value="${_('Join Cluster')}" class="toolbar_button" id="tb_join" />
<input type="submit" name="MultiAction" value="${_('Leave Cluster')}" class="toolbar_button" id="tb_leave" />
<input type="submit" name="MultiAction" value="${_('Delete')}" class="toolbar_button" id="tb_delete" />
<a href="${tg.url(tmpl_context.cluster_url + base_url + '_cmd/add')}" class="toolbar_button" id="tb_add">Add</a>
@@ -31,45 +37,50 @@
<tr>
<th class="checkbox"></th>
<th class="icon">!</th>
- <th class="main_id">Name</th>
+ <th class="main_id">Node Name</th>
+ <th class="node_tlist_id">Node ID</th>
+ <th class="node_tlist_votes">Votes</th>
<th class="node_tlist_status">Status</th>
- <th class="node_tlist_load">Service Load</th>
- <th class="node_tlist_ip">IP Address</th>
+ <th class="node_tlist_ip">Hostname</th>
<td class="table_space"></td>
</tr>
</thead>
<tbody>
<!--! List all the nodes. -->
- <tr py:for="i, (entity_name, node_data) in enumerate(cluster_data.nodes.iteritems())"
+ <tr py:for="i, (entity_name, node_data) in enumerate(cluster_status.nodes.iteritems())"
py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
- py:with="identifier=entity_name">
+ py:with="identifier=entity_name;nodeobj = cluster_data.getNodeByName(entity_name);nodedbobj = db_helpers.get_cluster_node(tmpl_context.cluster_name, entity_name);">
<td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
<!--! Branch according to the status of the node. -->
- <py:choose test="node_data['status']">
+ <py:choose test="node_data['clustered']">
<!--! 1) Node is active. -->
- <py:when test="cluster_data.NODE_ACTIVE">
+ <py:when test="'true'">
<td class="icon"></td>
<td class="main_id">
<a href="${tmpl_context.cluster_url + entity_name}">
<span class="entity_ok">${entity_name}</span>
</a>
</td>
- <td class="node_tlist_status">${_('OK')}</td>
+ <td class="node_tlist_id">${nodeobj.getID()}</td>
+ <td class="node_tlist_votes">${nodeobj.getVotes()}</td>
+ <td class="node_tlist_status">${_('Cluster Member')}</td>
</py:when>
<!--! 2) Node is inactive. -->
- <py:when test="cluster_data.NODE_INACTIVE">
+ <py:when test="'false'">
<td class="icon">
- <img src="${tg.url('/images/exclamation.png')}" alt="Node has a problem." />
+ <img src="${tg.url('/images/exclamation.png')}" alt="Node is not a cluster member." />
</td>
<td class="main_id">
<a href="${tmpl_context.cluster_url + entity_name}">
<span class="entity_fail">${entity_name}</span>
</a>
</td>
- <td class="node_tlist_status">${node_data.get('msg', _('Uknown problem'))}</td>
+ <td class="node_tlist_id">${nodeobj.getID()}</td>
+ <td class="node_tlist_votes">${nodeobj.getVotes()}</td>
+ <td class="node_tlist_status">${_('Not a cluster member')}</td>
</py:when>
<!--! 3) Status of the node is unknown. -->
- <py:when test="cluster_data.NODE_UNKNOWN">
+ <py:when test="'unknown'">
<td class="icon">
<img src="${tg.url('/images/question.png')}" alt="Status of the node is unknown." />
</td>
@@ -78,11 +89,12 @@
<span class="entity_unknown">${entity_name}</span>
</a>
</td>
- <td class="node_tlist_status">${node_data.get('msg', _('Status unknown'))}</td>
+ <td class="node_tlist_id">${nodeobj.getID()}</td>
+ <td class="node_tlist_votes">${nodeobj.getVotes()}</td>
+ <td class="node_tlist_status">${_('The status of this node is unknown')}</td>
</py:when>
</py:choose>
- <td class="node_tlist_load">${node_data['serviceload']}</td>
- <td class="node_tlist_ip">${node_data['ip']}</td>
+ <td class="node_tlist_ip">${nodedbobj.hostname}</td>
<td class="table_space"></td>
</tr>
</tbody>
@@ -97,29 +109,29 @@
<py:when test="None">
<div id="details_header">
- <div id="not_selected" py:choose="len(cluster_data.nodes)">
+ <div id="not_selected" py:choose="len(cluster_status.nodes)">
<py:when test="0">No item to display</py:when>
<py:otherwise>Select an item to view details</py:otherwise>
</div>
</div>
</py:when>
- <py:otherwise py:with="details = cluster_data.nodes[name]">
+ <py:otherwise py:with="details = cluster_status.nodes[name]">
<!--! DETAILS - header section. -->
<div id="details_header">
<h3 py:content="name">Node A</h3>
<div id="details_header_buttons">
<a href="${tg.url('nodes_cmd?command=Reboot' + '&name=' + name)}" id="dh_reboot" title="reboot"><span class="hide">reboot</span></a>
- <a href="${tg.url('nodes_cmd?command=Fence' + '&name=' + name)}" id="dh_fence" title="fence"><span class="hide">fence</span></a>
+ <a href="${tg.url('nodes_cmd?command=Join' + '&name=' + name)}" id="dh_join" title="join"><span class="hide">join</span></a>
<a href="${tg.url('nodes_cmd?command=Leave' + '&name=' + name)}" id="dh_leave" title="leave cluster"><span class="hide">leave cluster</span></a>
<a href="${tg.url('nodes_cmd?command=Delete' + '&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
</div>
<span class="details_header_info_label">Status</span>
- <span class="details_header_info" py:choose="details['status']">
- <py:when test="cluster_data.NODE_ACTIVE">${_('OK')}</py:when>
- <py:when test="cluster_data.NODE_INACTIVE">${details.get('msg', _('Uknown problem'))}</py:when>
- <py:when test="cluster_data.NODE_UNKNOWN">${details.get('msg', _('Status uknown'))}</py:when>
+ <span class="details_header_info" py:choose="details['clustered']">
+ <py:when test="'true'">${_('Cluster Member')}</py:when>
+ <py:when test="'false'">${_('Not a cluster member')}</py:when>
+ <py:when test="'unknown'">${_('The status of this node is unknown')}</py:when>
</span>
</div>
@@ -130,11 +142,10 @@
<div class="details_inner">
<table id="node_tservices" class="detailstable">
<tbody>
- <!--! List all the services connected with the current failover domain. -->
<!--! 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 = cluster_data.services.has_key(service) and (cluster_data.services[service]['running'])">
+ <tr py:for="service in details['services']" class="grid_row">
+ <py:with vars="running = service.running == 'true'">
<td class="icon">
<img py:if="not running" src="${tg.url('/images/exclamation.png')}" alt="Service has a problem." />
</td>
@@ -147,7 +158,7 @@
</div>
</div>
- <!--! DETAILS - failover domains section. -->
+<!--
<div class="details_section">
<a href="${tg.url('/update_settings?name=' + name)}" class="float_button">Update settings</a>
<h4>Failover Domains</h4>
@@ -173,8 +184,10 @@
</table>
</div>
</div>
+-->
- <!--! DETAILS - fence devices section. -->
+
+ <!--! DETAILS - fence devices section.
<div class="details_section">
<a href="${tg.url('/update_settings?name=' + name)}" class="float_button">Update settings</a>
<h4>Fence Devices</h4>
@@ -200,8 +213,9 @@
</table>
</div>
</div>
+ -->
- <!--! DETAILS - cluster daemons section. -->
+ <!--! DETAILS - cluster daemons section.
<div class="details_section">
<a href="${tg.url('/update_properties?name=' + name)}" class="float_button">Update Properties</a>
<h4>Cluster Daemons</h4>
@@ -216,7 +230,7 @@
</tr>
</thead>
<tbody>
- <tr py:for="daemon in ['cman', 'rgmanager']" class="grid_row"
+ <tr py:for="daemon in ['cman', 'rgmanager', 'ricci', 'modclusterd']" class="grid_row"
py:if="details.has_key(daemon)">
<td class="icon"></td>
<td class="node_tdaemons_name">${daemon}</td>
@@ -228,7 +242,7 @@
</table>
</div>
</div>
-
+ -->
</py:otherwise>
</div>
diff --git a/luci/templates/service.html b/luci/templates/service.html
index 9ac866b..6ca7b84 100644
--- a/luci/templates/service.html
+++ b/luci/templates/service.html
@@ -12,12 +12,14 @@
</head>
<body py:with="cluster_data = tmpl_context.cluster_data;
+ cluster_status = tmpl_context.cluster_status;
form_utils = app_globals.form_utils">
<form action="${tg.url(services_cmd)}" method="post">
<div class="sectionblock">
<div id="toolbar">
- <input type="submit" name="MultiAction" value="${_('Reboot')}" class="toolbar_button" id="tb_reboot" />
+ <input type="submit" name="MultiAction" value="${_('Restart')}" class="toolbar_button" id="tb_reboot" />
+ <input type="submit" name="MultiAction" value="${_('Disable')}" class="toolbar_button" id="tb_disable" />
<input type="submit" name="MultiAction" value="${_('Delete')}" class="toolbar_button" id="tb_delete" />
<a href="${tg.url('/add')}" class="toolbar_button" id="tb_add">Add</a>
</div>
@@ -31,21 +33,21 @@
<th class="icon">!</th>
<th class="main_id">Name</th>
<th class="service_tlist_status">Status</th>
- <th class="service_tlist_enabled">Enabled</th>
+ <th class="service_tlist_enabled">Autostart</th>
<th class="service_tlist_fdom">Failover Domain</th>
<td class="table_space"></td>
</tr>
</thead>
<tbody>
<!--! List all the services. -->
- <tr py:for="i, (entity_name, service_data) in enumerate(cluster_data.services.iteritems())"
- py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
- py:with="identifier=entity_name">
- <td class="checkbox"><input type="checkbox" name="${identifier}"/></td>
+ <tr py:for="(i, service_data) in enumerate(cluster_data.getServices())"
+ py:with="entity_name = service_data.getName()"
+ py:attrs="entity_name==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)">
+ <td class="checkbox"><input type="checkbox" name="${entity_name}"/></td>
<!--! Branch according to the status of the service. -->
- <py:choose test="service_data['running']">
+ <py:choose test="cluster_status.services[entity_name].running">
<!--! 1) Service is running. -->
- <py:when test="True">
+ <py:when test="'true'">
<td class="icon"></td>
<td class="main_id">
<a href="${tg.url(base_url + '/' + entity_name)}">
@@ -53,14 +55,14 @@
</a>
</td>
<td class="service_tlist_status"
- py:with="msg = _('Running on %s') % service_data['node']">
+ py:with="msg = _('Running on %s') % cluster_status.services[entity_name].nodename">
${form_utils.compactString(msg, 18)}
<!--! TODO: Avoid following nasty hack !-->
<py:if test="name == entity_name">${setattr(tmpl_context, 'current_service_status', msg)}</py:if>
</td>
</py:when>
<!--! 2) Service is not running. -->
- <py:when test="False">
+ <py:when test="'false'">
<td class="icon">
<img src="${tg.url('/images/exclamation.png')}" alt="Service has a problem." />
</td>
@@ -70,17 +72,18 @@
</a>
</td>
<td class="service_tlist_status"
- py:with="msg = not service_data['autostart'] and _('Autostart not enabled') or
- (cluster_data.nodes.has_key(service_data['node']) and cluster_data.nodes[service_data['node']].get('msg', _('Unknown problem'))
- or _('Internal error'))">
+ py:with="msg = cluster_status.services[entity_name].failed == 'true' and _('Service failed') or _('Service disabled')">
${form_utils.compactString(msg, 18)}
<!--! TODO: Avoid following nasty hack !-->
<py:if test="name == entity_name">${setattr(tmpl_context, 'current_service_status', msg)}</py:if>
</td>
</py:when>
</py:choose>
- <td class="service_tlist_enabled"><input type="checkbox" disabled="disabled"/></td>
- <td class="service_tlist_fdom">${service_data.get('failover', None) or _('No/default failover domain')}</td>
+ <td class="service_tlist_enabled">
+ <input type="checkbox" disabled="disabled"
+ py:attrs="cluster_status.services[entity_name].autostart and {'checked': 'checked'} or {}"/>
+ </td>
+ <td class="service_tlist_fdom">${service_data.getAttribute('domain') or _('No/default failover domain')}</td>
<td class="table_space"></td>
</tr>
</tbody>
@@ -95,28 +98,29 @@
<py:when test="None">
<div id="details_header">
- <div id="not_selected" py:choose="len(cluster_data.services)">
+ <div id="not_selected" py:choose="len(cluster_data.getServices())">
<py:when test="0">No item to display</py:when>
<py:otherwise>Select an item to view details</py:otherwise>
</div>
</div>
</py:when>
- <py:otherwise py:with="details = cluster_data.services[name]">
+ <py:otherwise py:with="details = cluster_status.services[name]">
<!--! DETAILS - header section. -->
<div id="details_header">
<h3 py:content="name">Service A</h3>
<div id="details_header_buttons">
<a href="${tg.url(services_cmd + '?command=Delete' + '&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
- <a href="${tg.url(services_cmd + '?command=Reboot' + '&name=' + name)}" id="dh_update" title="reboot"><span class="hide">reboot</span></a>
- <!-- TODO: remove? <a href="${tg.url(request.path_qs)}" id="dh_update" title="refresh"><span class="hide">refresh</span></a> -->
+ <a href="${tg.url(services_cmd + '?command=Start' + '&name=' + name)}" id="dh_start" title="stop"><span class="hide">stop</span></a>
+ <a href="${tg.url(services_cmd + '?command=Stop' + '&name=' + name)}" id="dh_stop" title="stop"><span class="hide">stop</span></a>
+ <a href="${tg.url(services_cmd + '?command=Restart' + '&name=' + name)}" id="dh_update" title="restart"><span class="hide">restart</span></a>
</div>
<span class="details_header_info_label">Status</span> <span class="details_header_info">${tmpl_context.current_service_status}</span>
<form style="display: inline; padding-left: 24px; ">
<select style="font-size: small;">
<option value="null">Start on node...</option>
- <py:for each="node in cluster_data.nodes.iterkeys()" py:if="node != details['node']">
+ <py:for each="node in cluster_data.getNodeNames()" py:if="node != details.nodename">
<option value="form_utils.string2Id(node)">${node}</option>
</py:for>
</select>
@@ -139,10 +143,10 @@
</tr>
</thead>
<tbody>
- <tr py:for="resource, res_vals in details['resources'].iteritems()" class="grid_row">
+ <tr py:for="res_vals in cluster_data.getResources()" class="grid_row">
<td class="icon"></td>
- <td class="service_tresources_name">${resource}</td>
- <td class="service_tresources_type">${res_vals[0]}</td>
+ <td class="service_tresources_name">${res_vals.getName()}</td>
+ <td class="service_tresources_type">${res_vals.getResourceType()}</td>
<td class="table_space"></td>
</tr>
</tbody>
14 years, 3 months