[luci] Small configuration tweaks for compatibility with the latest TurboGears releases
by Ryan McCabe
commit 32e388214ba863537ec11fe809f5443ef407f863
Author: Ryan McCabe <ryan(a)chipotle.numb.lan>
Date: Wed Sep 30 14:17:23 2009 -0400
Small configuration tweaks for compatibility with the latest TurboGears releases
COPYING | 340 ++++++++++++++++++++++++++++++++++++++++++++++++
luci/config/app_cfg.py | 1 +
setup.py | 7 +-
3 files changed, 345 insertions(+), 3 deletions(-)
---
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..d60c31a
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/luci/config/app_cfg.py b/luci/config/app_cfg.py
index 34f7f0b..720ee17 100644
--- a/luci/config/app_cfg.py
+++ b/luci/config/app_cfg.py
@@ -54,6 +54,7 @@ base_config.sa_auth.form_plugin = None
# You may optionally define a page where you want users to be redirected to
# on login:
base_config.sa_auth.post_login_url = '/post_login'
+base_config.sa_auth.cookie_secret = '59c05701f'
# You may optionally define a page where you want users to be redirected to
# on logout:
diff --git a/setup.py b/setup.py
index 3aee0f5..0baae9f 100644
--- a/setup.py
+++ b/setup.py
@@ -8,17 +8,18 @@ except ImportError:
setup(
name='luci',
- version='0.1',
- description='',
+ version='0.20.0',
+ description='Web-based cluster administration application',
author='',
author_email='',
- #url='',
+ url='http://sources.redhat.com/cluster/conga',
install_requires=[
"TurboGears2 >= 2.0b7",
"Catwalk >= 2.0.2",
"Babel >=0.9.4",
#can be removed iif use_toscawidgets = False
"toscawidgets >= 0.9.7.1",
+ "tw.dynforms",
"zope.sqlalchemy >= 0.4 ",
"repoze.tm2 >= 1.0a4",
14 years, 7 months
[luci] - Worked on improving menu system.
by Chris Feist
commit 313033ce9620143a87d20a9c7ec3d4822ad65a5d
Author: Chris Feist <cfeist(a)redhat.com>
Date: Tue Sep 29 17:42:05 2009 -0500
- Worked on improving menu system.
- Added left menu items that should list all the clusters
- Not yet fully functional, but using the look from Eve's layouts.
luci/controllers/cluster.py | 3 +-
luci/controllers/root.py | 3 ++
luci/public/css/style.css | 69 ++++++++++++++++++++++--------------------
luci/public/images/conga.png | Bin 0 -> 4171 bytes
luci/templates/about.html | 20 +++++++++---
luci/templates/header.html | 3 +-
luci/templates/master.html | 45 +++++++++++++++++----------
7 files changed, 85 insertions(+), 58 deletions(-)
---
diff --git a/luci/controllers/cluster.py b/luci/controllers/cluster.py
index 587bb82..ce7e7fe 100644
--- a/luci/controllers/cluster.py
+++ b/luci/controllers/cluster.py
@@ -74,13 +74,14 @@ class ClusterController(Subcontroller):
in the module's doc.
"""
+ page = 'clusters'
tmpl_context.title_list.append(TitleStrings.CLUSTERS)
app_url = scheme.APP_PREFIX + u'/'
return dict(base_url=app_url + scheme.CLUSTERS,
cmd_url=app_url + base2CmdCtrl(scheme.CLUSTERS),
- apply_cmds=ClusterApplyCommands)
+ apply_cmds=ClusterApplyCommands,page=page)
@expose()
def lookup(self, name, *args):
diff --git a/luci/controllers/root.py b/luci/controllers/root.py
index 818e534..7584d05 100644
--- a/luci/controllers/root.py
+++ b/luci/controllers/root.py
@@ -94,6 +94,9 @@ class RootController(BaseController):
#flash('current is %s %s [%s]' % (clusterpage, request.method, str(args)), 'error')
from luci.widget_validators.validate_create_cluster_form import validate_create_cluster_form
validate_create_cluster_form(self, **args)
+ if clusterpage == 'clusterlist':
+ redirect ('/clusters/',args)
+
return dict(page='cluster', clusterpage=clusterpage)
@expose('luci.templates.storage')
diff --git a/luci/public/css/style.css b/luci/public/css/style.css
index ca7e45c..115f92d 100644
--- a/luci/public/css/style.css
+++ b/luci/public/css/style.css
@@ -26,37 +26,33 @@ a {
color: #286571;
}
-#header {
- height: 132px;
- margin: 10px 10px 0 10px;
- background: url('../images/logo.jpg') top left no-repeat;
+.leftmenu {
+ position: relative;
+ float:left;
+ width: 136px;
+ background-color: #ffffff;
}
-#header h1 {
- padding: 0;
- margin: 0;
- padding-top: 30px;
- padding-left: 180px;
- color: #fff;
- position: relative;
- font-size: 36px;
+.leftmenu h2 {
+ background-color: #bdd898;
+ border: 1px solid #000000;
+ margin: 0 0 0 0;
}
-#header h1 .subtitle {
- font-size: 60%;
- position: absolute;
- left: 240px;
- top: 70px;
+.leftmenu h3 {
+ margin: 5px 5px 5px 5px;
+ font-weight: normal;
}
-ul#mainmenu {
+ul.mainmenu {
margin: 0;
- padding: 0 10px;
- background: url('../images/menubg.png') top left no-repeat;
- height: 38px;
+ padding: 0 0px;
+ position: relative;
+ float: left;
+ height: 30px;
}
-ul#mainmenu li {
+ul.mainmenu li {
list-style-type: none;
margin: 0;
padding: 0;
@@ -65,7 +61,7 @@ ul#mainmenu li {
float: left;
}
-ul#mainmenu li a {
+ul.mainmenu li a {
color: #fff;
float: left;
height: 31px;
@@ -75,37 +71,44 @@ ul#mainmenu li a {
padding: 0 10px;
font-size: 12px;
text-decoration: none;
- background: url('../images/menu-item-border.png') left top no-repeat;
}
-ul#mainmenu li a:hover, ul#mainmenu li a.active {
+ul.mainmenu li a:hover, ul.mainmenu li a.active {
background: url('../images/menu-item-actibg.png') left top no-repeat;
}
-ul#mainmenu li.first a {
+ul.mainmenu li.first a {
background: none;
}
-ul#mainmenu li.first a:hover, ul#mainmenu li.first a.active {
- background: url('../images/menu-item-actibg-first.png') left top no-repeat;
+ul.mainmenu li.first a:hover, ul.mainmenu li.first a.active {
}
-ul#mainmenu li.loginlogout
+ul.mainmenu li.loginlogout
{
float: right;
- right: 10px;
}
-ul#mainmenu li.loginlogout a:hover
+ul.mainmenu li.loginlogout a:hover
{
- background: url('../images/menu-item-border.png') left top no-repeat;
+ background: url('../images/menu-item-actibg.png') left top no-repeat;
+}
+
+ul.submenu li a {
+ position: relative;
+ float: left;
+ height: 25px;
+ display: block;
+ font-size: 10px;
+ clear: right;
}
#content {
background: #fff url('../images/contentbg.png') left bottom no-repeat;
margin : 0 10px 10px 10px;
- padding: 0 10px;
+ padding: 0 0px;
overflow: hidden;
+ float: left;
}
#content .currentpage {
diff --git a/luci/public/images/conga.png b/luci/public/images/conga.png
new file mode 100644
index 0000000..e349cf4
Binary files /dev/null and b/luci/public/images/conga.png differ
diff --git a/luci/templates/about.html b/luci/templates/about.html
index b1f8e04..0981b4c 100644
--- a/luci/templates/about.html
+++ b/luci/templates/about.html
@@ -1,9 +1,18 @@
-<html>
- <head>
+<!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>Conga User Manual</title>
- </head>
+</head>
<body>
+ <table><tr><td width="800">
<h3>Conga Architecture</h3>
Conga is an agent/server architecture for remote administration of
@@ -34,7 +43,8 @@
which users are allowed to access which systems in the luci server
database. It is possible to import users as a batch operation in a new luci
server, just as it is possible to import systems.
-
+</td></tr>
+</table>
</body>
-
</html>
+
diff --git a/luci/templates/header.html b/luci/templates/header.html
index 931823e..560d112 100644
--- a/luci/templates/header.html
+++ b/luci/templates/header.html
@@ -3,8 +3,7 @@
py:strip="">
<py:def function="header">
<div id="header">
- <h1>
- </h1>
+ <h3></h3>
</div>
</py:def>
</html>
diff --git a/luci/templates/master.html b/luci/templates/master.html
index f23faa2..4762da3 100644
--- a/luci/templates/master.html
+++ b/luci/templates/master.html
@@ -17,23 +17,34 @@
<body py:match="body" py:attrs="select('@*')">
${header()}
- <ul id="mainmenu">
- <li><a href="${tg.url('/homebase')}">homebase</a></li>
- <li><a href="${tg.url('/cluster')}">cluster</a></li>
- <li><a href="${tg.url('/storage')}">storage</a></li>
- <li><a href="${tg.url('/about')}" class="${('', 'active')[defined('page') and page==page=='about']}">About</a></li>
- <py:if test="'cluster_url' in dir(tmpl_context)">
- <li><a href="${tg.url(tmpl_context.cluster_url + '/nodes')}">Nodes</a></li>
- <li><a href="${tg.url(tmpl_context.cluster_url + '/services')}">Services</a></li>
- <li><a href="${tg.url(tmpl_context.cluster_url + '/failovers')}">Failovers</a></li>
- <li><a href="${tg.url(tmpl_context.cluster_url +'/fences')}">Fences</a></li>
- </py:if>
- <span py:if="tg.auth_stack_enabled" py:strip="True">
- <li py:if="not request.identity" id="login" class="loginlogout"><a href="${tg.url('/login')}">Login</a></li>
- <li py:if="request.identity" id="login" class="loginlogout"><a href="${tg.url('/logout_handler')}">Logout</a></li>
- <li py:if="request.identity" id="admin" class="loginlogout"><a href="${tg.url('/admin')}">Admin</a></li>
- </span>
- </ul>
+ <div class="leftmenu">
+ <img src="../../images/conga.png"/>
+ <h2>CLUSTERS</h2>
+ <h3>ClusterOne</h3>
+ <h3>ClusterTwo</h3>
+ </div>
+
+ <div class="menus">
+ <ul class="mainmenu">
+ <li><a href="${tg.url('/homebase')}" class="${('', 'active')[defined('page') and page==page=='homebase']}">homebase</a></li>
+ <li><a href="${tg.url('/cluster')}" class="${('', 'active')[defined('page') and page==page=='clusters']}">cluster</a></li>
+ <li><a href="${tg.url('/about')}" class="${('', 'active')[defined('page') and page==page=='about']}">About</a></li>
+ <span py:if="tg.auth_stack_enabled" py:strip="True">
+ <li py:if="not request.identity" id="login" class="loginlogout"><a href="${tg.url('/login')}">Login</a></li>
+ <li py:if="request.identity" id="login" class="loginlogout"><a href="${tg.url('/logout_handler')}">Logout</a></li>
+ <li py:if="request.identity" id="admin" class="loginlogout"><a href="${tg.url('/admin')}">Admin</a></li>
+ </span>
+ </ul><br/><br/>
+ <py:if test="'cluster_url' in dir(tmpl_context)">
+ <ul class="mainmenu submenu">
+ <li><a href="${tg.url(tmpl_context.cluster_url + '/nodes')}">Nodes</a></li>
+ <li><a href="${tg.url(tmpl_context.cluster_url + '/services')}">Services</a></li>
+ <li><a href="${tg.url(tmpl_context.cluster_url + '/failovers')}">Failovers</a></li>
+ <li><a href="${tg.url(tmpl_context.cluster_url +'/fences')}">Fences</a></li>
+ </ul>
+ <br/><br/>
+ </py:if>
+ </div>
<div id="content">
<py:if test="defined('page')">
<div class="currentpage">
14 years, 7 months
[luci] - Add file missing from the last commit
by Ryan McCabe
commit f341922c3e86cc3da041a298dad57c98fdd27bc0
Author: root <root(a)chipotle.numb.lan>
Date: Fri Sep 25 13:54:18 2009 -0400
- Add file missing from the last commit
- Clean up the database objects a bit
luci/lib/cluster_status.py | 40 ++++
luci/lib/db_helpers.py | 51 +++++-
luci/model/objects.py | 56 +++---
luci/templates/cluster.html | 2 +-
.../validate_create_cluster_form.py | 41 +++--
old_db.py | 191 --------------------
6 files changed, 138 insertions(+), 243 deletions(-)
---
diff --git a/luci/lib/cluster_status.py b/luci/lib/cluster_status.py
new file mode 100644
index 0000000..1a66a3d
--- /dev/null
+++ b/luci/lib/cluster_status.py
@@ -0,0 +1,40 @@
+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')
+
+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'
+
+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 = []
+
+ 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))
diff --git a/luci/lib/db_helpers.py b/luci/lib/db_helpers.py
index 8db6069..9f1174c 100644
--- a/luci/lib/db_helpers.py
+++ b/luci/lib/db_helpers.py
@@ -1,10 +1,11 @@
"""Database Helpers used in luci."""
-from luci.model import metadata, DBSession, NoResultFound
+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
-from luci.lib.ricci_queries import getClusterConf
+import luci.lib.ricci_queries as rq
def get_cluster_db_obj(cluster_name):
db_obj = None
@@ -17,7 +18,7 @@ def get_cluster_db_obj(cluster_name):
pass
return db_obj
-def get_conf_for_cluster(cluster_name):
+def get_agent_for_cluster(cluster_name):
db_obj = get_cluster_db_obj(cluster_name)
if db_obj is None:
return None
@@ -30,15 +31,40 @@ def get_conf_for_cluster(cluster_name):
port = node_obj.port
try:
rc = RicciCommunicator(host, port=port)
- conf = getClusterConf(rc)
- if conf is not None:
- return conf
+ if rc is not None:
+ return rc
except Exception, e:
- continue
+ # log this
+ pass
+ return None
+
+def get_model_for_cluster(cluster_name, rc=None):
+ if rc is None:
+ rc = get_agent_for_cluster(cluster_name)
+ if rc is None:
+ return None
+
+ try:
+ from luci.lib.ClusterConf.ModelBuilder import ModelBuilder
+ conf = rq.getClusterConf(rc)
+ if conf is not None:
+ model = ModelBuilder(None, conf, rc.os())
+ return model
+ except Exception, e:
+ # log this
+ pass
# Couldn't get the conf from any nodes
return None
+def get_cluster_status(rc):
+ try:
+ doc = rq.getClusterStatusBatch(rc)
+ except Exception, e:
+ # log this
+ return None
+ return doc
+
def get_cluster_list():
db_obj = None
try:
@@ -50,3 +76,14 @@ def get_cluster_list():
return None
return db_obj
+
+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
+
+ return False
diff --git a/luci/model/objects.py b/luci/model/objects.py
index 5bfb57c..ec520fe 100644
--- a/luci/model/objects.py
+++ b/luci/model/objects.py
@@ -16,6 +16,30 @@ cluster_tasks_table = Table('cluster_tasks', metadata,
Column('cluster_id', Integer, ForeignKey('clusters.cluster_id'), primary_key=True),
Column('task_id', Integer, ForeignKey('tasks.task_id'), primary_key=True))
+node_tasks_table = Table('node_tasks', metadata,
+ Column('node_id', Integer, ForeignKey('nodes.node_id'), primary_key=True),
+ Column('task_id', Integer, ForeignKey('tasks.task_id'), primary_key=True))
+
+class Task(DeclarativeBase):
+ __tablename__ = 'tasks'
+
+ task_id = Column(Integer, autoincrement=True, primary_key=True)
+ batch_id = Column(Integer)
+ batch_status = Column(Integer)
+ started = Column(DateTime, default=datetime.now)
+ last_status = Column(DateTime, default=datetime.now)
+ finished = Column(DateTime)
+ description = Column(Unicode(255))
+ status_msg = Column(Unicode(255))
+ redirect_url = Column(Unicode(512))
+
+ def __repr__(self):
+ return '<Task: id=%d batchid=%d>' % (self.task_id, self.batch_id)
+
+ def __unicode__(self):
+ return self.task_id
+
+
class Node(DeclarativeBase):
__tablename__ = 'nodes'
@@ -29,6 +53,7 @@ class Node(DeclarativeBase):
port = Column(Integer)
#{ Relations
+ tasks = relation(Task, secondary=node_tasks_table, backref="node")
#{ Special methods
@@ -40,49 +65,26 @@ class Node(DeclarativeBase):
#}
-class Task(DeclarativeBase):
- __tablename__ = 'tasks'
-
- task_id = Column(Integer, autoincrement=True, primary_key=True)
- batch_id = Column(Integer)
- batch_status = Column(Integer)
- started = Column(DateTime, default=datetime.now)
- last_status = Column(DateTime, default=datetime.now)
- finished = Column(DateTime)
- description = Column(Unicode(255))
- status_msg = Column(Unicode(255))
- system = Column(Integer)
- cluster = Column(Integer)
-
- def __repr__(self):
- return '<Task: id=%d batchid=%d>' % (self.task_id, self.batch_id)
-
- def __unicode__(self):
- return self.task_id
-
class Cluster(DeclarativeBase):
__tablename__ = 'clusters'
#{ Columns
cluster_id = Column(Integer, autoincrement=True, primary_key=True)
-
name = Column(Unicode(16), unique=True, nullable=False)
-
display_name = Column(Unicode(255))
-
created = Column(DateTime, default=datetime.now)
#{ Relations
- nodes = relation(Node, secondary=cluster_nodes_table, backref="clusters")
- tasks = relation(Task, secondary=cluster_tasks_table, backref="clusters")
+ nodes = relation(Node, secondary=cluster_nodes_table, backref="cluster")
+ tasks = relation(Task, secondary=cluster_tasks_table, backref="cluster")
#{ Special methods
def __repr__(self):
- return '<Cluster: display name="%s">' % (
- self.display_name)
+ return '<Cluster: id="%d" name="%s" display_name="%s">' % (
+ self.cluster_id, self.name, self.display_name)
def __unicode__(self):
return self.display_name or self.name
diff --git a/luci/templates/cluster.html b/luci/templates/cluster.html
index 3236ac3..5c75249 100644
--- a/luci/templates/cluster.html
+++ b/luci/templates/cluster.html
@@ -16,7 +16,7 @@
<tr>
<td valign="top">
<div class="sidebar">
- <a href="${tg.url('/cluster/clusterlist')}">Cluster List</a><br/>
+ <a href="${tg.url('/clusters/')}">Cluster List</a><br/>
<a href="${tg.url('/cluster/createcluster')}">Create a New Cluster</a><br/>
<a href="${tg.url('/cluster/configure')}">Configure</a><br/>
</div>
diff --git a/luci/widget_validators/validate_create_cluster_form.py b/luci/widget_validators/validate_create_cluster_form.py
index 496c95c..7142861 100644
--- a/luci/widget_validators/validate_create_cluster_form.py
+++ b/luci/widget_validators/validate_create_cluster_form.py
@@ -76,12 +76,13 @@ def validate_create_cluster_form(self, **kw):
cluster_db_obj = Cluster(name=cluster_name,
display_name=cluster_name)
+
node_list = []
- node_db_obj = []
- task_db_obj = []
+ node_db_obj = {}
+ task_db_obj = {}
for node in nodes:
try:
- rc = RicciCommunicator(node[0], enforce_trust=False)
+ rc = RicciCommunicator(node[0], port=int(node[2]), enforce_trust=False)
rc.trust()
rc.auth(node[1])
if not rc.authed():
@@ -97,16 +98,18 @@ def validate_create_cluster_form(self, **kw):
break
node_list.append(node[0])
- node_db_obj.append(Node(node_name=node[0],
- display_name=node[0],
- hostname=node[0],
- ipaddr=node[0],
- port=node[2]))
+ node_db_obj[node[0]] = Node(node_name=node[0],
+ display_name=node[0],
+ hostname=node[0],
+ ipaddr=node[0],
+ port=node[2])
+ DBSession.add(node_db_obj[node[0]])
except Exception, e:
errors.append(str(e))
if len(errors) > 0:
flash(_('The following errors occurred: %s') % str(errors), 'error')
+ DBSession.rollback()
return
ret = send_batch_to_hosts(node_list, 10, rq.create_cluster,
@@ -118,19 +121,23 @@ def validate_create_cluster_form(self, **kw):
if ret[i].has_key('error'):
errors.append(_('Unable to connect to the ricci agent on node %s: %s') % (i, ret[i]['err_msg']))
task_id = ret[i]['batch_result'][0]
- task_db_obj.append(
- Task(batch_id=task_id,
- batch_status=0,
- status_msg=_('Creating node "%s" for cluster "%s"') % (i, cluster_name)))
+ task_obj = Task(batch_id=task_id,
+ batch_status=0,
+ redirect_url='/clusters/%s/' % cluster_name,
+ status_msg=_('Creating node "%s" for cluster "%s"') % (i, cluster_name))
+ DBSession.add(task_obj)
+ task_db_obj[i] = task_obj
try:
- for i in node_db_obj:
- DBSession.add(i)
- cluster_db_obj.nodes = node_db_obj
- cluster_db_obj.tasks = task_db_obj
+ cluster_db_obj.nodes = node_db_obj.values()
+ cluster_db_obj.tasks = task_db_obj.values()
+ for i in node_db_obj.iterkeys():
+ node_db_obj[i].tasks = [ task_db_obj[i] ]
DBSession.add(cluster_db_obj)
except Exception, e:
- flash(_('An error occurred while updating the luci database'))
+ DBSession.rollback()
+ flash(_('An error occurred while updating the luci database: %s') % str(e), 'error')
return
+ flash(_('Creating the cluster "%s"... Please wait...') % cluster_name, 'info')
redirect("/clusters/%s/" % cluster_name)
14 years, 7 months
[luci] Remove accidentally committed pyc files
by Ryan McCabe
commit f9551af38747c89cdbc6ef3b853fde394e59e5e3
Author: Ryan McCabe <ryan(a)chipotle.numb.lan>
Date: Fri Sep 25 08:51:56 2009 -0400
Remove accidentally committed pyc files
luci/widget_validators/__init__.pyc | Bin 140 -> 0 bytes
.../validate_create_cluster_form.pyc | Bin 4394 -> 0 bytes
2 files changed, 0 insertions(+), 0 deletions(-)
14 years, 7 months
[luci] updates to the database model and some db helper bits
by Ryan McCabe
commit 75979b4044b911468c02efafb7a070d9ede28586
Author: Ryan McCabe <ryan(a)chipotle.numb.lan>
Date: Fri Sep 25 08:51:00 2009 -0400
updates to the database model and some db helper bits
luci/config/app_cfg.py | 2 +-
luci/controllers/cluster.py | 6 +-
luci/controllers/root.py | 42 +++---
luci/lib/app_globals.py | 1 -
luci/lib/db_helpers.py | 52 +++++++
luci/model/model.template | 1 -
luci/model/objects.py | 69 ++++++----
luci/templates/create.html | 147 --------------------
luci/widget_validators/__init__.pyc | Bin 0 -> 140 bytes
.../validate_create_cluster_form.py | 136 ++++++++++++++++++
.../validate_create_cluster_form.pyc | Bin 0 -> 4394 bytes
luci/widgets/add_existing_form.py | 6 +-
luci/widgets/add_user_form.py | 1 -
luci/widgets/create_cluster_form.py | 37 +++---
14 files changed, 277 insertions(+), 223 deletions(-)
---
diff --git a/luci/config/app_cfg.py b/luci/config/app_cfg.py
index 008d069..34f7f0b 100644
--- a/luci/config/app_cfg.py
+++ b/luci/config/app_cfg.py
@@ -17,7 +17,7 @@ from tg.configuration import AppConfig
import luci
from luci import model
-from luci.lib import app_globals, helpers #, ricci_communicator, ricci_defines, ricci_helpers
+from luci.lib import app_globals, helpers
base_config = AppConfig()
base_config.renderers = []
diff --git a/luci/controllers/cluster.py b/luci/controllers/cluster.py
index ca6f321..587bb82 100644
--- a/luci/controllers/cluster.py
+++ b/luci/controllers/cluster.py
@@ -39,7 +39,7 @@
"""
-from tg import flash, request, redirect, expose, tmpl_context, app_globals
+from tg import flash, request, redirect, expose, tmpl_context, app_globals, validate
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from luci.lib.base import Subcontroller, SubcontrollerApplyMixin
@@ -47,6 +47,8 @@ from luci.controllers.decorators import *
from luci.lib.strings import TitleStrings
from luci.lib.helpers import urlList2String
+from luci.widgets.create_cluster_form import create_cluster_form
+
__all__ = ['ClusterController', 'ClusterCmdController']
@@ -80,7 +82,6 @@ class ClusterController(Subcontroller):
cmd_url=app_url + base2CmdCtrl(scheme.CLUSTERS),
apply_cmds=ClusterApplyCommands)
-
@expose()
def lookup(self, name, *args):
"""Dynamic dispatching to subcontrollers according to cluster's name.
@@ -260,4 +261,3 @@ class ClusterCmdController(Subcontroller, SubcontrollerApplyMixin):
base_url_str = urlList2String(scheme.APP_PREFIX, self.base_name)
return dict(use_referrer=False, redir_target=base_url_str)
-
diff --git a/luci/controllers/root.py b/luci/controllers/root.py
index ac5df2f..818e534 100644
--- a/luci/controllers/root.py
+++ b/luci/controllers/root.py
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
"""Main Controller"""
-
-from tg import expose, flash, require, url, request, redirect, app_globals, tmpl_context
+from tg import expose, flash, require, url, request, redirect, app_globals, tmpl_context, validate
from tg import tmpl_context
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from catwalk.tg2 import Catwalk
@@ -60,9 +59,8 @@ class RootController(BaseController):
admin = Catwalk(model, DBSession)
error = ErrorController()
-
# METHODS OF THE ROOT CONTROLLER
-
+
@expose('luci.templates.index')
def index(self):
"""Handle the front-page."""
@@ -75,23 +73,28 @@ class RootController(BaseController):
@expose('luci.templates.homebase')
def homebase(self, homebasepage='homebasepage', **args):
- if homebasepage == 'addsystem':
- tmpl_context.form = create_add_system_form
- elif homebasepage == 'addexisting':
- tmpl_context.form = create_add_existing_form
- elif homebasepage == 'adduser':
- tmpl_context.form = create_add_user_form
- elif homebasepage == 'managesystems':
- tmpl_context.form = create_manage_systems_form
-
- return dict(page='homebase',homebasepage=homebasepage,args=args)
+ if homebasepage == 'addsystem':
+ tmpl_context.form = create_add_system_form
+ elif homebasepage == 'addexisting':
+ tmpl_context.form = create_add_existing_form
+ if request.method == 'POST':
+ flash('psted')
+ elif homebasepage == 'adduser':
+ tmpl_context.form = create_add_user_form
+ elif homebasepage == 'managesystems':
+ tmpl_context.form = create_manage_systems_form
+
+ return dict(page='homebase',homebasepage=homebasepage,args=args)
@expose('luci.templates.cluster')
- def cluster(self,clusterpage='clusterlist', **args):
- if clusterpage == 'createcluster':
- tmpl_context.form = create_cluster_form
-
- return dict(page='cluster', clusterpage=clusterpage)
+ def cluster(self, clusterpage='clusterlist', **args):
+ if clusterpage == 'createcluster':
+ tmpl_context.form = create_cluster_form
+ if request.method == 'POST':
+ #flash('current is %s %s [%s]' % (clusterpage, request.method, str(args)), 'error')
+ from luci.widget_validators.validate_create_cluster_form import validate_create_cluster_form
+ validate_create_cluster_form(self, **args)
+ return dict(page='cluster', clusterpage=clusterpage)
@expose('luci.templates.storage')
def storage(self,storagepage='systemlist'):
@@ -148,4 +151,3 @@ class RootController(BaseController):
"""
flash(_('We hope to see you soon!'))
redirect(came_from)
-
diff --git a/luci/lib/app_globals.py b/luci/lib/app_globals.py
index 7c76c25..0975673 100644
--- a/luci/lib/app_globals.py
+++ b/luci/lib/app_globals.py
@@ -22,4 +22,3 @@ class Globals(object):
self.form_utils = FormUtils
self.scheme = LuciScheme
self.data = ClusterData()
-
diff --git a/luci/lib/db_helpers.py b/luci/lib/db_helpers.py
new file mode 100644
index 0000000..8db6069
--- /dev/null
+++ b/luci/lib/db_helpers.py
@@ -0,0 +1,52 @@
+"""Database Helpers used in luci."""
+
+from luci.model import metadata, DBSession, NoResultFound
+from luci.model.objects import Node,Cluster,Task
+
+from luci.lib.ricci_communicator import RicciCommunicator
+from luci.lib.ricci_queries import getClusterConf
+
+def get_cluster_db_obj(cluster_name):
+ db_obj = None
+ try:
+ db_obj = DBSession.query(Cluster).filter_by(name=cluster_name).one()
+ except NoResultFound, e:
+ pass
+ except Exception, e:
+ # log this
+ pass
+ return db_obj
+
+def get_conf_for_cluster(cluster_name):
+ db_obj = get_cluster_db_obj(cluster_name)
+ if db_obj is None:
+ return None
+ node_list = db_obj.nodes
+ if len(node_list) < 1:
+ return None
+
+ for node_obj in node_list:
+ host = node_obj.hostname
+ port = node_obj.port
+ try:
+ rc = RicciCommunicator(host, port=port)
+ conf = getClusterConf(rc)
+ if conf is not None:
+ return conf
+ except Exception, e:
+ continue
+
+ # Couldn't get the conf from any nodes
+ return None
+
+def get_cluster_list():
+ db_obj = None
+ try:
+ db_obj = DBSession.query(Cluster).all()
+ except NoResultFound, e:
+ return None
+ except Exception, e:
+ # log this
+ return None
+
+ return db_obj
diff --git a/luci/model/model.template b/luci/model/model.template
index eae98da..d6e2942 100644
--- a/luci/model/model.template
+++ b/luci/model/model.template
@@ -9,7 +9,6 @@ from sqlalchemy.types import Integer, Unicode
from luci.model import DeclarativeBase, metadata, DBSession
-
class SampleModel(DeclarativeBase):
__tablename__ = 'sample_model'
diff --git a/luci/model/objects.py b/luci/model/objects.py
index bf6a627..5bfb57c 100644
--- a/luci/model/objects.py
+++ b/luci/model/objects.py
@@ -3,38 +3,62 @@ from datetime import datetime
import sys
from sqlalchemy import Table, ForeignKey, Column
-from sqlalchemy.types import Unicode, Integer, DateTime
+from sqlalchemy.types import Unicode, Integer, DateTime, String
from sqlalchemy.orm import relation, synonym
from luci.model import DeclarativeBase, metadata, DBSession
+cluster_nodes_table = Table('cluster_nodes', metadata,
+ Column('cluster_id', Integer, ForeignKey('clusters.cluster_id'), primary_key=True),
+ Column('node_id', Integer, ForeignKey('nodes.node_id'), primary_key=True))
+
+cluster_tasks_table = Table('cluster_tasks', metadata,
+ Column('cluster_id', Integer, ForeignKey('clusters.cluster_id'), primary_key=True),
+ Column('task_id', Integer, ForeignKey('tasks.task_id'), primary_key=True))
+
class Node(DeclarativeBase):
- __tablename__ = 'system'
+ __tablename__ = 'nodes'
#{ Columns
- group_id = Column(Integer, autoincrement=True, primary_key=True)
-
- group_name = Column(Unicode(16), unique=True, nullable=False)
-
+ node_id = Column(Integer, autoincrement=True, primary_key=True)
+ node_name = Column(Unicode(16), unique=True, nullable=False)
display_name = Column(Unicode(255))
hostname = Column(Unicode(255))
- cluster = Column(Integer)
-
- created = Column(DateTime, default=datetime.now)
+ ipaddr = Column(String(16))
+ port = Column(Integer)
#{ Relations
#{ Special methods
def __repr__(self):
- return '<Group: name=%s>' % self.group_name
+ return '<Node: name=%s>' % self.node_name
def __unicode__(self):
- return self.group_name
+ return self.node_name
#}
+class Task(DeclarativeBase):
+ __tablename__ = 'tasks'
+
+ task_id = Column(Integer, autoincrement=True, primary_key=True)
+ batch_id = Column(Integer)
+ batch_status = Column(Integer)
+ started = Column(DateTime, default=datetime.now)
+ last_status = Column(DateTime, default=datetime.now)
+ finished = Column(DateTime)
+ description = Column(Unicode(255))
+ status_msg = Column(Unicode(255))
+ system = Column(Integer)
+ cluster = Column(Integer)
+
+ def __repr__(self):
+ return '<Task: id=%d batchid=%d>' % (self.task_id, self.batch_id)
+
+ def __unicode__(self):
+ return self.task_id
class Cluster(DeclarativeBase):
__tablename__ = 'clusters'
@@ -48,6 +72,11 @@ class Cluster(DeclarativeBase):
display_name = Column(Unicode(255))
created = Column(DateTime, default=datetime.now)
+
+ #{ Relations
+
+ nodes = relation(Node, secondary=cluster_nodes_table, backref="clusters")
+ tasks = relation(Task, secondary=cluster_tasks_table, backref="clusters")
#{ Special methods
@@ -57,21 +86,3 @@ class Cluster(DeclarativeBase):
def __unicode__(self):
return self.display_name or self.name
-
-class Task(DeclarativeBase):
- __tablename__ = 'tasks'
-
- task_id = Column(Integer, autoincrement=True, primary_key=True)
- batch_id = Column(Integer)
- batch_status = Column(Integer)
- started = Column(DateTime, default=datetime.now)
- ended = Column(DateTime)
- description = Column(Unicode(255))
- status_msg = Column(Unicode(255))
- system = Column(Integer)
-
- def __repr__(self):
- return '<Task: id=%d>' % self.task_id
-
- def __unicode__(self):
- return self.task_id
diff --git a/luci/widget_validators/__init__.py b/luci/widget_validators/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/luci/widget_validators/__init__.pyc b/luci/widget_validators/__init__.pyc
new file mode 100644
index 0000000..52cb4e2
Binary files /dev/null and b/luci/widget_validators/__init__.pyc differ
diff --git a/luci/widget_validators/validate_create_cluster_form.py b/luci/widget_validators/validate_create_cluster_form.py
new file mode 100644
index 0000000..496c95c
--- /dev/null
+++ b/luci/widget_validators/validate_create_cluster_form.py
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+
+from tg import flash, request, redirect, expose, tmpl_context, app_globals, validate
+from pylons.i18n import ugettext as _, lazy_ugettext as l_
+
+from luci.lib.base import Subcontroller
+from luci.controllers.decorators import *
+from luci.lib.strings import TitleStrings
+from luci.lib.helpers import urlList2String
+
+from luci.model import metadata, DBSession
+from luci.model.objects import Node,Cluster,Task
+
+from luci.lib.ricci_helpers import send_batch_to_hosts
+from luci.lib.ricci_communicator import RicciCommunicator
+import luci.lib.ricci_queries as rq
+
+from luci.widgets.create_cluster_form import create_cluster_form
+
+# Imports into module's namespace.
+scheme = app_globals.scheme
+data = app_globals.data
+
+def validate_create_cluster_form(self, **kw):
+ errors = [];
+
+ cluster_name = kw.get('cluster_name')
+ if not cluster_name or len(cluster_name) < 1:
+ flash(_('No cluster name was given'), 'error')
+ return
+
+ if len(cluster_name) > 15:
+ flash(_('Cluster names must be less than 16 characters long'), 'error')
+ return
+
+ try:
+ ret = int(DBSession.query(Cluster).filter_by(name=cluster_name).count())
+ if ret != 0:
+ flash(_('Luci is already managing a cluster named "%s"') % cluster_name, 'error')
+ return
+ except Exception, e:
+ pass
+
+ create_opt_args = kw.get('opt')
+ if create_opt_args is None:
+ create_opts = []
+ elif isinstance(create_opt_args, list):
+ create_opts = create_opt_args
+ else:
+ create_opts = [ create_opt_args ]
+
+ enable_storage = 'shared_storage' in create_opts
+ reboot_nodes = 'reboot_nodes' in create_opts
+ download_pkgs = kw.get('download_pkgs') == 'download'
+
+ nodes = []
+ cur = 0
+ while True:
+ cur_host = kw.get('host_list.grow-%d.hostname' % cur)
+ cur_passwd = kw.get('host_list.grow-%d.password' % cur)
+ cur_port = kw.get('host_list.grow-%d.port' % cur)
+ cur += 1
+ if cur_port is None:
+ break
+ if not cur_host:
+ continue
+ if not cur_passwd:
+ errors.append(_('No password was provided for node "%s"') % cur_host)
+ continue
+
+ nodes.append([ cur_host, cur_passwd, cur_port ])
+
+ if len(errors) > 0:
+ flash(str(errors), 'error')
+ return
+
+ cluster_db_obj = Cluster(name=cluster_name,
+ display_name=cluster_name)
+ node_list = []
+ node_db_obj = []
+ task_db_obj = []
+ for node in nodes:
+ try:
+ rc = RicciCommunicator(node[0], enforce_trust=False)
+ rc.trust()
+ rc.auth(node[1])
+ if not rc.authed():
+ errors.append('Authentication to node %s failed' % node[0])
+ break
+ else:
+ rc = RicciCommunicator(node[0])
+
+ cur_cluster_name = rc.cluster_info()[0]
+ if cur_cluster_name:
+ errors.append('%s is already a member of a cluster %s' \
+ % (node[0], cur_cluster_name))
+ break
+
+ node_list.append(node[0])
+ node_db_obj.append(Node(node_name=node[0],
+ display_name=node[0],
+ hostname=node[0],
+ ipaddr=node[0],
+ port=node[2]))
+ except Exception, e:
+ errors.append(str(e))
+
+ if len(errors) > 0:
+ flash(_('The following errors occurred: %s') % str(errors), 'error')
+ return
+
+ ret = send_batch_to_hosts(node_list, 10, rq.create_cluster,
+ 'rhel5', cluster_name, cluster_name,
+ node_list, True, True, enable_storage, False,
+ download_pkgs, None, reboot_nodes)
+
+ for i in ret.iterkeys():
+ if ret[i].has_key('error'):
+ errors.append(_('Unable to connect to the ricci agent on node %s: %s') % (i, ret[i]['err_msg']))
+ task_id = ret[i]['batch_result'][0]
+ task_db_obj.append(
+ Task(batch_id=task_id,
+ batch_status=0,
+ status_msg=_('Creating node "%s" for cluster "%s"') % (i, cluster_name)))
+
+ try:
+ for i in node_db_obj:
+ DBSession.add(i)
+ cluster_db_obj.nodes = node_db_obj
+ cluster_db_obj.tasks = task_db_obj
+ DBSession.add(cluster_db_obj)
+ except Exception, e:
+ flash(_('An error occurred while updating the luci database'))
+ return
+
+ redirect("/clusters/%s/" % cluster_name)
diff --git a/luci/widget_validators/validate_create_cluster_form.pyc b/luci/widget_validators/validate_create_cluster_form.pyc
new file mode 100644
index 0000000..acf69d0
Binary files /dev/null and b/luci/widget_validators/validate_create_cluster_form.pyc differ
diff --git a/luci/widgets/add_existing_form.py b/luci/widgets/add_existing_form.py
index 1ade914..d5700ed 100644
--- a/luci/widgets/add_existing_form.py
+++ b/luci/widgets/add_existing_form.py
@@ -1,10 +1,10 @@
from tw.api import WidgetsList
-from tw.forms import TableForm, TextField, Label
+from tw.forms import TableForm, TextField, Label, PasswordField
class AddExistingForm(TableForm):
class fields(WidgetsList):
system_hostname = TextField()
- root_password = TextField()
+ root_password = PasswordField()
-create_add_existing_form = AddExistingForm("create_add_existing_form")
+create_add_existing_form = AddExistingForm("create_add_existing_form", action='addexisting')
diff --git a/luci/widgets/add_user_form.py b/luci/widgets/add_user_form.py
index bbc5a94..376581e 100644
--- a/luci/widgets/add_user_form.py
+++ b/luci/widgets/add_user_form.py
@@ -8,4 +8,3 @@ class AddUserForm(TableForm):
confirm_password = PasswordField()
create_add_user_form = AddUserForm("create_add_user_form")
-
diff --git a/luci/widgets/create_cluster_form.py b/luci/widgets/create_cluster_form.py
index d17cd27..91275c7 100644
--- a/luci/widgets/create_cluster_form.py
+++ b/luci/widgets/create_cluster_form.py
@@ -1,21 +1,24 @@
from tw.api import WidgetsList
-from tw.forms import TableForm, TextField, PasswordField, RadioButtonList, RadioButton, CheckBox, CheckBoxTable
-import tw.dynforms as twd
+import tw.forms as twf
+from tw.forms import TableForm, TextField, PasswordField, RadioButtonList, RadioButton, CheckBox, CheckBoxTable, Label, Spacer
+import tw.dynforms as twd
-class HostList(twd.GrowingTableFieldSet):
- class children(WidgetsList):
- node_hostname = TextField()
- root_password = PasswordField()
+__all__ = ["CreateClusterForm", "create_cluster_form"]
-class CreateClusterForm(TableForm):
- class fields(WidgetsList):
- cluster_name = TextField()
- host_list = HostList(suppress_label = True)
- download_packages = RadioButtonList(options=["Download Packages","Use locally installed packages"], suppress_label = True, default = "Download Packages");
-# use_locally_installed_packages = RadioButton();
- cb = CheckBoxTable(options=["Enable Shared Storage Support", "Reboot nodes before joining cluster", "Check if node passwords are identical"], suppress_label = True, default = "Enable Shared Storage Support")
-# enable_shared_storage_support = CheckBox()
-# reboot_nodes_before_joining_cluster = CheckBox()
-# check_if_node_passwords_are_identical = CheckBox()
-create_cluster_form = CreateClusterForm("create_cluster_form")
+class HostForm(twd.GrowingTableFieldSet):
+ children = [
+ TextField('hostname'),
+ PasswordField('password'),
+ TextField('port', size=5, default='11111',validator=twf.validators.Int(min=1, max=65535))
+ ]
+class CreateClusterForm(twd.CustomisedForm):
+ children = [
+ TextField('cluster_name', validator=twf.validators.NotEmpty),
+ HostForm('host_list'),
+ RadioButtonList('download_pkgs', options=[('download',"Download Packages"),('local',"Use locally installed packages")], suppress_label = True, default = 'download'),
+
+ CheckBoxTable('opt', options=[('shared_storage', "Enable Shared Storage Support"), ('reboot_nodes', "Reboot nodes before joining cluster"), ('same_passwd', "Check if node passwords are identical")], suppress_label = True, default = ("Enable Shared Storage Support"))
+ ]
+
+create_cluster_form = CreateClusterForm('create_cluster_form', action='createcluster')
14 years, 7 months
[luci] Cluster listing added, minor changes, new icons for "create" action.
by Jan Pokorný
commit 818cad1649a920668124b5ec956da164e674c9bc
Author: Jan Pokorny <jpokorny(a)redhat.com>
Date: Tue Sep 22 18:42:13 2009 +0200
Cluster listing added, minor changes, new icons for "create" action.
luci/controllers/cluster.py | 186 +++++++++++++++++++++--------
luci/controllers/cluster_part/failover.py | 65 +++++-----
luci/controllers/cluster_part/fence.py | 45 ++++----
luci/controllers/cluster_part/node.py | 51 ++++-----
luci/controllers/cluster_part/service.py | 64 +++++-----
luci/controllers/decorators.py | 66 ++---------
luci/controllers/root.py | 7 +-
luci/controllers/scheme.py | 35 +++---
luci/lib/base.py | 49 +++++++-
luci/lib/demo_data.py | 61 ++++++++--
luci/lib/strings.py | 16 ++-
luci/public/css/cluster.css | 29 +++++
luci/public/css/shared.css | 15 ++-
luci/public/css/style.css | 1 +
luci/public/images/create-grey.png | Bin 0 -> 545 bytes
luci/public/images/create-white.png | Bin 0 -> 377 bytes
luci/templates/cluster_list.html | 104 ++++++++++++++++
luci/templates/cluster_part/failover.html | 2 +-
luci/templates/cluster_part/fence.html | 6 +-
luci/templates/cluster_part/node.html | 2 +-
luci/templates/cluster_part/service.html | 18 ++-
luci/templates/title.html | 7 +-
22 files changed, 551 insertions(+), 278 deletions(-)
---
diff --git a/luci/controllers/cluster.py b/luci/controllers/cluster.py
index 8785c0d..ca6f321 100644
--- a/luci/controllers/cluster.py
+++ b/luci/controllers/cluster.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-"""Controller API for `clusters' part of the Luci.
+"""Controller API for `clusters' part.
Functionality description:
@@ -8,10 +8,10 @@
'clusters' identifier of the root controller and subcontroller handling
related commands (ClusterCmdController) through 'clusters_cmd' identifier.
- Server is accessible via http://example.com. Clusters in the system are
- ClusterOne, ClusterTwo and ClusterThree.
+ Luci application is accessible via http://example.com. Clusters
+ in the system are ClusterOne, ClusterTwo and ClusterThree.
- Accessing following URLs has following consequences:
+ Accessing following URLs has following consequences (some cases omitted):
1) <http://example.com/clusters> OR <http://example.com/clusters/>
- displaying list of the clusters with some information about them.
@@ -42,21 +42,19 @@
from tg import flash, request, redirect, expose, tmpl_context, app_globals
from pylons.i18n import ugettext as _, lazy_ugettext as l_
-from luci.lib.base import Subcontroller
+from luci.lib.base import Subcontroller, SubcontrollerApplyMixin
from luci.controllers.decorators import *
from luci.lib.strings import TitleStrings
from luci.lib.helpers import urlList2String
-from luci.lib.ricci_helpers import send_batch_to_hosts
-from luci.lib.ricci_communicator import RicciCommunicator
-import luci.lib.ricci_queries as rq
-
__all__ = ['ClusterController', 'ClusterCmdController']
# Imports into module's namespace.
scheme = app_globals.scheme
+cluster_scheme = app_globals.scheme.ClusterScheme
data = app_globals.data
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
#
@@ -66,71 +64,150 @@ data = app_globals.data
class ClusterController(Subcontroller):
"""Subcontroller handling basic requests related with `clusters' part."""
- @forPageShowWithUrlCorrection()
+ @forPageShowWithUrlCorrection('luci.templates.cluster_list')
def default(self):
"""Handle simple cluster listing.
- (Handle requests not connected with any of defined method.)
+ E.g. <http://example.com/clusters> for situation described
+ in the module's doc.
"""
- tmpl_context.title_list.append(TitleStrings.CLUSTER)
- flash(_('Clusters list not implemented yet.'), status='warning')
- redirect(urlList2String(scheme.APP_PREFIX))
+ tmpl_context.title_list.append(TitleStrings.CLUSTERS)
+
+ app_url = scheme.APP_PREFIX + u'/'
+
+ return dict(base_url=app_url + scheme.CLUSTERS,
+ cmd_url=app_url + base2CmdCtrl(scheme.CLUSTERS),
+ apply_cmds=ClusterApplyCommands)
@expose()
def lookup(self, name, *args):
- """Dynamic dispatching to subcontrollers according to cluster's name."""
-
+ """Dynamic dispatching to subcontrollers according to cluster's name.
+
+ E.g. <http://example.com/clusters/ClusterOne> for situation described
+ in the module's doc.
+
+ The request is dynamically delegated to _CertainClusterController
+ object.
+
+ Keyword arguments:
+ name Name of the cluster to be taken as a context for
+ nested subcontrollers (NodeController, etc.).
+
+ """
if name in data.clusters.iterkeys():
- # Following 3 lines necessary because of strange behavior of TG.
- if not tmpl_context.title_used.get('cluster'):
- tmpl_context.title_list.append(TitleStrings.CLUSTER % name)
- tmpl_context.title_used['cluster'] = True
+ # Following 3 lines necessary because of strange(?) behavior of TG.
+ if not tmpl_context.already_used.get('cluster'):
+ tmpl_context.title_list.append(TitleStrings.CERTAIN_CLUSTER \
+ % name)
+ tmpl_context.cluster_name = name
+ tmpl_context.cluster_data = data.clusters[name]
+ tmpl_context.cluster_url = \
+ urlList2String(scheme.APP_PREFIX, scheme.CLUSTERS, name)
+ tmpl_context.already_used['cluster'] = True
dynamic_ctrl = _CertainClusterController()
- tmpl_context.cluster_name = name
- tmpl_context.cluster_data = data.clusters[name]
- tmpl_context.cluster_url = \
- urlList2String(scheme.APP_PREFIX, scheme.CLUSTERS, name)
return dynamic_ctrl, args
else:
- msg = l_('Bad cluster name.')
+ msg = l_('Bad cluster name: %s') % name
status = 'error'
flash(msg, status=status)
redirect(urlList2String(scheme.APP_PREFIX, scheme.CLUSTERS))
-(a)mountSubControllers(scheme.getCertainClusterMounts())
+(a)mountSubControllers(cluster_scheme.mounts())
class _CertainClusterController(Subcontroller):
- # Subcontroller handling information pages for certain cluster.
+ """Subcontroller preparing subcontrollers for certain cluster."""
@forImmediateRedirect(allowed_methods='GET', use_referrer=False)
def default(self, *args, **kwargs):
- return dict(redir_target=\
- urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_NODES))
+ """Handle request for displaying details of certain cluster.
+
+ E.g. <http://example.com/clusters/ClusterOne> for situation described
+ in the module's doc.
+
+ Redirect to `nodes' part in the context the cluster.
+
+ """
+ return dict(redir_target= urlList2String(tmpl_context.cluster_url,
+ cluster_scheme.NODES))
-# @expose()
-# def lookup(self, *args):
-# redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_NODES))
+ # 'lookup' method not necessary, 'default' method above is enough even for
+ # such cases.
#
# COMMANDS HANDLING.
#
-class ClusterCmdController(Subcontroller):
+class ClusterApplyCommands:
+ """Encapsulation of available commands to apply over selected cluster(s)."""
+
+ DELETE = 'cmd_delete'
+
+
+ def _delete(which):
+ clusters = app_globals.data.clusters
+ msg = l_('Deleting selected cluster(s)... %s') \
+ % (', '.join(which))
+ status = 'info'
+ for cluster in which:
+ if clusters.has_key(cluster):
+ del clusters[cluster]
+ else:
+ # If Luci's data are consistent, this should never happen.
+ msg = l_('Internal error.'),
+ status = '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)
+
+
+class ClusterCmdController(Subcontroller, SubcontrollerApplyMixin):
"""Subcontroller handling commands related with `clusters' part."""
+ _apply_cmds = ClusterApplyCommands
+
@forPageShowWithUrlCorrection('luci.templates.create')
def create(self, name=""):
"""Display form for cluster creation."""
- return dict()
+ return dict(cmd_url= \
+ scheme.APP_PREFIX + u'/' + base2CmdCtrl(scheme.CLUSTERS))
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def add(self):
+ """Handle addition of a new cluster."""
+
+ flash('It should be a dialog to add new cluster 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 cluster_create(self, **kw):
"""Handle the process of cluster creation."""
- errors = ()
+
+ from sys import stderr
+ from luci.lib.ricci_helpers import send_batch_to_hosts
+ from luci.lib.ricci_communicator import RicciCommunicator, RicciError
+ import luci.lib.ricci_queries as rq
+
+ redir_target = urlList2String(scheme.APP_PREFIX,
+ base2CmdCtrl(scheme.CLUSTERS),
+ 'create')
+
+ errors = []
cluster_name = kw.get('clustername')
enable_storage = kw.get('enable_storage')
reboot_nodes = kw.get('reboot_nodes')
@@ -146,36 +223,41 @@ class ClusterCmdController(Subcontroller):
if not cluster_name:
flash(_('No cluster name was given'))
- redirect('/create')
+ return dict(redir_target=redir_target)
+
if len(cluster_name) > 15:
flash(_('Cluster names must be less than 16 characters long'))
- redirect('/create')
+ return dict(redir_target=redir_target)
for node in nodes:
- rc = RicciCommunicator(node[0], enforce_trust=False)
- rc.trust()
- rc.auth(node[1])
- if not rc.authed():
- errors.append('Authentication to node %s failed' % node[0])
- break
+ try:
+ rc = RicciCommunicator(node[0], enforce_trust=False)
+ except RicciError, errmsg:
+ errors.append(str(errmsg))
else:
- rc = RicciCommunicator(node[0])
-
- cur_cluster_name = rc.cluster_info()[0]
- if cur_cluster_name:
- errors.append('%s is already a member of a cluster %s' \
- % (node[0], cur_cluster_name))
- break
+ rc.trust()
+ rc.auth(node[1])
+ if not rc.authed():
+ errors.append('Authentication to node %s failed' % node[0])
+ break
+ else:
+ rc = RicciCommunicator(node[0])
+
+ cur_cluster_name = rc.cluster_info()[0]
+ if cur_cluster_name:
+ errors.append('%s is already a member of a cluster %s' \
+ % (node[0], cur_cluster_name))
+ break
if len(errors) > 0:
flash('The following errors occurred: %s' % str(errors))
- redirect("/create")
+ return dict(redir_target=redir_target)
ret = send_batch_to_hosts(node_list, 10, rq.create_cluster,
'rhel5', cluster_name, cluster_name,
node_list, True, True, enable_storage, False,
download_pkgs, None, reboot_nodes)
- base_url_str = urlList2String(self.ctrl_url[:-1]) + ['/create']
+ base_url_str = urlList2String(scheme.APP_PREFIX, self.base_name)
return dict(use_referrer=False, redir_target=base_url_str)
diff --git a/luci/controllers/cluster_part/failover.py b/luci/controllers/cluster_part/failover.py
index c18022d..86fcef4 100644
--- a/luci/controllers/cluster_part/failover.py
+++ b/luci/controllers/cluster_part/failover.py
@@ -13,7 +13,7 @@
http://example.com/clusters/ClusterOne. Failovers in the cluster are
Failover1 and Failover2.
- Accessing following URLs has following consequences:
+ Accessing following URLs has following consequences (some cases omitted):
1) <http://example.com/clusters/ClusterOne/failovers>
OR <http://example.com/clusters/ClusterOne/failovers/>
@@ -42,7 +42,7 @@
- apply updating members of certain failover.
7) <http://example.com/clusters/ClusterOne/failovers_cmd/apply>
- OR <http://example.com/clusters/ClusterOne/failovers_cmd/apply?cmd=CMDID...>
+ OR <http://example.com/clusters/ClusterOne/failovers_cmd/apply?CMDID...>
- apply a command on given failover(s); both GET and POST method
supported.
@@ -60,10 +60,14 @@ __all__ = ['FailoverController', 'FailoverCmdController']
# Imports into module's namespace.
form_utils = app_globals.form_utils
-scheme = app_globals.scheme
+cluster_scheme = app_globals.scheme.ClusterScheme
base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+#
+# ITEMS LISTING.
+#
+
class FailoverController(Subcontroller):
"""Subcontroller handling basic requests related with `failovers' part."""
@@ -77,14 +81,13 @@ class FailoverController(Subcontroller):
"""
tmpl_context.title_list.append(TitleStrings.FAILOVERS)
- base_url_str = urlList2String(tmpl_context.cluster_url,
- scheme.CLUSTER_FAILOVERS)
- cmd_url_str = urlList2String(tmpl_context.cluster_url,
- base2CmdCtrl(scheme.CLUSTER_FAILOVERS))
+ cluster_url = tmpl_context.cluster_url + u'/'
return dict(name=None,
- base_url=base_url_str,
- cmd_url=cmd_url_str,
+ base_url=cluster_url + cluster_scheme.FAILOVERS,
+ cmd_url=cluster_url + base2CmdCtrl(cluster_scheme.FAILOVERS),
+ service_cmd_url= \
+ cluster_url + base2CmdCtrl(cluster_scheme.SERVICES),
apply_cmds=FailoverApplyCommands)
@@ -111,7 +114,8 @@ class FailoverController(Subcontroller):
status = 'error'
flash(msg, status=status)
- redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_FAILOVERS))
+ redirect(urlList2String(tmpl_context.cluster_url,
+ cluster_scheme.FAILOVERS))
class _CertainFailoverController(Subcontroller):
@@ -125,16 +129,16 @@ class _CertainFailoverController(Subcontroller):
for situation described in the module's doc.
"""
- tmpl_context.title_list.append(TitleStrings.FAILOVER % self.entity_name)
+ tmpl_context.title_list.append(TitleStrings.CERTAIN_FAILOVER \
+ % self.entity_name)
- base_url_str = urlList2String(tmpl_context.cluster_url,
- scheme.CLUSTER_FAILOVERS)
- cmd_url_str = urlList2String(tmpl_context.cluster_url,
- base2CmdCtrl(scheme.CLUSTER_FAILOVERS))
+ cluster_url = tmpl_context.cluster_url + u'/'
return dict(name=self.entity_name,
- base_url=base_url_str,
- cmd_url=cmd_url_str,
+ base_url=cluster_url + cluster_scheme.FAILOVERS,
+ cmd_url=cluster_url + base2CmdCtrl(cluster_scheme.FAILOVERS),
+ service_cmd_url= \
+ cluster_url + base2CmdCtrl(cluster_scheme.SERVICES),
apply_cmds=FailoverApplyCommands)
@@ -146,8 +150,9 @@ class _CertainFailoverController(Subcontroller):
for situation described in the module's doc.
"""
- redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_FAILOVERS,
- self.node_name))
+ redirect(urlList2String(tmpl_context.cluster_url,
+ cluster_scheme.FAILOVERS,
+ self.entity_name))
#
@@ -195,7 +200,7 @@ class FailoverCmdController(Subcontroller, SubcontrollerApplyMixin):
flash(VerboseStrings.BAD_COMMAND_REQUEST, status='error')
- base_url_str = urlList2String(tmpl_context.cluster_url, self.base_name)
+ base_url_str = urlList2String(tmpl_context.cluster_url, base_name)
return dict(redir_target=base_url_str)
@@ -203,7 +208,7 @@ class FailoverCmdController(Subcontroller, SubcontrollerApplyMixin):
def add(self):
"""Handle creation of a new failover domain."""
- flash('It should be a dialog to add new failover domain here instead,'
+ 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
@@ -253,23 +258,19 @@ class FailoverCmdController(Subcontroller, SubcontrollerApplyMixin):
if failovers.has_key(failover):
# Clear the dictionary of member nodes and start from scratch.
failovers[failover]['nodes'].clear()
- nodes = map(lambda id_check: form_utils.id2String(id_check.replace('.check', '')),
- filter(lambda id_str: id_str.endswith('.check'),
- kwargs.iterkeys()))
+ 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 in nodes:
+ for node_id, node in nodes:
if tmpl_context.cluster_data.nodes.has_key(node):
failovers[failover]['nodes'][node] = \
- kwargs.get(form_utils.id2String(node) + '.priority', 0)
+ kwargs.get(node_id + '.priority', 0)
else:
# If the luci's data are consistent this should never
# happen.
flash(_('Internal error'), status='error')
- @forImmediateRedirect(allowed_methods = 'GET')
- def service(self):
- """Handle creation of a new service."""
-
- flash('Demo of adding a service...', status='info')
-
diff --git a/luci/controllers/cluster_part/fence.py b/luci/controllers/cluster_part/fence.py
index 75d99a0..565921d 100644
--- a/luci/controllers/cluster_part/fence.py
+++ b/luci/controllers/cluster_part/fence.py
@@ -12,7 +12,7 @@
http://example.com/clusters/ClusterOne. Fences in the cluster are
FenceA and FenceB.
- Accessing following URLs has following consequences:
+ Accessing following URLs has following consequences (some cases omitted):
1) <http://example.com/clusters/ClusterOne/fences>
OR <http://example.com/clusters/ClusterOne/fences/>
@@ -35,7 +35,7 @@
- display form for a fence addition.
5) <http://example.com/clusters/ClusterOne/fences_cmd/apply>
- OR <http://example.com/clusters/ClusterOne/fences_cmd/apply?cmd=CMDID...>
+ OR <http://example.com/clusters/ClusterOne/fences_cmd/apply?CMDID...>
- apply a command on given node(s); both GET and POST method supported.
"""
@@ -53,7 +53,7 @@ __all__ = ['FenceController', 'FenceCmdController']
# Imports into module's namespace.
form_utils = app_globals.form_utils
-scheme = app_globals.scheme
+cluster_scheme = app_globals.scheme.ClusterScheme
base2CmdCtrl = app_globals.scheme.base2CmdCtrl
@@ -69,19 +69,17 @@ class FenceController(Subcontroller):
"""Handle simple fences listing.
E.g. <http://example.com/clusters/ClusterOne/fences> for situation
- described in the module's doc.
+ described in the module's doc.
"""
tmpl_context.title_list.append(TitleStrings.FENCES)
- base_url_str = urlList2String(tmpl_context.cluster_url,
- scheme.CLUSTER_FENCES)
- cmd_url_str = urlList2String(tmpl_context.cluster_url,
- base2CmdCtrl(scheme.CLUSTER_FENCES))
+ cluster_url = tmpl_context.cluster_url + u'/'
return dict(name=None,
- base_url=base_url_str,
- cmd_url=cmd_url_str,
+ 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)
@@ -107,7 +105,8 @@ class FenceController(Subcontroller):
status = 'error'
flash(msg, status=status)
- redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_FENCES))
+ redirect(urlList2String(tmpl_context.cluster_url,
+ cluster_scheme.FENCES))
class _CertainFenceController(Subcontroller):
@@ -116,23 +115,23 @@ class _CertainFenceController(Subcontroller):
@forPageShowWithUrlCorrection('luci.templates.cluster_part.fence')
def default(self):
"""Handle fences listing with details of certain fence.
-
+
E.g. <http://example.com/clusters/ClusterOne/fences/FenceA>
for situation described in the module's doc.
"""
- tmpl_context.title_list.append(TitleStrings.FENCE % self.entity_name)
-
- base_url_str = urlList2String(tmpl_context.cluster_url,
- scheme.CLUSTER_FENCES)
- cmd_url_str = urlList2String(tmpl_context.cluster_url,
- base2CmdCtrl(scheme.CLUSTER_FENCES))
+ tmpl_context.title_list.append(TitleStrings.CERTAIN_FENCE \
+ % self.entity_name)
+
+ cluster_url = tmpl_context.cluster_url + u'/'
return dict(name=self.entity_name,
- base_url=base_url_str,
- cmd_url=cmd_url_str,
+ 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 node.
@@ -141,7 +140,7 @@ class _CertainFenceController(Subcontroller):
for situation described in the module's doc.
"""
- redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_FENCES,
+ redirect(urlList2String(tmpl_context.cluster_url, cluster_scheme.FENCES,
self.entity_name))
@@ -197,9 +196,9 @@ class FenceCmdController(Subcontroller, SubcontrollerApplyMixin):
@forImmediateRedirect(allowed_methods = ('GET', 'POST'))
def default(self, *args, **kwargs):
"""Handle requests not connected with any of defined method."""
-
+
flash(VerboseStrings.BAD_COMMAND_REQUEST, status='error')
-
+
base_url_str = urlList2String(tmpl_context.cluster_url, self.base_name)
return dict(redir_target=base_url_str)
diff --git a/luci/controllers/cluster_part/node.py b/luci/controllers/cluster_part/node.py
index 0a50b36..298408c 100644
--- a/luci/controllers/cluster_part/node.py
+++ b/luci/controllers/cluster_part/node.py
@@ -35,7 +35,7 @@
- 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?cmd=CMDID...>
+ OR <http://example.com/clusters/ClusterOne/nodes_cmd/apply?CMDID...>
- apply a command on given node(s); both GET and POST method supported.
"""
@@ -52,7 +52,7 @@ __all__ = ['NodeController', 'NodeCmdController']
# Imports into module's namespace.
form_utils = app_globals.form_utils
-scheme = app_globals.scheme
+cluster_scheme = app_globals.scheme.ClusterScheme
base2CmdCtrl = app_globals.scheme.base2CmdCtrl
@@ -73,14 +73,14 @@ class NodeController(Subcontroller):
"""
tmpl_context.title_list.append(TitleStrings.NODES)
- base_url_str = urlList2String(tmpl_context.cluster_url,
- scheme.CLUSTER_NODES)
- cmd_url_str = urlList2String(tmpl_context.cluster_url,
- base2CmdCtrl(scheme.CLUSTER_NODES))
+ cluster_url = tmpl_context.cluster_url + u'/'
return dict(name=None,
- base_url=base_url_str,
- cmd_url=cmd_url_str,
+ 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)
@@ -102,11 +102,11 @@ class NodeController(Subcontroller):
dynamic_ctrl = _CertainNodeController(entity_name=name)
return dynamic_ctrl, args
else:
- msg = l_('Bad node name.')
+ msg = l_('Bad node name: %s') % name
status = 'error'
flash(msg, status=status)
- redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_NODES))
+ redirect(urlList2String(tmpl_context.cluster_url, cluster_scheme.NODES))
class _CertainNodeController(Subcontroller):
@@ -120,16 +120,17 @@ class _CertainNodeController(Subcontroller):
for situation described in the module's doc.
"""
- tmpl_context.title_list.append(TitleStrings.NODE % self.entity_name)
-
- base_url_str = urlList2String(tmpl_context.cluster_url,
- scheme.CLUSTER_NODES)
- cmd_url_str = urlList2String(tmpl_context.cluster_url,
- base2CmdCtrl(scheme.CLUSTER_NODES))
+ tmpl_context.title_list.append(TitleStrings.CERTAIN_NODE \
+ % self.entity_name)
+
+ cluster_url = tmpl_context.cluster_url + u'/'
return dict(name=self.entity_name,
- base_url=base_url_str,
- cmd_url=cmd_url_str,
+ 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)
@@ -141,7 +142,7 @@ class _CertainNodeController(Subcontroller):
for situation described in the module's doc.
"""
- redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_NODES,
+ redirect(urlList2String(tmpl_context.cluster_url, cluster_scheme.NODES,
self.entity_name))
@@ -228,7 +229,7 @@ class NodeCmdController(Subcontroller, SubcontrollerApplyMixin):
def add(self):
"""Handle creation of a new node."""
- flash('It should be a dialog to add new node here instead,'
+ 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
@@ -256,13 +257,3 @@ class NodeCmdController(Subcontroller, SubcontrollerApplyMixin):
base_url_str = urlList2String(tmpl_context.cluster_url, self.base_name)
return dict(redir_target=base_url_str)
-
- @forImmediateRedirect(allowed_methods = 'GET')
- def service(self):
- """Handle creation of a new service."""
-
- flash('Demo of adding a service...', status='info')
-
- base_url_str = urlList2String(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
index 7f1f7a5..86ec20a 100644
--- a/luci/controllers/cluster_part/service.py
+++ b/luci/controllers/cluster_part/service.py
@@ -36,7 +36,7 @@
- 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?cmd=CMDID...>
+ OR <http://example.com/clusters/ClusterOne/services_cmd/apply?CMDID...>
- apply a command on given service(s); both GET and POST method supported.
"""
@@ -53,7 +53,7 @@ __all__ = ['ServiceController', 'ServiceCmdController']
# Imports into module's namespace.
form_utils = app_globals.form_utils
-scheme = app_globals.scheme
+cluster_scheme = app_globals.scheme.ClusterScheme
base2CmdCtrl = app_globals.scheme.base2CmdCtrl
@@ -67,21 +67,18 @@ class ServiceController(Subcontroller):
@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.
-
+
"""
tmpl_context.title_list.append(TitleStrings.SERVICES)
- base_url_str = urlList2String(tmpl_context.cluster_url,
- scheme.CLUSTER_SERVICES)
- cmd_url_str = urlList2String(tmpl_context.cluster_url,
- base2CmdCtrl(scheme.CLUSTER_SERVICES))
+ cluster_url = tmpl_context.cluster_url + u'/'
return dict(name=None,
- base_url=base_url_str,
- cmd_url=cmd_url_str,
+ base_url=cluster_url + cluster_scheme.SERVICES,
+ cmd_url=cluster_url + base2CmdCtrl(cluster_scheme.SERVICES),
apply_cmds=ServiceApplyCommands)
@@ -100,7 +97,7 @@ class ServiceController(Subcontroller):
"""
# Check whether required service exists.
- if tmpl_context.cluster_data.services.has_key(name):
+ if tmpl_context.cluster_data.services.has_key(name):
dynamic_ctrl = _CertainServiceController(entity_name=name)
return dynamic_ctrl, args
else:
@@ -108,7 +105,7 @@ class ServiceController(Subcontroller):
status = 'error'
flash(msg, status=status)
- redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_SERVICES))
+ redirect(urlList2String(tmpl_context.cluster_url, cluster_scheme.SERVICES))
class _CertainServiceController(Subcontroller):
@@ -117,22 +114,20 @@ class _CertainServiceController(Subcontroller):
@forPageShowWithUrlCorrection('luci.templates.cluster_part.service')
def default(self):
"""Handle services listing with details of certain service.
-
+
E.g. <http://example.com/clusters/ClusterOne/services/ServiceX>
for situation described in the module's doc.
-
+
"""
- tmpl_context.title_list.append(TitleStrings.SERVICE % self.entity_name)
-
- base_url_str = urlList2String(tmpl_context.cluster_url,
- scheme.CLUSTER_SERVICES)
- cmd_url_str = urlList2String(tmpl_context.cluster_url,
- base2CmdCtrl(scheme.CLUSTER_SERVICES))
+ tmpl_context.title_list.append(TitleStrings.CERTAIN_SERVICE \
+ % self.entity_name)
+
+ cluster_url = tmpl_context.cluster_url + u'/'
return dict(name=self.entity_name,
- base_url=base_url_str,
- cmd_url=cmd_url_str,
- apply_cmds=ServiceApplyCommands)
+ base_url=cluster_url + cluster_scheme.SERVICES,
+ cmd_url=cluster_url + base2CmdCtrl(cluster_scheme.SERVICES),
+ apply_cmds=ServiceApplyCommands)
@expose()
@@ -144,7 +139,7 @@ class _CertainServiceController(Subcontroller):
"""
redirect(urlList2String(tmpl_context.cluster_url,
- scheme.CLUSTER_SERVICES, self.node_name))
+ cluster_scheme.SERVICES, self.node_name))
#
@@ -199,9 +194,9 @@ class ServiceCmdController(Subcontroller, SubcontrollerApplyMixin):
@forImmediateRedirect(allowed_methods = ('GET', 'POST'))
def default(self, *args, **kwargs):
"""Handle requests not connected with any of defined method."""
-
+
flash(VerboseStrings.BAD_COMMAND_REQUEST, status='error')
-
+
base_url_str = urlList2String(tmpl_context.cluster_url, self.base_name)
return dict(redir_target=base_url_str)
@@ -210,13 +205,18 @@ class ServiceCmdController(Subcontroller, SubcontrollerApplyMixin):
def add(self, node=None, failover=None):
"""Handle creation of a new service."""
- flash('It should be a dialog to add a new service here instead,'
- 'but not implemented yet...', status='info')
+ 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 = urlList2String(self.ctrl_url[:-1]) + u'/' \
- + self.base_name
- return dict(redir_target=base_url_str)
+
+ base_url_str = urlList2String(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 c2bc73c..e54d9c3 100644
--- a/luci/controllers/decorators.py
+++ b/luci/controllers/decorators.py
@@ -19,35 +19,33 @@ base2CmdCtrl = app_globals.scheme.base2CmdCtrl
def mountSubControllers(ctrls_dict=None, **ctrls_kwargs):
- """Decorator serving as an elegant way to mount subcontrollers.
+ """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.
- 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).
-
-
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. Let's have this example of use:
+ 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:
---
+ @mountLuciSubControllers(items=(ItemController, ItemCmdController))
class RootController(BaseController):
...
- @mountLuciSubControllers(items=(ItemController, ItemCmdController))
- def __new__(cls, *args, **kwargs): pass
- ...
---
Then it's equivalent with following commands sequence:
---
class RootController(BaseController):
- ...
items = ItemController()
- items_cmd = ItemCmdController()
+ items_cmd = ItemCmdController(base_name='items')
...
---
Notice: '_cmd' suffix is default suffix for `commands subcontroller'
@@ -60,8 +58,8 @@ def mountSubControllers(ctrls_dict=None, **ctrls_kwargs):
"""
def decorator(cls):
- """Function (method) binding."""
- # Take all specified subcontrollers and mount them.
+ """Class binding."""
+ # Take all specified subcontrollers and `mount' them to the class.
if ctrls_dict and type(ctrls_dict is dict):
ctrls_kwargs.update(ctrls_dict)
for base_name, controller in ctrls_kwargs.iteritems():
@@ -84,46 +82,6 @@ def mountSubControllers(ctrls_dict=None, **ctrls_kwargs):
# Retval is the value returned by the decorator.
return decorator
-# def decorator(fn):
-# """Function (method) binding."""
-# def wrapper(object, *args, **kwargs):
-# """Arguments binding, decorated function (method) call."""
-#
-# # Wrapper's retval is the value returned by the method of the same
-# # name taken from the base class.
-#
-# if hasattr(object, '__base__'):
-# # `object' is a class.
-# base_fn = getattr(object.__base__, fn.func_name)
-# else:
-# # `object' is an instance.
-# base_fn = getattr(object.__class__.__base__, fn.func_name)
-# retval = base_fn(object, *args, **kwargs)
-#
-# # Take all specified subcontrollers and mount them.
-# if ctrls_dict and type(ctrls_dict is dict):
-# ctrls_kwargs.update(ctrls_dict)
-# for base_name, controller in ctrls_kwargs.iteritems():
-# # Branch according to whether a pair of controllers is used
-# # instead of a single controller.
-# if type(controller) in (tuple, list) and len(controller) == 2:
-# # 1) Pair of controllers.
-# cmd_name = base2CmdCtrl(base_name)
-# setattr(object, base_name, controller[0]())
-# setattr(object, cmd_name, controller[1](base_name=base_name))
-# else:
-# # 2) Single controller.
-# if type(controller) in (tuple, list):
-# controller = controller[0]
-# setattr(object, base_name, controller())
-# return retval
-#
-# # Decorator's retval is the value returned by the wrapper.
-# return wrapper
-#
-# # Retval is the value returned by the decorator.
-# return decorator
-
ATTR_REDIR_TARGET = 'REDIR_TARGET'
diff --git a/luci/controllers/root.py b/luci/controllers/root.py
index 9ab430d..ac5df2f 100644
--- a/luci/controllers/root.py
+++ b/luci/controllers/root.py
@@ -7,7 +7,6 @@ from tg import tmpl_context
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from catwalk.tg2 import Catwalk
from repoze.what import predicates
-from sys import stderr
from luci.controllers.decorators import *
from luci.lib.base import BaseController
@@ -32,7 +31,7 @@ __all__ = ['RootController']
scheme = app_globals.scheme
-(a)mountSubControllers(scheme.getRootMounts())
+(a)mountSubControllers(scheme.mounts())
class RootController(BaseController):
"""
The root controller for the luci application.
@@ -51,8 +50,8 @@ class RootController(BaseController):
def __call__(self, environ, start_response):
"""Invoke the Controller"""
tmpl_context.title_list = []
- tmpl_context.title_used = dict()
- tmpl_context.title_used.setdefault(False)
+ tmpl_context.already_used = dict()
+ tmpl_context.already_used.setdefault(False)
return BaseController.__call__(self, environ, start_response)
# SUBCONTROLLERS
diff --git a/luci/controllers/scheme.py b/luci/controllers/scheme.py
index 7b3c5d0..caed64f 100644
--- a/luci/controllers/scheme.py
+++ b/luci/controllers/scheme.py
@@ -14,18 +14,20 @@ class LuciScheme:
# First level of the scheme.
-
CLUSTERS = u'clusters'
USERS = u'users'
STORAGE = u'storage'
SYSTEMS = u'systems'
@classmethod
- def getRootMounts(cls):
+ def mounts(cls):
"""Defined as a method to avoid circular."""
# TODO: getter?
from luci.controllers.cluster import ClusterController, \
ClusterCmdController
+# from luci.controllers.storage import StorageController, \
+# StorageCmdController
+
return {
cls.CLUSTERS: (ClusterController, ClusterCmdController)
# cls.USERS: (UsersController, UsersCmdController],
@@ -36,20 +38,21 @@ class LuciScheme:
# Second level of the scheme -- `Clusters'.
- CLUSTER_NODES = u'nodes'
- CLUSTER_SERVICES = u'services'
- CLUSTER_FAILOVERS = u'failovers'
- CLUSTER_FENCES = u'fences'
-
- @classmethod
- def getCertainClusterMounts(cls):
- from luci.controllers.cluster_part import *
- return {
- cls.CLUSTER_NODES: (NodeController, NodeCmdController),
- cls.CLUSTER_SERVICES: (ServiceController, ServiceCmdController),
- cls.CLUSTER_FAILOVERS: (FailoverController, FailoverCmdController),
- cls.CLUSTER_FENCES: (FenceController, FenceCmdController)
- }
+ class ClusterScheme:
+ NODES = u'nodes'
+ SERVICES = u'services'
+ FAILOVERS = u'failovers'
+ FENCES = u'fences'
+
+ @classmethod
+ def mounts(cls):
+ from luci.controllers.cluster_part import *
+ return {
+ cls.NODES: (NodeController, NodeCmdController),
+ cls.SERVICES: (ServiceController, ServiceCmdController),
+ cls.FAILOVERS: (FailoverController, FailoverCmdController),
+ cls.FENCES: (FenceController, FenceCmdController)
+ }
"""Suffix which makes identifier of base subcontroller unique.
diff --git a/luci/lib/base.py b/luci/lib/base.py
index 764af54..e48c964 100644
--- a/luci/lib/base.py
+++ b/luci/lib/base.py
@@ -50,8 +50,9 @@ class Subcontroller(object):
def __new__(cls, *args, **kwargs):
# If the class that invoked this method is directly inherited from this
- # class, set 'ctrl_url' attribute and delegate this invocation upwards
- # (omitting the level of LuciSubController class).
+ # 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':
for kw, v in kwargs.iteritems():
@@ -68,11 +69,47 @@ class SubcontrollerApplyMixin:
def apply(self, name=None, **kwargs):
"""Handle applying a command over selected node(s).
+ Class using this mixin has to define attribute '_apply_cmds' that
+ contains object that defines attribute 'cmds', which is a dictionary.
+
+ Example of definition of such object:
+ ---
+ class ApplyCommands:
+ DELETE = 'cmd_delete'
+
+ def _delete(which):
+ ...
+ return msg, status, False
+
+ # Mapping.
+ cmds = {DELETE: _delete}
+
+ _delete = staticmethod(_delete)
+ ---
+
+ This 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
+ on the context of use, e.g. nodes) in this manner:
+ 1) if name argument was specified, than the list consists of one
+ item -- this name
+ 2) otherwise the list consists of identifiers (taken from the rest
+ of keyword arguments) converted back to string form (they are
+ supposed to be previously converted to HTML form-safe sequences)
+ Then it calls associated function ('_delete' from example above),
+ passing the mentioned list of identifiers.
+
+ Such called function ('_delete' from example above) should return
+ 2- or 3-tuple (msg, status [, use_referrer]), where:
+ msg Message to be displayed (e.g. 'entity X deleted').
+ status Status argument for tg.flash ('info', 'warning', ...).
+ use_referrer Whether to allow use referrer (if available)
+ for redirection target.
+
Keyword arguments:
- cmd Identifier of the command (see 'NodeCommands.cmds'
- attribute).
- name Name of the node to be used (if GET method is used).
- kwargs Dict of nodes to be used (if POST method is used).
+ 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).
"""
cmd_set = False
diff --git a/luci/lib/demo_data.py b/luci/lib/demo_data.py
index b70ebc2..7eea305 100644
--- a/luci/lib/demo_data.py
+++ b/luci/lib/demo_data.py
@@ -10,21 +10,32 @@ class ClusterData:
self.clusters = {'ClusterOne': ClusterOne(),
'ClusterTwo': ClusterTwo(),
'ClusterThree': ClusterThree()}
+ self.storage = {'storage_a': StorageA(),
+ 'storage_b': StorageB(),
+ 'storage_c': StorageC()}
# Certain clusters.
class Cluster:
"""Base class for certain cluster."""
-
+
NODE_ACTIVE = '0'
NODE_UNKNOWN = '1'
NODE_INACTIVE = '2'
+ CLUSTER_OK = '0'
+ CLUSTER_UNKNOWN = '1'
+ CLUSTER_NOT_OK = '2'
+
class ClusterOne(Cluster):
"""Data for 'ClusterOne'."""
def __init__(self):
+ self.status = self.CLUSTER_OK
+ self.cluster_votes = 2
+ self.required_quorum = 1
+
self.nodes = \
{'NodeAlpha': {'ip': '144.92.235.11',
'serviceload': 10,
@@ -57,7 +68,7 @@ class ClusterOne(Cluster):
'services': ('Service Z'),
'cman': {'running': True, 'autostart': True},
'rgmanager': {'running': True, 'autostart': True}}}
-
+
self.services = \
{'Service W': {'running': True,
'autostart': True,
@@ -83,7 +94,7 @@ class ClusterOne(Cluster):
#'failover': None,
'node': 'NodeDelta',
'resources': {'Res A': ('Script', False)}}}
-
+
self.failovers = \
{'Failover1':{'prioritized': False,
'restricted': True,
@@ -122,7 +133,7 @@ class ClusterOne(Cluster):
'Service W'),
'nodes': {'NodeAlpha': 1,
'NodeEpsilon': 0}}}
-
+
self.fences = \
{'Fence A':{'type': 'iLO',
'host': 'hostname.host.org',
@@ -156,6 +167,10 @@ class ClusterOne(Cluster):
class ClusterTwo(Cluster):
"""Data for 'ClusterTwo'."""
def __init__(self):
+ self.status = self.CLUSTER_OK
+ self.cluster_votes = 2
+ self.required_quorum = 1
+
self.nodes = \
{'NodeAlpha 2': {'ip': '144.92.235.11',
'serviceload': 10,
@@ -188,7 +203,7 @@ class ClusterTwo(Cluster):
'services': ('Service Z 2'),
'cman': {'running': True, 'autostart': True},
'rgmanager': {'running': True, 'autostart': True}}}
-
+
self.services = \
{'Service W 2': {'running': True,
'autostart': True,
@@ -214,7 +229,7 @@ class ClusterTwo(Cluster):
#'failover': None,
'node': 'NodeDelta 2',
'resources': {'Res A': ('Script', False)}}}
-
+
self.failovers = \
{'Failover1 2':{'prioritized': False,
'restricted': True,
@@ -253,7 +268,7 @@ class ClusterTwo(Cluster):
'Service W 2'),
'nodes': {'NodeAlpha 2': 1,
'NodeEpsilon 2': 0}}}
-
+
self.fences = \
{'Fence A 2':{'type': 'iLO',
'host': 'hostname.host.org',
@@ -287,6 +302,11 @@ class ClusterTwo(Cluster):
class ClusterThree(Cluster):
"""Data for 'ClusterThree'."""
def __init__(self):
+
+ self.status = self.CLUSTER_OK
+ self.cluster_votes = 2
+ self.required_quorum = 1
+
self.nodes = \
{'NodeAlpha 3': {'ip': '144.92.235.11',
'serviceload': 10,
@@ -319,7 +339,7 @@ class ClusterThree(Cluster):
'services': ('Service Z 3'),
'cman': {'running': True, 'autostart': True},
'rgmanager': {'running': True, 'autostart': True}}}
-
+
self.services = \
{'Service W 3': {'running': True,
'autostart': True,
@@ -345,7 +365,7 @@ class ClusterThree(Cluster):
#'failover': None,
'node': 'NodeDelta 3',
'resources': {'Res A': ('Script', False)}}}
-
+
self.failovers = \
{'Failover1 3':{'prioritized': False,
'restricted': True,
@@ -384,7 +404,7 @@ class ClusterThree(Cluster):
'Service W 3'),
'nodes': {'NodeAlpha 3': 1,
'NodeEpsilon 3': 0}}}
-
+
self.fences = \
{'Fence A 3':{'type': 'iLO',
'host': 'hostname.host.org',
@@ -415,3 +435,24 @@ 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
+
diff --git a/luci/lib/strings.py b/luci/lib/strings.py
index f81fe25..0198710 100644
--- a/luci/lib/strings.py
+++ b/luci/lib/strings.py
@@ -18,25 +18,27 @@ class TitleStrings:
# respectively.
CLUSTERS = _('clusters')
- CLUSTER = _('cluster %s')
+ CERTAIN_CLUSTER = _('cluster %s')
NODES = _('nodes')
- NODE = _('node %s')
+ CERTAIN_NODE = _('node %s')
SERVICES = _('services')
- SERVICE = _('service %s')
+ CERTAIN_SERVICE = _('service %s')
FAILOVERS = _('failovers')
- FAILOVER = _('failover %s')
+ CERTAIN_FAILOVER = _('failover %s')
FENCES = _('fences')
- FENCE = _('fence %s')
+ CERTAIN_FENCE = _('fence %s')
+
+ STORAGE = _('storage')
+ CERTAIN_STORAGE = _('storage %s')
class VerboseStrings:
"""Strings used for error/warning/notice reporting to the user."""
-
+
BAD_COMMAND_REQUEST = _('Bad command request.')
INTERNAL_ERROR = _('Internal error.')
NOTHING_CHOSEN = _('Nothing was chosen.')
-
\ No newline at end of file
diff --git a/luci/public/css/cluster.css b/luci/public/css/cluster.css
new file mode 100644
index 0000000..2ec4844
--- /dev/null
+++ b/luci/public/css/cluster.css
@@ -0,0 +1,29 @@
+/*============================= C L U S T E R S ============================ */
+
+/* -------------------------------- overview -------------------------------- */
+
+.cluster_tlist_status {
+ text-align: left;
+ padding: 0 15px 0 0;
+}
+
+.cluster_tlist_votes {
+ text-align: left;
+ padding: 0 15px 0 0;
+}
+
+th.cluster_tlist_votes {
+ width: 15%;
+ min-width: 15%;
+}
+
+.cluster_tlist_quorum {
+ text-align: left;
+ padding: 0 15px 0 0;
+}
+
+th.cluster_tlist_quorum {
+ width: 25%;
+ min-width: 25%;
+}
+
diff --git a/luci/public/css/shared.css b/luci/public/css/shared.css
index c2613ca..5002c07 100644
--- a/luci/public/css/shared.css
+++ b/luci/public/css/shared.css
@@ -190,6 +190,19 @@ a.float_button {
background-image: url('../images/add-grey.png');
}
+/* create */
+#tb_create {
+ background: url('../images/create-white.png') center left no-repeat;
+ padding-left: 17px;
+ border: none;
+ cursor: pointer;
+ font-size: inherit;
+}
+
+#tb_create:hover {
+ background-image: url('../images/create-grey.png');
+}
+
/* delete */
#tb_delete {
background: url('../images/delete-white.png') center left no-repeat;
@@ -300,7 +313,7 @@ a.float_button {
float: right;
position: relative;
top: -5px;
- width: 50%;
+ width: 25%;
}
#details_header_buttons a {
diff --git a/luci/public/css/style.css b/luci/public/css/style.css
index 5dcb279..ca7e45c 100644
--- a/luci/public/css/style.css
+++ b/luci/public/css/style.css
@@ -1,4 +1,5 @@
@import url("shared.css");
+@import url("cluster.css");
@import url("node.css");
@import url("service.css");
@import url("failover.css");
diff --git a/luci/public/images/create-grey.png b/luci/public/images/create-grey.png
new file mode 100644
index 0000000..fc98f37
Binary files /dev/null and b/luci/public/images/create-grey.png differ
diff --git a/luci/public/images/create-white.png b/luci/public/images/create-white.png
new file mode 100644
index 0000000..bca6529
Binary files /dev/null and b/luci/public/images/create-white.png differ
diff --git a/luci/templates/cluster_list.html b/luci/templates/cluster_list.html
new file mode 100644
index 0000000..804b2cf
--- /dev/null
+++ b/luci/templates/cluster_list.html
@@ -0,0 +1,104 @@
+<!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>${title()}</title>
+</head>
+
+<body py:with="clusters = app_globals.data.clusters;
+ form_utils = app_globals.form_utils">
+
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
+ <div id="toolbar">
+ <!--<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 + '/create')}" class="toolbar_button" id="tb_create">create</a>
+ <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">
+ <thead>
+ <tr>
+ <th class="checkbox"></th>
+ <th class="icon"></th>
+ <th class="main_id">name</th>
+ <th class="cluster_tlist_status">status</th>
+ <th class="cluster_tlist_votes">total votes</th>
+ <th class="cluster_tlist_quorum">min. required quorum</th>
+ <td class="table_space"></td>
+ </tr>
+ <tr>
+ <td colspan="7" id="table_sep"><hr /></td>
+ </tr>
+ </thead>
+ <tbody>
+ <!--! List all the fences. -->
+ <tr py:for="i, (entity_name, cluster_data) in enumerate(clusters.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>
+ <!--! Branch according to the status of the cluster. -->
+ <py:choose test="cluster_data['status']">
+ <!--! 1) Cluster is OK. -->
+ <py:when test="cluster_data.CLUSTER_OK">
+ <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>
+ </py:when>
+ <!--! 2) Cluster is not OK. -->
+ <py:when test="cluster_data.CLUSTER_NOT_OK">
+ <td class="icon">
+ <img src="${tg.url('/images/exclamation.png')}" alt="Cluster is not OK." />
+ </td>
+ <td class="main_id">
+ <a href="${tg.url(base_url + '/' + entity_name)}">
+ <span class="entity_fail">${entity_name}</span>
+ </a>
+ </td>
+ <td class="cluster_tlist_status">Not OK</td>
+ </py:when>
+ <!--! 3) Status of the cluster is unknown. -->
+ <py:when test="cluster_data.CLUSTER_UNKNOWN">
+ <td class="icon">
+ <img src="${tg.url('/images/question.png')}" alt="Status of the cluster 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="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="table_space"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </form>
+
+ <!--! DETAILS SECTION. -->
+ <div id="details" py:if="len(clusters) == 0">
+ <div id="details_header">
+ <div id="not_selected">
+ No item to display
+ </div>
+ </div>
+ </div>
+
+</body>
+</html>
diff --git a/luci/templates/cluster_part/failover.html b/luci/templates/cluster_part/failover.html
index 6978352..315e27a 100644
--- a/luci/templates/cluster_part/failover.html
+++ b/luci/templates/cluster_part/failover.html
@@ -123,7 +123,7 @@
<!--! DETAILS - services section. -->
<div class="details_section">
- <a href="${tg.url(cmd_url + '/service')}" class="float_button">Add a Service</a>
+ <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">
diff --git a/luci/templates/cluster_part/fence.html b/luci/templates/cluster_part/fence.html
index e70f425..fa1464b 100644
--- a/luci/templates/cluster_part/fence.html
+++ b/luci/templates/cluster_part/fence.html
@@ -89,8 +89,8 @@
<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>
+ <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>
@@ -107,7 +107,7 @@
<!--! DETAILS - nodes section. -->
<div class="details_section">
- <a href="${tg.url('/nodes')}" class="float_button">Manage Nodes</a>
+ <a href="${tg.url(node_base_url)}" class="float_button">Manage Nodes</a>
<h4>Nodes</h4>
<div class="details_inner">
<table id="fence_tnodes">
diff --git a/luci/templates/cluster_part/node.html b/luci/templates/cluster_part/node.html
index ce0ce69..577b49a 100644
--- a/luci/templates/cluster_part/node.html
+++ b/luci/templates/cluster_part/node.html
@@ -125,7 +125,7 @@
<!--! DETAILS - services section. -->
<div class="details_section">
- <a href="${tg.url(cmd_url + '/add_service')}" class="float_button">Add a Service</a>
+ <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">
diff --git a/luci/templates/cluster_part/service.html b/luci/templates/cluster_part/service.html
index a9cefee..16e84a9 100644
--- a/luci/templates/cluster_part/service.html
+++ b/luci/templates/cluster_part/service.html
@@ -54,7 +54,12 @@
<span class="entity_ok">${entity_name}</span>
</a>
</td>
- <td class="service_tlist_status">${form_utils.compactString(_('Running on %s') % service_data['node'], 18)}</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 !-->
+ <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">
@@ -69,7 +74,11 @@
<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)}</td>
+ or _('Internal error'))">
+ ${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>
@@ -103,16 +112,15 @@
<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>
- <py:with vars="msg = 'bla'">
- <span class="details_header_info_label">status</span> <span class="details_header_info">${msg}</span>
+ <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>
- </py:with>
</div>
<!--! DETAILS - resources. -->
diff --git a/luci/templates/title.html b/luci/templates/title.html
index c49f5a9..4ca2211 100644
--- a/luci/templates/title.html
+++ b/luci/templates/title.html
@@ -1,5 +1,10 @@
<html xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude"
py:strip="">
-<py:def function="title">Luci | ${' > '.join(tmpl_context.title_list)}</py:def>
+<py:def function="title(title_list=tmpl_context.title_list)">
+Luci
+<py:if test="len(title_list) != 0"
+ py:replace="' | ' + ' > '.join(title_list)">
+</py:if>
+</py:def>
</html>
14 years, 8 months
[luci] Quite a big amount of changes, mainly of internal character.
by Jan Pokorný
commit 7ed7a700d586b2c72a178a1aede8ccd54aea5db5
Author: Jan Pokorny <jpokorny(a)redhat.com>
Date: Fri Sep 18 20:25:55 2009 +0200
Quite a big amount of changes, mainly of internal character.
- better files hierarchy
- context of certain cluster introduced
- systematic generation of page's titles
- some common code and constructions separated to standalone parts
(decorators, inheritance), which led to, let's say, mini framework
- more systematic overall concept
- more robust application (redirections on bad URLs etc.)
- minor changes (renamed files, identifiers, etc.)
luci/config/app_cfg.py | 2 +-
luci/controllers/cluster.py | 181 +++++++++
luci/controllers/cluster_part/__init__.py | 13 +
luci/controllers/cluster_part/failover.py | 275 +++++++++++++
luci/controllers/cluster_part/fence.py | 219 ++++++++++
luci/controllers/cluster_part/node.py | 268 +++++++++++++
luci/controllers/cluster_part/service.py | 222 +++++++++++
luci/controllers/decorators.py | 197 +++++++---
luci/controllers/demo_data.py | 144 -------
luci/controllers/fdom.py | 165 --------
luci/controllers/fence.py | 116 ------
luci/controllers/node.py | 125 ------
luci/controllers/root.py | 97 +----
luci/controllers/scheme.py | 72 ++++
luci/controllers/service.py | 127 ------
luci/i18n/ru/LC_MESSAGES/luci.po | 24 --
luci/lib/app_globals.py | 11 +-
luci/lib/base.py | 82 +++-
luci/lib/demo_data.py | 417 ++++++++++++++++++++
luci/lib/form_helpers.py | 62 ---
luci/lib/form_utils.py | 71 ++++
luci/lib/helpers.py | 20 +-
luci/lib/strings.py | 42 ++
luci/public/css/{fdom.css => failover.css} | 0
luci/public/css/style.css | 2 +-
luci/public/js/{fdom.js => failover.js} | 0
luci/templates/.cluster.html.swp | Bin 12288 -> 0 bytes
luci/templates/.homebase.html.swp | Bin 12288 -> 0 bytes
luci/templates/cluster_part/__init__.py | 2 +
.../{fdom.html => cluster_part/failover.html} | 58 ++--
luci/templates/{ => cluster_part}/fence.html | 63 ++--
luci/templates/{ => cluster_part}/node.html | 87 ++--
luci/templates/{ => cluster_part}/service.html | 67 ++--
luci/templates/master.html | 11 +-
luci/templates/title.html | 5 +
35 files changed, 2195 insertions(+), 1052 deletions(-)
---
diff --git a/luci/config/app_cfg.py b/luci/config/app_cfg.py
index bb6a03b..008d069 100644
--- a/luci/config/app_cfg.py
+++ b/luci/config/app_cfg.py
@@ -17,7 +17,7 @@ from tg.configuration import AppConfig
import luci
from luci import model
-from luci.lib import app_globals, helpers, ricci_communicator, ricci_defines, ricci_helpers
+from luci.lib import app_globals, helpers #, ricci_communicator, ricci_defines, ricci_helpers
base_config = AppConfig()
base_config.renderers = []
diff --git a/luci/controllers/cluster.py b/luci/controllers/cluster.py
new file mode 100644
index 0000000..8785c0d
--- /dev/null
+++ b/luci/controllers/cluster.py
@@ -0,0 +1,181 @@
+# -*- coding: utf-8 -*-
+
+"""Controller API for `clusters' part of the Luci.
+
+ Functionality description:
+
+ Let's have the main subcontroller (ClusterController) available through
+ 'clusters' identifier of the root controller and subcontroller handling
+ related commands (ClusterCmdController) through 'clusters_cmd' identifier.
+
+ Server is accessible via http://example.com. Clusters in the system are
+ ClusterOne, ClusterTwo and ClusterThree.
+
+ Accessing following URLs has following consequences:
+
+ 1) <http://example.com/clusters> OR <http://example.com/clusters/>
+ - displaying list of the clusters with some information about them.
+
+ 2) <http://example.com/clusters/ClusterOne>
+ OR <http://example.com/clusters/ClusterOne/>
+ - redirection to nodes listing related to cluster called `ClusterOne'
+ (<http://example.com/clusters/ClusterOne/nodes>) because there is no
+ general page for certain cluster, only a few pages with further
+ information about it (and the `nodes' is probably the most important).
+
+ 3) <http://example.com/clusters/NonExistingCluster>
+ OR <http://example.com/clusters/NonExistingCluster/>
+ OR <http://example.com/clusters/NonExistingCluster/something_more>
+ - redirection back to base `clusters' page and displaying information
+ about the request of non-existing cluster.
+
+ ---
+
+ 4) <http://example.com/clusters_cmd/create>
+ - display form for cluster creation.
+
+ 5) <http://example.com/clusters_cmd/add>
+ - display form for existing cluster addition.
+
+"""
+
+from tg import flash, request, redirect, expose, tmpl_context, app_globals
+from pylons.i18n import ugettext as _, lazy_ugettext as l_
+
+from luci.lib.base import Subcontroller
+from luci.controllers.decorators import *
+from luci.lib.strings import TitleStrings
+from luci.lib.helpers import urlList2String
+
+from luci.lib.ricci_helpers import send_batch_to_hosts
+from luci.lib.ricci_communicator import RicciCommunicator
+import luci.lib.ricci_queries as rq
+
+__all__ = ['ClusterController', 'ClusterCmdController']
+
+
+# Imports into module's namespace.
+scheme = app_globals.scheme
+data = app_globals.data
+
+
+#
+# ITEMS LISTING.
+#
+
+class ClusterController(Subcontroller):
+ """Subcontroller handling basic requests related with `clusters' part."""
+
+ @forPageShowWithUrlCorrection()
+ def default(self):
+ """Handle simple cluster listing.
+
+ (Handle requests not connected with any of defined method.)
+
+ """
+ tmpl_context.title_list.append(TitleStrings.CLUSTER)
+ flash(_('Clusters list not implemented yet.'), status='warning')
+ redirect(urlList2String(scheme.APP_PREFIX))
+
+
+ @expose()
+ def lookup(self, name, *args):
+ """Dynamic dispatching to subcontrollers according to cluster's name."""
+
+ if name in data.clusters.iterkeys():
+ # Following 3 lines necessary because of strange behavior of TG.
+ if not tmpl_context.title_used.get('cluster'):
+ tmpl_context.title_list.append(TitleStrings.CLUSTER % name)
+ tmpl_context.title_used['cluster'] = True
+ dynamic_ctrl = _CertainClusterController()
+ tmpl_context.cluster_name = name
+ tmpl_context.cluster_data = data.clusters[name]
+ tmpl_context.cluster_url = \
+ urlList2String(scheme.APP_PREFIX, scheme.CLUSTERS, name)
+ return dynamic_ctrl, args
+ else:
+ msg = l_('Bad cluster name.')
+ status = 'error'
+
+ flash(msg, status=status)
+ redirect(urlList2String(scheme.APP_PREFIX, scheme.CLUSTERS))
+
+
+(a)mountSubControllers(scheme.getCertainClusterMounts())
+class _CertainClusterController(Subcontroller):
+ # Subcontroller handling information pages for certain cluster.
+
+ @forImmediateRedirect(allowed_methods='GET', use_referrer=False)
+ def default(self, *args, **kwargs):
+ return dict(redir_target=\
+ urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_NODES))
+
+# @expose()
+# def lookup(self, *args):
+# redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_NODES))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class ClusterCmdController(Subcontroller):
+ """Subcontroller handling commands related with `clusters' part."""
+
+ @forPageShowWithUrlCorrection('luci.templates.create')
+ def create(self, name=""):
+ """Display form for cluster creation."""
+ return dict()
+
+ @forImmediateRedirect(allowed_methods='POST')
+ def cluster_create(self, **kw):
+ """Handle the process of cluster creation."""
+ errors = ()
+ cluster_name = kw.get('clustername')
+ enable_storage = kw.get('enable_storage')
+ reboot_nodes = kw.get('reboot_nodes')
+ download_pkgs = kw.get('download_pkgs')
+
+ nodes = [
+ [ kw.get('__SYSTEM0_addr'), kw.get('__SYSTEM0_passwd') ],
+ [ kw.get('__SYSTEM1_addr'), kw.get('__SYSTEM1_passwd') ],
+ [ kw.get('__SYSTEM2_addr'), kw.get('__SYSTEM2_passwd') ]
+ ]
+ node_list = [ i[0] for i in nodes ]
+ stderr.write('nodes: %s\n' % str(node_list))
+
+ if not cluster_name:
+ flash(_('No cluster name was given'))
+ redirect('/create')
+ if len(cluster_name) > 15:
+ flash(_('Cluster names must be less than 16 characters long'))
+ redirect('/create')
+
+ for node in nodes:
+ rc = RicciCommunicator(node[0], enforce_trust=False)
+ rc.trust()
+ rc.auth(node[1])
+ if not rc.authed():
+ errors.append('Authentication to node %s failed' % node[0])
+ break
+ else:
+ rc = RicciCommunicator(node[0])
+
+ cur_cluster_name = rc.cluster_info()[0]
+ if cur_cluster_name:
+ errors.append('%s is already a member of a cluster %s' \
+ % (node[0], cur_cluster_name))
+ break
+
+ if len(errors) > 0:
+ flash('The following errors occurred: %s' % str(errors))
+ redirect("/create")
+
+ ret = send_batch_to_hosts(node_list, 10, rq.create_cluster,
+ 'rhel5', cluster_name, cluster_name,
+ node_list, True, True, enable_storage, False,
+ download_pkgs, None, reboot_nodes)
+
+ base_url_str = urlList2String(self.ctrl_url[:-1]) + ['/create']
+ return dict(use_referrer=False, redir_target=base_url_str)
+
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..c18022d
--- /dev/null
+++ b/luci/controllers/cluster_part/failover.py
@@ -0,0 +1,275 @@
+# -*- 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 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:
+
+ 1) <http://example.com/clusters/ClusterOne/failovers>
+ OR <http://example.com/clusters/ClusterOne/failovers/>
+ - displaying list of the 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?cmd=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 luci.lib.base import Subcontroller, SubcontrollerApplyMixin
+from luci.controllers.decorators import *
+from luci.lib.strings import TitleStrings, VerboseStrings
+from luci.lib.helpers import urlList2String
+
+__all__ = ['FailoverController', 'FailoverCmdController']
+
+
+# Imports into module's namespace.
+form_utils = app_globals.form_utils
+scheme = app_globals.scheme
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+class FailoverController(Subcontroller):
+ """Subcontroller handling basic requests related with `failovers' part."""
+
+ @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.
+
+ """
+ tmpl_context.title_list.append(TitleStrings.FAILOVERS)
+
+ base_url_str = urlList2String(tmpl_context.cluster_url,
+ scheme.CLUSTER_FAILOVERS)
+ cmd_url_str = urlList2String(tmpl_context.cluster_url,
+ base2CmdCtrl(scheme.CLUSTER_FAILOVERS))
+
+ return dict(name=None,
+ base_url=base_url_str,
+ cmd_url=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(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_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.
+
+ """
+ tmpl_context.title_list.append(TitleStrings.FAILOVER % self.entity_name)
+
+ base_url_str = urlList2String(tmpl_context.cluster_url,
+ scheme.CLUSTER_FAILOVERS)
+ cmd_url_str = urlList2String(tmpl_context.cluster_url,
+ base2CmdCtrl(scheme.CLUSTER_FAILOVERS))
+
+ return dict(name=self.entity_name,
+ base_url=base_url_str,
+ cmd_url=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(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_FAILOVERS,
+ self.node_name))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class FailoverApplyCommands:
+ """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 = l_('Internal error.'),
+ status = '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)
+
+
+class FailoverCmdController(Subcontroller, SubcontrollerApplyMixin):
+ """Subcontroller handling commands related with `failovers' part."""
+
+ _apply_cmds = FailoverApplyCommands
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def default(self, *args, **kwargs):
+ """Handle requests not connected with any of defined method."""
+
+ flash(VerboseStrings.BAD_COMMAND_REQUEST, status='error')
+
+ base_url_str = urlList2String(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(_('Internal error.'), status='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(_('Internal error.'), status='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(_('Internal error.'), status='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 id_check: form_utils.id2String(id_check.replace('.check', '')),
+ filter(lambda id_str: id_str.endswith('.check'),
+ kwargs.iterkeys()))
+ flash(_('Updating members for %s') % failover, status='info')
+ for node in nodes:
+ if tmpl_context.cluster_data.nodes.has_key(node):
+ failovers[failover]['nodes'][node] = \
+ kwargs.get(form_utils.id2String(node) + '.priority', 0)
+ else:
+ # If the luci's data are consistent this should never
+ # happen.
+ flash(_('Internal error'), status='error')
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def service(self):
+ """Handle creation of a new service."""
+
+ flash('Demo of adding a service...', status='info')
+
diff --git a/luci/controllers/cluster_part/fence.py b/luci/controllers/cluster_part/fence.py
new file mode 100644
index 0000000..75d99a0
--- /dev/null
+++ b/luci/controllers/cluster_part/fence.py
@@ -0,0 +1,219 @@
+# -*- 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 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:
+
+ 1) <http://example.com/clusters/ClusterOne/fences>
+ OR <http://example.com/clusters/ClusterOne/fences/>
+ - displaying list of the 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?cmd=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 luci.lib.base import Subcontroller, SubcontrollerApplyMixin
+from luci.controllers.decorators import *
+from luci.lib.strings import TitleStrings, VerboseStrings
+from luci.lib.helpers import urlList2String
+
+
+__all__ = ['FenceController', 'FenceCmdController']
+
+
+# Imports into module's namespace.
+form_utils = app_globals.form_utils
+scheme = app_globals.scheme
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+#
+# ITEMS LISTING.
+#
+
+class FenceController(Subcontroller):
+ """Subcontroller handling basic requests related with `fences' part."""
+
+ @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.
+
+ """
+ tmpl_context.title_list.append(TitleStrings.FENCES)
+
+ base_url_str = urlList2String(tmpl_context.cluster_url,
+ scheme.CLUSTER_FENCES)
+ cmd_url_str = urlList2String(tmpl_context.cluster_url,
+ base2CmdCtrl(scheme.CLUSTER_FENCES))
+
+ return dict(name=None,
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ apply_cmds=FenceApplyCommands)
+
+
+ @expose()
+ def lookup(self, name, *args):
+ """Handle fences listing with details of certain fence.
+
+ 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(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_FENCES))
+
+
+class _CertainFenceController(Subcontroller):
+ """Subcontroller handling fences listing with details of certain fence."""
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.fence')
+ def default(self):
+ """Handle fences listing with details of certain fence.
+
+ E.g. <http://example.com/clusters/ClusterOne/fences/FenceA>
+ for situation described in the module's doc.
+
+ """
+ tmpl_context.title_list.append(TitleStrings.FENCE % self.entity_name)
+
+ base_url_str = urlList2String(tmpl_context.cluster_url,
+ scheme.CLUSTER_FENCES)
+ cmd_url_str = urlList2String(tmpl_context.cluster_url,
+ base2CmdCtrl(scheme.CLUSTER_FENCES))
+
+ return dict(name=self.entity_name,
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ apply_cmds=FenceApplyCommands)
+
+ @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(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_FENCES,
+ self.entity_name))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class FenceApplyCommands:
+ """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 = l_('Internal error.'),
+ status = '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)
+
+
+class FenceCmdController(Subcontroller, SubcontrollerApplyMixin):
+ """Subcontroller handling commands related with `fences' part."""
+
+ _apply_cmds = FenceApplyCommands
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def default(self, *args, **kwargs):
+ """Handle requests not connected with any of defined method."""
+
+ flash(VerboseStrings.BAD_COMMAND_REQUEST, status='error')
+
+ base_url_str = urlList2String(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..0a50b36
--- /dev/null
+++ b/luci/controllers/cluster_part/node.py
@@ -0,0 +1,268 @@
+# -*- 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 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 list of the 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?cmd=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 luci.lib.base import Subcontroller, SubcontrollerApplyMixin
+from luci.controllers.decorators import *
+from luci.lib.strings import TitleStrings, VerboseStrings
+from luci.lib.helpers import urlList2String
+
+__all__ = ['NodeController', 'NodeCmdController']
+
+
+# Imports into module's namespace.
+form_utils = app_globals.form_utils
+scheme = app_globals.scheme
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+#
+# ITEMS LISTING.
+#
+
+class NodeController(Subcontroller):
+ """Subcontroller handling basic requests related with `nodes' part."""
+
+ @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.
+
+ """
+ tmpl_context.title_list.append(TitleStrings.NODES)
+
+ base_url_str = urlList2String(tmpl_context.cluster_url,
+ scheme.CLUSTER_NODES)
+ cmd_url_str = urlList2String(tmpl_context.cluster_url,
+ base2CmdCtrl(scheme.CLUSTER_NODES))
+
+ return dict(name=None,
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ apply_cmds=NodeApplyCommands)
+
+
+ @expose()
+ def lookup(self, name, *args):
+ """Handle nodes listing with details of certain node.
+
+ 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.')
+ status = 'error'
+
+ flash(msg, status=status)
+ redirect(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_NODES))
+
+
+class _CertainNodeController(Subcontroller):
+ """Subcontroller handling nodes listing with details of certain node."""
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.node')
+ def default(self):
+ """Handle nodes listing with details of certain node.
+
+ E.g. <http://example.com/clusters/ClusterOne/nodes/NodeAlpha>
+ for situation described in the module's doc.
+
+ """
+ tmpl_context.title_list.append(TitleStrings.NODE % self.entity_name)
+
+ base_url_str = urlList2String(tmpl_context.cluster_url,
+ scheme.CLUSTER_NODES)
+ cmd_url_str = urlList2String(tmpl_context.cluster_url,
+ base2CmdCtrl(scheme.CLUSTER_NODES))
+
+ return dict(name=self.entity_name,
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ 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(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_NODES,
+ self.entity_name))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class NodeApplyCommands:
+ """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 = l_('Internal error.'),
+ status = '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)
+
+
+class NodeCmdController(Subcontroller, SubcontrollerApplyMixin):
+ """Subcontroller handling commands related with `nodes' part."""
+
+ _apply_cmds = NodeApplyCommands
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def default(self, *args, **kwargs):
+ """Handle requests not connected with any of defined method."""
+
+ flash(VerboseStrings.BAD_COMMAND_REQUEST, status='error')
+
+ base_url_str = urlList2String(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 = urlList2String(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 = urlList2String(tmpl_context.cluster_url, self.base_name)
+ return dict(redir_target=base_url_str)
+
+
+ @forImmediateRedirect(allowed_methods = 'GET')
+ def service(self):
+ """Handle creation of a new service."""
+
+ flash('Demo of adding a service...', status='info')
+
+ base_url_str = urlList2String(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..7f1f7a5
--- /dev/null
+++ b/luci/controllers/cluster_part/service.py
@@ -0,0 +1,222 @@
+# -*- 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 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 list of the 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?cmd=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 luci.lib.base import Subcontroller, SubcontrollerApplyMixin
+from luci.controllers.decorators import *
+from luci.lib.strings import TitleStrings, VerboseStrings
+from luci.lib.helpers import urlList2String
+
+__all__ = ['ServiceController', 'ServiceCmdController']
+
+
+# Imports into module's namespace.
+form_utils = app_globals.form_utils
+scheme = app_globals.scheme
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+#
+# ITEMS LISTING.
+#
+
+class ServiceController(Subcontroller):
+ """Subcontroller handling basic requests related with `services' part."""
+
+ @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.
+
+ """
+ tmpl_context.title_list.append(TitleStrings.SERVICES)
+
+ base_url_str = urlList2String(tmpl_context.cluster_url,
+ scheme.CLUSTER_SERVICES)
+ cmd_url_str = urlList2String(tmpl_context.cluster_url,
+ base2CmdCtrl(scheme.CLUSTER_SERVICES))
+
+ return dict(name=None,
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ apply_cmds=ServiceApplyCommands)
+
+
+ @expose()
+ def lookup(self, name, *args):
+ """Handle service listing with details of certain service.
+
+ 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(urlList2String(tmpl_context.cluster_url, scheme.CLUSTER_SERVICES))
+
+
+class _CertainServiceController(Subcontroller):
+ """Subcontroller handling services listing with details of certain service."""
+
+ @forPageShowWithUrlCorrection('luci.templates.cluster_part.service')
+ def default(self):
+ """Handle services listing with details of certain service.
+
+ E.g. <http://example.com/clusters/ClusterOne/services/ServiceX>
+ for situation described in the module's doc.
+
+ """
+ tmpl_context.title_list.append(TitleStrings.SERVICE % self.entity_name)
+
+ base_url_str = urlList2String(tmpl_context.cluster_url,
+ scheme.CLUSTER_SERVICES)
+ cmd_url_str = urlList2String(tmpl_context.cluster_url,
+ base2CmdCtrl(scheme.CLUSTER_SERVICES))
+
+ return dict(name=self.entity_name,
+ base_url=base_url_str,
+ cmd_url=cmd_url_str,
+ 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(urlList2String(tmpl_context.cluster_url,
+ scheme.CLUSTER_SERVICES, self.node_name))
+
+
+#
+# COMMANDS HANDLING.
+#
+
+class ServiceApplyCommands:
+ """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 = l_('Internal error.'),
+ status = '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)
+
+
+class ServiceCmdController(Subcontroller, SubcontrollerApplyMixin):
+ """Subcontroller handling commands related with `services' part."""
+
+ _apply_cmds = ServiceApplyCommands
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def default(self, *args, **kwargs):
+ """Handle requests not connected with any of defined method."""
+
+ flash(VerboseStrings.BAD_COMMAND_REQUEST, status='error')
+
+ base_url_str = urlList2String(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."""
+
+ flash('It should be a dialog to add a new service 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()
+
+ base_url_str = urlList2String(self.ctrl_url[:-1]) + u'/' \
+ + self.base_name
+ return dict(redir_target=base_url_str)
+
diff --git a/luci/controllers/decorators.py b/luci/controllers/decorators.py
index 22e090f..c2bc73c 100644
--- a/luci/controllers/decorators.py
+++ b/luci/controllers/decorators.py
@@ -1,70 +1,147 @@
# -*- coding: utf-8 -*-
+
"""Decorators used within the application's controllers."""
-from tg import url, redirect, request, flash, expose
+from tg import url, redirect, request, flash, expose, app_globals
from pylons.i18n import ugettext as _, lazy_ugettext as l_
-__all__ = ['mountLuciSubControllers',
+from luci.lib.helpers import urlList2String
+
+__all__ = ['mountSubControllers',
'forPageShowWithUrlCorrection',
'forPageShowOnly',
'forImmediateRedirect']
-ATTR_CTRL_URL = 'ctrl_url'
-def mountLuciSubControllers(**ctrls_kwargs):
+# Imports into module's namespace.
+APP_PREFIX = app_globals.scheme.APP_PREFIX
+base2CmdCtrl = app_globals.scheme.base2CmdCtrl
+
+
+def mountSubControllers(ctrls_dict=None, **ctrls_kwargs):
"""Decorator serving as an elegant way to mount subcontrollers.
- Instead of using (note repeating strings/identifiers):
+ 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.
+
+ 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).
+
+
+ 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. Let's have this example of use:
---
class RootController(BaseController):
...
- failover = FailoverController('failover')
- fence = FenceController('fence')
+ @mountLuciSubControllers(items=(ItemController, ItemCmdController))
+ def __new__(cls, *args, **kwargs): pass
...
---
- this decorator can be used with the same meaning as follows:
+ Then it's equivalent with following commands sequence:
---
class RootController(BaseController):
...
- @mountLuciSubControllers(failover = FailoverController,
- fence = FenceController)
- def __new__(cls, *args, **kwargs): pass
+ items = ItemController()
+ items_cmd = ItemCmdController()
...
---
+ Notice: '_cmd' suffix is default suffix for `commands subcontroller'
+ as given by 'base2CmdCtrl()' (see luci.controllers.scheme).
Keyword arguments:
- ctrls_kwargs Definition of controllers, see example above.
+ ctrls_dict Same purpose as ctrls_kwargs argument, but for
+ passing subcontrollers in the form of a single dict.
+ ctrls_kwargs Definition of subcontrollers, see examples above.
"""
- def decorator(fn):
+ def decorator(cls):
"""Function (method) binding."""
- def wrapper(cls, *args, **kwargs):
- """Arguments binding, decorated function (method) call."""
- for ctrl_name, controller in ctrls_kwargs.iteritems():
- prev_ctrl_url = getattr(cls, ATTR_CTRL_URL, '/')
- if prev_ctrl_url != '/':
- prev_ctrl_url += '/'
- setattr(cls, ctrl_name, controller(prev_ctrl_url + ctrl_name))
- # Wrapper's retval is the value returned by the method of the same
- # name taken from the base class.
- return getattr(cls.__base__, fn.func_name)(cls, *args, **kwargs)
+ # Take all specified subcontrollers and mount them.
+ if ctrls_dict and type(ctrls_dict is dict):
+ ctrls_kwargs.update(ctrls_dict)
+ for base_name, controller in ctrls_kwargs.iteritems():
+ # Branch according to whether a pair of controllers is used
+ # instead of a single controller.
+ if type(controller) in (tuple, list) and len(controller) == 2:
+ # 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))
+ else:
+ # 2) Single controller.
+ if type(controller) in (tuple, list):
+ controller = controller[0]
+ setattr(cls, base_name, controller())
# Decorator's retval is the value returned by the wrapper.
- return wrapper
+ return cls
- # Returned value is the value returned by the decorator.
+ # Retval is the value returned by the decorator.
return decorator
+# def decorator(fn):
+# """Function (method) binding."""
+# def wrapper(object, *args, **kwargs):
+# """Arguments binding, decorated function (method) call."""
+#
+# # Wrapper's retval is the value returned by the method of the same
+# # name taken from the base class.
+#
+# if hasattr(object, '__base__'):
+# # `object' is a class.
+# base_fn = getattr(object.__base__, fn.func_name)
+# else:
+# # `object' is an instance.
+# base_fn = getattr(object.__class__.__base__, fn.func_name)
+# retval = base_fn(object, *args, **kwargs)
+#
+# # Take all specified subcontrollers and mount them.
+# if ctrls_dict and type(ctrls_dict is dict):
+# ctrls_kwargs.update(ctrls_dict)
+# for base_name, controller in ctrls_kwargs.iteritems():
+# # Branch according to whether a pair of controllers is used
+# # instead of a single controller.
+# if type(controller) in (tuple, list) and len(controller) == 2:
+# # 1) Pair of controllers.
+# cmd_name = base2CmdCtrl(base_name)
+# setattr(object, base_name, controller[0]())
+# setattr(object, cmd_name, controller[1](base_name=base_name))
+# else:
+# # 2) Single controller.
+# if type(controller) in (tuple, list):
+# controller = controller[0]
+# setattr(object, base_name, controller())
+# return retval
+#
+# # Decorator's retval is the value returned by the wrapper.
+# return wrapper
+#
+# # Retval is the value returned by the decorator.
+# return decorator
+
+
-def forPageShowOnly(template, allowed_methods = ('GET', 'POST'), failback = '',
- use_referer = True):
+ATTR_REDIR_TARGET = 'REDIR_TARGET'
+
+
+def forPageShowOnly(template='', allowed_methods=('GET', 'POST'),
+ redirect='', use_referrer=True):
"""Decorator to show page incl. check of the request's method.
Keyword arguments:
template Which template to internally use with @expose().
allowed_methods Tuple listing all supported methods (e.g. GET or POST).
- failback Where to redirect on failure.
- use_referer On redirect call, try to use referer.
+ redir_target Where to redirect on failure. If omitted, it can
+ 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.
+ use_referrer On redirect call, try to use referrer first.
"""
if type(allowed_methods) not in (tuple, list):
@@ -77,11 +154,12 @@ def forPageShowOnly(template, allowed_methods = ('GET', 'POST'), failback = '',
"""Arguments binding, decorated function (method) call."""
if request.method not in allowed_methods:
flash(_('Unsupported method of the request.'), status='error')
- redir_url = failback
- if use_referer and request.referer:
- redir_url = request.referer
+ redir_url = redir_target
+ if use_referrer and request.referrer:
+ redir_url = request.referrer
elif not redir_url:
- redir_url = getattr(fn.__self__, ATTR_CTRL_URL, '/')
+ redir_url = getattr(fn.__self__, ATTR_REDIR_TARGET,
+ LUCI_APP_PREFIX)
redirect(redir_url)
else:
return fn(self, *args, **kwargs)
@@ -89,11 +167,11 @@ def forPageShowOnly(template, allowed_methods = ('GET', 'POST'), failback = '',
# Decorator's retval is the value returned by the wrapper.
return wrapper
- # Returned value is the value returned by the decorator.
+ # Retval is the value returned by the decorator.
return decorator
-def forPageShowWithUrlCorrection(template):
+def forPageShowWithUrlCorrection(template=''):
"""Decorator to show page incl. stripping off unsupported kwargs from URL.
Obviously, the only supported method is GET.
@@ -109,7 +187,7 @@ def forPageShowWithUrlCorrection(template):
"""Arguments binding, decorated function (method) call."""
if request.method != 'GET':
flash(_('Unsupported method of the request.'), status='error')
- redirect(url(request.path, **kwargs))
+ redirect(request.path, **kwargs)
else:
redir = False
# Get all the keyword arguments of original function.
@@ -122,29 +200,40 @@ def forPageShowWithUrlCorrection(template):
del kwargs[kwarg]
redir = True
if redir:
- redirect(url(request.path, **kwargs))
+ redirect(request.path, **kwargs)
else:
- return fn(self, *args, **kwargs)
+ return fn(self, **kwargs)
# Decorator's retval is the value returned by the wrapper.
return wrapper
- # Returned value is the value returned by the decorator.
+ # Retval is the value returned by the decorator.
return decorator
-def forImmediateRedirect(allowed_methods = ('GET', 'POST'), failback = '',
- use_referer = True):
- """Decorator to redirect immediatelly after handling the request.
+def forImmediateRedirect(**outer_kwargs):
+ """Decorator to redirect immediately after handling the request.
Check of the request's method is applied.
- Keyword arguments:
+ Keyword arguments (not stated directly as kwargs are better in this case):
allowed_methods Tuple listing all supported methods (e.g. GET or POST).
- failback Where to redirect on failure.
- use_referer On redirect call, try to use referer.
+ Default: ('GET', 'POST')
+ redir_target Where to redirect. If omitted, it can 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.
+ Default: ''
+ use_referrer On redirect call, try to use referrer first.
+ Default: True
+
+ Values of these keyword arguments (excluding allowed_methods) can be changed
+ additionally by the decorated method, according to its return value, which
+ 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]
@@ -153,19 +242,25 @@ def forImmediateRedirect(allowed_methods = ('GET', 'POST'), failback = '',
@expose()
def wrapper(self, *args, **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')
else:
- fn(self, *args, **kwargs)
- redir_url = failback
- if use_referer and request.referer:
- redir_url = request.referer
+ override_kwargs = fn(self, *args, **kwargs)
+ # If retval of decorated function is dict, override original
+ # arguments with values from this dict.
+ if type(override_kwargs) is dict:
+ inner_kwargs.update(override_kwargs)
+ redir_url = inner_kwargs.get('redir_target', '')
+ if inner_kwargs.get('use_referrer', True) and request.referrer:
+ redir_url = request.referrer
elif not redir_url:
- redir_url = getattr(self, ATTR_CTRL_URL, '/')
+ redir_url = getattr(self, ATTR_REDIR_TARGET, LUCI_APP_PREFIX)
redirect(redir_url)
# Decorator's retval is the value returned by the wrapper (no retval).
return wrapper
- # Returned value is the value returned by the decorator.
+ # Retval is the value returned by the decorator.
return decorator
+
diff --git a/luci/controllers/root.py b/luci/controllers/root.py
index fd6ee74..9ab430d 100644
--- a/luci/controllers/root.py
+++ b/luci/controllers/root.py
@@ -1,28 +1,24 @@
# -*- coding: utf-8 -*-
+
"""Main Controller"""
-from tg import expose, flash, require, url, request, redirect
+from tg import expose, flash, require, url, request, redirect, app_globals, tmpl_context
from tg import tmpl_context
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from catwalk.tg2 import Catwalk
from repoze.what import predicates
from sys import stderr
-from luci.lib.base import BaseController, LuciSubController
-from luci.model import DBSession, metadata
+from luci.controllers.decorators import *
+from luci.lib.base import BaseController
+
from luci.controllers.error import ErrorController
-from luci import model
from luci.controllers.secure import SecureController
+
+from luci.model import DBSession, metadata
+from luci import model
#from luci.widgets.fdom_add_form import create_fdom_add_form
-from luci.lib.ricci_helpers import send_batch_to_hosts
-from luci.lib.ricci_communicator import RicciCommunicator
-import luci.lib.ricci_queries as rq
-from luci.lib.form_helpers import string2id, id2string
-from luci.controllers.decorators import *
-from luci.controllers.fdom import FailoverController
-from luci.controllers.fence import FenceController
-from luci.controllers.node import NodeController
-from luci.controllers.service import ServiceController
+
from luci.widgets.add_system_form import create_add_system_form
from luci.widgets.add_existing_form import create_add_existing_form
from luci.widgets.add_user_form import create_add_user_form
@@ -32,6 +28,11 @@ from luci.widgets.create_cluster_form import create_cluster_form
__all__ = ['RootController']
+# Imports into module's namespace.
+scheme = app_globals.scheme
+
+
+(a)mountSubControllers(scheme.getRootMounts())
class RootController(BaseController):
"""
The root controller for the luci application.
@@ -47,18 +48,19 @@ class RootController(BaseController):
"""
+ def __call__(self, environ, start_response):
+ """Invoke the Controller"""
+ tmpl_context.title_list = []
+ tmpl_context.title_used = dict()
+ tmpl_context.title_used.setdefault(False)
+ return BaseController.__call__(self, environ, start_response)
+
# SUBCONTROLLERS
secc = SecureController()
admin = Catwalk(model, DBSession)
error = ErrorController()
- @mountLuciSubControllers(failovers = FailoverController,
- fences = FenceController,
- nodes = NodeController,
- services = ServiceController)
- def __new__(cls, *args, **kwargs): pass
-
# METHODS OF THE ROOT CONTROLLER
@@ -148,60 +150,3 @@ class RootController(BaseController):
flash(_('We hope to see you soon!'))
redirect(came_from)
-
- # Create cluster part.
-
- @forPageShowWithUrlCorrection('luci.templates.create')
- def create(self, name=""):
- return dict()
-
- @expose()
- def cluster_create(self, **kw):
- errors = ()
- cluster_name = kw.get('clustername')
- enable_storage = kw.get('enable_storage')
- reboot_nodes = kw.get('reboot_nodes')
- download_pkgs = kw.get('download_pkgs')
-
- nodes = [
- [ kw.get('__SYSTEM0_addr'), kw.get('__SYSTEM0_passwd') ],
- [ kw.get('__SYSTEM1_addr'), kw.get('__SYSTEM1_passwd') ],
- [ kw.get('__SYSTEM2_addr'), kw.get('__SYSTEM2_passwd') ]
- ]
- node_list = [ i[0] for i in nodes ]
- stderr.write('nodes: %s\n' % str(node_list))
-
- if not cluster_name:
- flash(_('No cluster name was given'))
- redirect('/create')
- if len(cluster_name) > 15:
- flash(_('Cluster names must be less than 16 characters long'))
- redirect('/create')
-
- for node in nodes:
- rc = RicciCommunicator(node[0], enforce_trust=False)
- rc.trust()
- rc.auth(node[1])
- if not rc.authed():
- errors.append('Authentication to node %s failed' % node[0])
- break
- else:
- rc = RicciCommunicator(node[0])
-
- cur_cluster_name = rc.cluster_info()[0]
- if cur_cluster_name:
- errors.append('%s is already a member of a cluster %s' \
- % (node[0], cur_cluster_name))
- break
-
- if len(errors) > 0:
- flash('The following errors occurred: %s' % str(errors))
- redirect("/create")
-
- ret = send_batch_to_hosts(node_list, 10, rq.create_cluster,
- 'rhel5', cluster_name, cluster_name,
- node_list, True, True, enable_storage, False,
- download_pkgs, None, reboot_nodes)
-
- redirect('/create')
-
diff --git a/luci/controllers/scheme.py b/luci/controllers/scheme.py
new file mode 100644
index 0000000..7b3c5d0
--- /dev/null
+++ b/luci/controllers/scheme.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+"""Constants and functions regarding the (logical) scheme of the Luci."""
+
+__all__ = ['LuciScheme']
+
+
+class LuciScheme:
+
+ # Static URL prefix (excluding host part and slash both at the beginning and
+ # at the end). E.g. 'luci' for Luci application starting at
+ # <http://example.com/luci/>.
+ APP_PREFIX = u''
+
+
+ # First level of the scheme.
+
+ CLUSTERS = u'clusters'
+ USERS = u'users'
+ STORAGE = u'storage'
+ SYSTEMS = u'systems'
+
+ @classmethod
+ def getRootMounts(cls):
+ """Defined as a method to avoid circular."""
+ # TODO: getter?
+ from luci.controllers.cluster import ClusterController, \
+ ClusterCmdController
+ return {
+ cls.CLUSTERS: (ClusterController, ClusterCmdController)
+# cls.USERS: (UsersController, UsersCmdController],
+# cls.STORAGE: (StorageController, StorageCmdController),
+# cls.SYSTEMS: (SystemsController, SystemsCmdController)
+ }
+
+
+ # Second level of the scheme -- `Clusters'.
+
+ CLUSTER_NODES = u'nodes'
+ CLUSTER_SERVICES = u'services'
+ CLUSTER_FAILOVERS = u'failovers'
+ CLUSTER_FENCES = u'fences'
+
+ @classmethod
+ def getCertainClusterMounts(cls):
+ from luci.controllers.cluster_part import *
+ return {
+ cls.CLUSTER_NODES: (NodeController, NodeCmdController),
+ cls.CLUSTER_SERVICES: (ServiceController, ServiceCmdController),
+ cls.CLUSTER_FAILOVERS: (FailoverController, FailoverCmdController),
+ cls.CLUSTER_FENCES: (FenceController, FenceCmdController)
+ }
+
+
+ """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/app_globals.py b/luci/lib/app_globals.py
index 0bd3089..7c76c25 100644
--- a/luci/lib/app_globals.py
+++ b/luci/lib/app_globals.py
@@ -2,6 +2,10 @@
"""The application's Globals object"""
+from luci.lib.form_utils import FormUtils
+from luci.controllers.scheme import LuciScheme
+from luci.lib.demo_data import ClusterData
+
__all__ = ['Globals']
@@ -14,5 +18,8 @@ class Globals(object):
"""
def __init__(self):
- """Do nothing, by default."""
- pass
+ """Prepare some common static functions, constants etc."""
+ self.form_utils = FormUtils
+ self.scheme = LuciScheme
+ self.data = ClusterData()
+
diff --git a/luci/lib/base.py b/luci/lib/base.py
index e5af5b0..764af54 100644
--- a/luci/lib/base.py
+++ b/luci/lib/base.py
@@ -2,14 +2,22 @@
"""The base Controller API."""
-from tg import TGController, tmpl_context
+from tg import TGController, tmpl_context, flash, app_globals
#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']
-__all__ = ['BaseController', 'LuciSubController']
+
+# Imports into module's namespace.
+form_utils = app_globals.form_utils
class BaseController(TGController):
@@ -32,30 +40,68 @@ class BaseController(TGController):
return TGController.__call__(self, environ, start_response)
-class LuciSubController(BaseController):
- """Base class for Luci's subcontrollers.
-
- Each such subcontroller expect one argument of instantiation, 'ctrl_url',
- that should contain url part of this subcontroller.
+class Subcontroller(object):
+ """Base class for subcontrollers.
- E.g. for subcontroller handling URLs 'example.com/movies/...', it should
- be set to 'movies'.
-
- This can be done automatically using mountLuciSubControllers() decorator.
-
- This 'ctrl_url' argument is stored to attribute (of the same name)
- of the inherited class.
+ Keyword arguments passed on object creation are automatically moved
+ inside the object as its attributes.
"""
- def __new__(cls, ctrl_url, *args, **kwargs):
+ def __new__(cls, *args, **kwargs):
# If the class that invoked this method is directly inherited from this
# class, set 'ctrl_url' attribute and delegate this invocation upwards
# (omitting the level of LuciSubController class).
# Otherwise, delegate the invocation directly upwards.
- if cls.__base__.__name__ == 'LuciSubController':
- cls.ctrl_url = ctrl_url
- return BaseController.__new__(cls, *args, **kwargs)
+ 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)
+
+class SubcontrollerApplyMixin:
+ """Mixin that adds 'apply' method to the derived controller."""
+
+ @forImmediateRedirect(allowed_methods = ('GET', 'POST'))
+ def apply(self, name=None, **kwargs):
+ """Handle applying a command over selected node(s).
+
+ Keyword arguments:
+ cmd Identifier of the command (see 'NodeCommands.cmds'
+ attribute).
+ name Name of the node to be used (if GET method is used).
+ kwargs Dict of nodes to be used (if POST method is used).
+
+ """
+ cmd_set = False
+ cmds_used = set(kwargs).intersection(self._apply_cmds.cmds)
+ if len(cmds_used) == 1:
+ cmd_set = True
+ cmd = cmds_used.pop()
+ kwargs.pop(cmd)
+
+ use_referrer = True
+ if not cmd_set:
+ # If Luci's templates are consistent, this should never happen.
+ msg = VerboseStrings.INTERNAL_ERROR
+ status = 'error'
+ else:
+ if name:
+ which = [name]
+ else:
+ which = [form_utils.id2String(s) for s in kwargs.iterkeys()]
+
+ if not which:
+ msg = VerboseStrings.NOTHING_CHOSEN
+ status ='warning'
+ else:
+ retval = self._apply_cmds.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)
diff --git a/luci/lib/demo_data.py b/luci/lib/demo_data.py
new file mode 100644
index 0000000..b70ebc2
--- /dev/null
+++ b/luci/lib/demo_data.py
@@ -0,0 +1,417 @@
+# -*- coding: utf-8 -*-
+"""Static demo data."""
+
+__all__ = ['ClusterData']
+
+
+class ClusterData:
+ """Class representation of overall demo data."""
+ def __init__(self):
+ self.clusters = {'ClusterOne': ClusterOne(),
+ 'ClusterTwo': ClusterTwo(),
+ 'ClusterThree': ClusterThree()}
+
+
+# Certain clusters.
+
+class Cluster:
+ """Base class for certain cluster."""
+
+ NODE_ACTIVE = '0'
+ NODE_UNKNOWN = '1'
+ NODE_INACTIVE = '2'
+
+
+class ClusterOne(Cluster):
+ """Data for 'ClusterOne'."""
+ def __init__(self):
+ self.nodes = \
+ {'NodeAlpha': {'ip': '144.92.235.11',
+ 'serviceload': 10,
+ 'status': self.NODE_ACTIVE,
+ 'services': ('Service W'),
+ 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'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}},
+ '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}},
+ '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}},
+ '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}}}
+
+ self.services = \
+ {'Service W': {'running': True,
+ 'autostart': True,
+ 'failover': 'Failover1',
+ 'node': 'NodeGamma',
+ 'resources': {'Res A': ('Script', False)}},
+ 'Service Y': {'running': False,
+ 'autostart': False,
+ #'failover': None,
+ 'node': 'NodeAlpha',
+ 'resources': {'Res A': ('Script', False)}},
+ 'Service X': {'running': True,
+ 'autostart': True,
+ 'failover': 'Failover2',
+ 'node': 'NodeBeta',
+ 'resources': {'Resource1': ('Script',False),
+ 'Resource2': ('Apache',True),
+ 'Resource3': ('LVM',False),
+ 'Resource4': ('IP Address',True),
+ 'Resource5': ('GFS File System',False)}},
+ 'Service Z': {'running': False,
+ 'autostart': True,
+ #'failover': None,
+ 'node': 'NodeDelta',
+ 'resources': {'Res A': ('Script', False)}}}
+
+ self.failovers = \
+ {'Failover1':{'prioritized': False,
+ 'restricted': True,
+ 'failback': False,
+ 'services': ('Service Y',
+ 'Service X'),
+ 'nodes': {'NodeAlpha': 1,
+ 'NodeBeta': 2}},
+ 'Failover2':{'prioritized': True,
+ 'restricted': True,
+ 'failback': False,
+ 'services': ('Service X',
+ 'Service Y',
+ 'Service Z'),
+ 'nodes': {'NodeAlpha': 1,
+ 'NodeBeta': 2,
+ 'NodeEpsilon': 0}},
+ 'FDOM a': {'prioritized': True,
+ 'restricted': False,
+ 'failback': False,
+ 'services': ('Service W',
+ 'Service Z'),
+ 'nodes': {'NodeBeta': 1,
+ 'NodeGamma': 0}},
+ 'FDOM b': {'prioritized': True,
+ 'restricted': False,
+ 'failback': False,
+ 'services': ('Service Y'),
+ 'nodes': {'NodeDelta': 0,
+ 'NodeGamma': 0}},
+ 'FDOM c': {'prioritized': True,
+ 'restricted': True,
+ 'failback': True,
+ 'services': ('Service Z',
+ 'Service X',
+ 'Service W'),
+ 'nodes': {'NodeAlpha': 1,
+ 'NodeEpsilon': 0}}}
+
+ self.fences = \
+ {'Fence A':{'type': 'iLO',
+ 'host': 'hostname.host.org',
+ 'ip': '123.123.78.90',
+ 'port': 6666,
+ 'nodes': {'NodeDelta': 1}},
+ 'Fence B':{'type': 'APC Power Device',
+ 'host': 'fenceb.host.org',
+ 'ip': '123.123.78.11',
+ 'port': 6667,
+ 'nodes': {'NodeAlpha': 1,
+ 'NodeDelta': 2}},
+ 'Fence C':{'type': 'Virtual Machine',
+ 'host': 'fencec.host.org',
+ 'ip': '123.123.78.156',
+ 'port': 11023,
+ 'nodes': {'NodeAlpha': 1,
+ 'NodeBeta': 2,
+ 'NodeGamma': 2,
+ 'NodeDelta': 2,
+ 'NodeEpsilon': 2}},
+ 'Fence D':{'type': 'SCSI Reservation',
+ 'host': 'fenced.host.org',
+ 'ip': '123.123.78.35',
+ 'port': 5487,
+ 'nodes': {'NodeAlpha': 2,
+ 'NodeBeta': 1,
+ 'NodeGamma': 2}}}
+
+
+class ClusterTwo(Cluster):
+ """Data for 'ClusterTwo'."""
+ def __init__(self):
+ self.nodes = \
+ {'NodeAlpha 2': {'ip': '144.92.235.11',
+ 'serviceload': 10,
+ 'status': self.NODE_ACTIVE,
+ 'services': ('Service W 2'),
+ 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'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}},
+ '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}},
+ '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}},
+ '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}}}
+
+ self.services = \
+ {'Service W 2': {'running': True,
+ 'autostart': True,
+ 'failover': 'Failover1 2',
+ 'node': 'NodeGamma 2',
+ 'resources': {'Res A': ('Script', False)}},
+ 'Service Y 2': {'running': False,
+ 'autostart': False,
+ #'failover': None,
+ 'node': 'NodeAlpha 2',
+ 'resources': {'Res A': ('Script', False)}},
+ 'Service X 2': {'running': True,
+ 'autostart': True,
+ 'failover': 'Failover2',
+ 'node': 'NodeBeta 2',
+ 'resources': {'Resource1': ('Script',False),
+ 'Resource2': ('Apache',True),
+ 'Resource3': ('LVM',False),
+ 'Resource4': ('IP Address',True),
+ 'Resource5': ('GFS File System',False)}},
+ 'Service Z 2': {'running': False,
+ 'autostart': True,
+ #'failover': None,
+ 'node': 'NodeDelta 2',
+ 'resources': {'Res A': ('Script', False)}}}
+
+ self.failovers = \
+ {'Failover1 2':{'prioritized': False,
+ 'restricted': True,
+ 'failback': False,
+ 'services': ('Service Y 2',
+ 'Service X 2'),
+ 'nodes': {'NodeAlpha 2': 1,
+ 'NodeBeta 2': 2}},
+ 'Failover2 2':{'prioritized': True,
+ 'restricted': True,
+ 'failback': False,
+ 'services': ('Service X 2',
+ 'Service Y 2',
+ 'Service Z 2'),
+ 'nodes': {'NodeAlpha 2': 1,
+ 'NodeBeta 2': 2,
+ 'NodeEpsilon 2': 0}},
+ 'FDOM a 2': {'prioritized': True,
+ 'restricted': False,
+ 'failback': False,
+ 'services': ('Service W 2',
+ 'Service Z 2'),
+ 'nodes': {'NodeBeta 2': 2,
+ 'NodeGamma 2': 0}},
+ 'FDOM b 2': {'prioritized': True,
+ 'restricted': False,
+ 'failback': False,
+ 'services': ('Service Y 2'),
+ 'nodes': {'NodeDelta 2': 0,
+ 'NodeGamma 2': 0}},
+ 'FDOM c 2': {'prioritized': True,
+ 'restricted': True,
+ 'failback': True,
+ 'services': ('Service Z 2',
+ 'Service X 2',
+ 'Service W 2'),
+ 'nodes': {'NodeAlpha 2': 1,
+ 'NodeEpsilon 2': 0}}}
+
+ self.fences = \
+ {'Fence A 2':{'type': 'iLO',
+ 'host': 'hostname.host.org',
+ 'ip': '123.123.78.90',
+ 'port': 6666,
+ 'nodes': {'NodeDelta 2': 1}},
+ 'Fence B 2':{'type': 'APC Power Device',
+ 'host': 'fenceb.host.org',
+ 'ip': '123.123.78.11',
+ 'port': 6667,
+ 'nodes': {'NodeAlpha 2': 1,
+ 'NodeDelta 2': 2}},
+ 'Fence C 2':{'type': 'Virtual Machine',
+ 'host': 'fencec.host.org',
+ 'ip': '123.123.78.156',
+ 'port': 11023,
+ 'nodes': {'NodeAlpha 2': 1,
+ 'NodeBeta 2': 2,
+ 'NodeGamma 2': 2,
+ 'NodeDelta 2': 2,
+ 'NodeEpsilon 2': 2}},
+ 'Fence D 2':{'type': 'SCSI Reservation',
+ 'host': 'fenced.host.org',
+ 'ip': '123.123.78.35',
+ 'port': 5487,
+ 'nodes': {'NodeAlpha 2': 2,
+ 'NodeBeta 2': 1,
+ 'NodeGamma 2': 2}}}
+
+
+class ClusterThree(Cluster):
+ """Data for 'ClusterThree'."""
+ def __init__(self):
+ self.nodes = \
+ {'NodeAlpha 3': {'ip': '144.92.235.11',
+ 'serviceload': 10,
+ 'status': self.NODE_ACTIVE,
+ 'services': ('Service W 3'),
+ 'cman': {'running': True, 'autostart': True},
+ 'rgmanager': {'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}},
+ '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}},
+ '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}},
+ '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}}}
+
+ self.services = \
+ {'Service W 3': {'running': True,
+ 'autostart': True,
+ 'failover': 'Failover1 3',
+ 'node': 'NodeGamma 3',
+ 'resources': {'Res A': ('Script', False)}},
+ 'Service Y 3': {'running': False,
+ 'autostart': False,
+ #'failover': None,
+ 'node': 'NodeAlpha 3',
+ 'resources': {'Res A': ('Script', False)}},
+ 'Service X 3': {'running': True,
+ 'autostart': True,
+ 'failover': 'Failover2 3',
+ 'node': 'NodeBeta 3',
+ 'resources': {'Resource1': ('Script',False),
+ 'Resource2': ('Apache',True),
+ 'Resource3': ('LVM',False),
+ 'Resource4': ('IP Address',True),
+ 'Resource5': ('GFS File System',False)}},
+ 'Service Z 3': {'running': False,
+ 'autostart': True,
+ #'failover': None,
+ 'node': 'NodeDelta 3',
+ 'resources': {'Res A': ('Script', False)}}}
+
+ self.failovers = \
+ {'Failover1 3':{'prioritized': False,
+ 'restricted': True,
+ 'failback': False,
+ 'services': ('Service Y 3',
+ 'Service X 3'),
+ 'nodes': {'NodeAlpha 3': 1,
+ 'NodeBeta 3': 2}},
+ 'Failover2 3':{'prioritized': True,
+ 'restricted': True,
+ 'failback': False,
+ 'services': ('Service X 3',
+ 'Service Y 3',
+ 'Service Z 3'),
+ 'nodes': {'NodeAlpha 3': 1,
+ 'NodeBeta 3': 2,
+ 'NodeEpsilon 3': 0}},
+ 'FDOM a 3': {'prioritized': True,
+ 'restricted': False,
+ 'failback': False,
+ 'services': ('Service W 3',
+ 'Service Z 3'),
+ 'nodes': {'NodeBeta 3': 2,
+ 'NodeGamma 3': 0}},
+ 'FDOM b 3': {'prioritized': True,
+ 'restricted': False,
+ 'failback': False,
+ 'services': ('Service Y 3'),
+ 'nodes': {'NodeDelta 3': 0,
+ 'NodeGamma 3': 0}},
+ 'FDOM c 3': {'prioritized': True,
+ 'restricted': True,
+ 'failback': True,
+ 'services': ('Service Z 3',
+ 'Service X 3',
+ 'Service W 3'),
+ 'nodes': {'NodeAlpha 3': 1,
+ 'NodeEpsilon 3': 0}}}
+
+ self.fences = \
+ {'Fence A 3':{'type': 'iLO',
+ 'host': 'hostname.host.org',
+ 'ip': '123.123.78.90',
+ 'port': 6666,
+ 'nodes': {'NodeDelta 3': 1}},
+ 'Fence B 3':{'type': 'APC Power Device',
+ 'host': 'fenceb.host.org',
+ 'ip': '123.123.78.11',
+ 'port': 6667,
+ 'nodes': {'NodeAlpha 3': 1,
+ 'NodeDelta 3': 2}},
+ 'Fence C 3':{'type': 'Virtual Machine',
+ 'host': 'fencec.host.org',
+ 'ip': '123.123.78.156',
+ 'port': 11023,
+ 'nodes': {'NodeAlpha 3': 1,
+ 'NodeBeta 3': 2,
+ 'NodeGamma 3': 2,
+ 'NodeDelta 3': 2,
+ 'NodeEpsilon 3': 2}},
+ 'Fence D 3':{'type': 'SCSI Reservation',
+ 'host': 'fenced.host.org',
+ 'ip': '123.123.78.35',
+ 'port': 5487,
+ 'nodes': {'NodeAlpha 3': 2,
+ 'NodeBeta 3': 1,
+ 'NodeGamma 3': 2}}}
+
+
diff --git a/luci/lib/form_utils.py b/luci/lib/form_utils.py
new file mode 100644
index 0000000..8f48b20
--- /dev/null
+++ b/luci/lib/form_utils.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+
+"""Utilities used in connection to HTML forms."""
+
+from base64 import b64encode, b64decode
+from string import maketrans
+
+__all__ = ['FormUtils']
+
+
+class FormUtils:
+
+ STARTING_CHAR = u'L' # First character has to be [A-Za-z].
+
+ B64_ORIG_CHARS = u'=+/'
+ B64_CONV_CHARS = u'-_:'
+
+ TRANS_TABLE_TO_CONV = maketrans(B64_ORIG_CHARS, B64_CONV_CHARS)
+ TRANS_TABLE_FROM_CONV = maketrans(B64_CONV_CHARS, B64_ORIG_CHARS)
+
+
+ @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.
+
+ The implementation uses base64 encoding with some other additions;
+ references:
+ http://www.w3.org/TR/xhtml1/#C_8
+ http://www.w3.org/TR/html4/types.html#h-6.2
+ http://tools.ietf.org/html/rfc3548.html
+
+ Keyword arguments:
+ s String to convert.
+
+ """
+ encoded = b64encode(s.encode('utf-8'))
+ return cls.STARTING_CHAR + encoded.translate(cls.TRANS_TABLE_TO_CONV)
+
+
+ @classmethod
+ def id2String(cls, s):
+ """Receive the string previously encoded by string2id().
+
+ Keyword arguments:
+ s String to convert back to human readable format.
+ """
+ back = b64decode(str(s)[1:].translate(cls.TRANS_TABLE_FROM_CONV))
+ return back.decode('utf-8')
+
+
+ @staticmethod
+ def compactString(s, maxlong, append='...'):
+ """For string longer than maxlong, shorten it and append e.g. '...'.
+
+ Keyword arguments:
+ s String to be `compacted'.
+ maxlong 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,
+ original string otherwise.
+
+ """
+ if len(s) > maxlong:
+ return s[:maxlong] + append
+ else:
+ return s
diff --git a/luci/lib/helpers.py b/luci/lib/helpers.py
index f00ecca..a2ab731 100644
--- a/luci/lib/helpers.py
+++ b/luci/lib/helpers.py
@@ -1,5 +1,21 @@
# -*- coding: utf-8 -*-
-"""WebHelpers used in luci."""
+"""Helpers used in luci."""
-from webhelpers import date, feedgenerator, html, number, misc, text
+#from webhelpers import date, feedgenerator, html, number, misc, text
+
+
+__all__ = ['urlList2String']
+
+def urlList2String(*url_list):
+ """Converts list of URL's parts to string begging with '/'.
+
+ Keyword arguments:
+ url_list List of URL's parts.
+
+ """
+
+ retval = u'/'.join(url_list)
+ if not retval.startswith(u'/'):
+ retval = u'/' + retval
+ return retval
diff --git a/luci/lib/strings.py b/luci/lib/strings.py
new file mode 100644
index 0000000..f81fe25
--- /dev/null
+++ b/luci/lib/strings.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+
+"""Common strings and short sentences presented to the user."""
+
+from pylons.i18n import ugettext as _
+
+__all__ = ['TitleStrings', 'VerboseStrings']
+
+
+class TitleStrings:
+ """Strings used in the title of the page.
+
+ Final form of the title adjusted by 'luci.templates.title'.
+ """
+
+ # Following pairs are used for general items listing or certain entity
+ # details displaying (thus, the '%s' is alias for the name of such entity)
+ # respectively.
+
+ CLUSTERS = _('clusters')
+ CLUSTER = _('cluster %s')
+
+ NODES = _('nodes')
+ NODE = _('node %s')
+
+ SERVICES = _('services')
+ SERVICE = _('service %s')
+
+ FAILOVERS = _('failovers')
+ FAILOVER = _('failover %s')
+
+ FENCES = _('fences')
+ FENCE = _('fence %s')
+
+
+class VerboseStrings:
+ """Strings used for error/warning/notice reporting to the user."""
+
+ BAD_COMMAND_REQUEST = _('Bad command request.')
+ INTERNAL_ERROR = _('Internal error.')
+ NOTHING_CHOSEN = _('Nothing was chosen.')
+
\ No newline at end of file
diff --git a/luci/public/css/fdom.css b/luci/public/css/failover.css
similarity index 100%
rename from luci/public/css/fdom.css
rename to luci/public/css/failover.css
diff --git a/luci/public/css/style.css b/luci/public/css/style.css
index 6531ca3..5dcb279 100644
--- a/luci/public/css/style.css
+++ b/luci/public/css/style.css
@@ -1,7 +1,7 @@
@import url("shared.css");
@import url("node.css");
@import url("service.css");
-@import url("fdom.css");
+@import url("failover.css");
@import url("fence.css");
@import url("create.css");
diff --git a/luci/public/js/fdom.js b/luci/public/js/failover.js
similarity index 100%
rename from luci/public/js/fdom.js
rename to luci/public/js/failover.js
diff --git a/luci/templates/cluster_part/__init__.py b/luci/templates/cluster_part/__init__.py
new file mode 100644
index 0000000..ea8c54a
--- /dev/null
+++ b/luci/templates/cluster_part/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+"""Templates package for `clusters' part."""
diff --git a/luci/templates/fdom.html b/luci/templates/cluster_part/failover.html
similarity index 76%
rename from luci/templates/fdom.html
rename to luci/templates/cluster_part/failover.html
index d7f0b25..6978352 100644
--- a/luci/templates/fdom.html
+++ b/luci/templates/cluster_part/failover.html
@@ -8,16 +8,18 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
- <title>Failover domains</title>
- <script type="text/javascript" src="${tg.url('/js/fdom.js')}"></script>
+ <title>${title()}</title>
+ <script type="text/javascript" src="${tg.url('/js/failover.js')}"></script>
</head>
-<body onload="onLoad()">
+<body onload="onLoad()"
+ py:with="cluster_data = tmpl_context.cluster_data;
+ form_utils = app_globals.form_utils">
- <form action="${tg.url(ctrl_url + '/apply')}" method="post">
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
<div id="toolbar">
- <input type="submit" name="cmd_delete" value="delete" class="toolbar_button" id="tb_delete"/>
- <a href="${tg.url(ctrl_url + '/add')}" class="toolbar_button" id="tb_add">add</a>
+ <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. -->
@@ -39,18 +41,18 @@
</thead>
<tbody>
<!--! List all the failover domains. -->
- <tr py:for="i, fdom in enumerate(fdoms)"
- py:attrs="fdom[0]==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
- py:with="identifier=string2id(fdom[0])">
+ <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(ctrl_url + '?name=' + fdom[0])}">${fdom[0]}</a></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="fdom[1]" src="${tg.url('/images/dot.png')}" alt="*" /></td>
- <td class="fdom_tlist_restricted"><img py:if="fdom[2]" src="${tg.url('/images/dot.png')}" alt="*" /></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>
</tbody>
@@ -59,32 +61,32 @@
</form>
<!--! DETAILS SECTION. -->
- <div id="details" py:choose="details">
+ <div id="details" py:choose="name">
<py:when test="None">
<div id="details_header">
- <div id="not_selected" py:choose="len(fdoms)">
+ <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: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(ctrl_url + '/apply?cmd_delete&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</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>
</div>
<!--! DETAILS - attributes section. -->
<div class="details_section">
<div class="details_inner">
- <form action="${tg.url(ctrl_url + '/update_props')}" method="post">
- <input type="hidden" name="name" value="${string2id(name)}"/>
+ <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>
@@ -121,7 +123,7 @@
<!--! DETAILS - services section. -->
<div class="details_section">
- <a href="${tg.url(ctrl_url + '/service')}" class="float_button">Add a Service</a>
+ <a href="${tg.url(cmd_url + '/service')}" class="float_button">Add a Service</a>
<h4>Services</h4>
<div class="details_inner">
<table id="fdom_tservices">
@@ -130,7 +132,7 @@
<!--! Note: "('Service')" is syntactically a string, not a tuple,
so this case is also solved. -->
<tr py:for="service in (type(details['services']) is tuple and details['services'] or (details['services'],) )" class="grid_row">
- <py:with vars="running = services.has_key(service) and (services[service]['running'])">
+ <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>
@@ -145,8 +147,8 @@
<!--! DETAILS - members section. -->
<div class="details_section">
- <form action="${tg.url(ctrl_url + '/update_members')}" method="post">
- <input type="hidden" name="name" value="${string2id(name)}"/>
+ <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">
@@ -162,23 +164,23 @@
</thead>
<tbody>
<!--! List all the nodes (of the current cluster). -->
- <tr py:for="node, node_dict in nodes.iteritems()" class="grid_row">
+ <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', nodeconst.NODE_UNKNOWN)">
+ <py:choose test="node_dict.get('status', cluster_data.NODE_UNKNOWN)">
<!--! 1) Node is active. -->
- <py:when test="nodeconst.NODE_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="nodeconst.NODE_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="nodeconst.NODE_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>
@@ -187,7 +189,7 @@
</py:choose>
<!--! Branch according to whether the current node is a member
of this failover domain. -->
- <py:choose test="details['nodes'].has_key(node)" py:with="identifier=string2id(node)">
+ <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">
diff --git a/luci/templates/fence.html b/luci/templates/cluster_part/fence.html
similarity index 64%
rename from luci/templates/fence.html
rename to luci/templates/cluster_part/fence.html
index 024f660..e70f425 100644
--- a/luci/templates/fence.html
+++ b/luci/templates/cluster_part/fence.html
@@ -8,16 +8,17 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
- <title>Fences</title>
+ <title>${title()}</title>
</head>
-<body>
+<body py:with="cluster_data = tmpl_context.cluster_data;
+ form_utils = app_globals.form_utils">
- <form action="${tg.url(ctrl_url + '/apply')}" method="post">
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
<div id="toolbar">
- <input type="submit" name="cmd_update" value="update" class="toolbar_button" id="tb_update"/>
- <input type="submit" name="cmd_delete" value="delete" class="toolbar_button" id="tb_delete"/>
- <a href="${tg.url(ctrl_url + '/add')}" class="toolbar_button" id="tb_add">add</a>
+ <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>
<!--! OVERVIEW SECTION. -->
@@ -39,28 +40,30 @@
</thead>
<tbody>
<!--! List all the fences. -->
- <tr py:for="i, fence in enumerate(fences)"
- py:attrs="fence[0]==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
- py:with="identifier=string2id(fence[0])">
+ <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="type(fence[2]) is not tuple" src="${tg.url('/images/global_11x11_black.png')}" alt="shared fence"/></td>
- <td class="main_id"><a href="${tg.url(ctrl_url + '?name=' + fence[0])}">${fence[0]}</a></td>
- <td class="fence_tlist_type">${fence[1]}</td>
+ <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="type(fence[2]) is tuple">
+ <py:choose test="local_fence">
<!--! 1) Local. -->
- <td py:when="True" class="fence_tlist_members">
- <py:choose test="fence[2][1].get('status', nodeconst.NODE_UNKNOWN)">
- <span py:when="nodeconst.NODE_ACTIVE" class="entity_ok">${fence[2][0]}</span>
- <span py:when="nodeconst.NODE_INACTIVE" class="entity_fail">${fence[2][0]}</span>
- <span py:when="nodeconst.NODE_UNKNOWN" class="entity_unknown">${fence[2][0]}</span>
+ <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">${fence[2]}</td>
+ <td py:otherwise="" class="fence_tlist_members">${len(fence_data['nodes'])}</td>
</py:choose>
- <td class="fence_tlist_ip">${fence[3]}</td>
+ <td class="fence_tlist_ip">${fence_data['ip']}</td>
<td class="table_space"></td>
</tr>
</tbody>
@@ -69,25 +72,25 @@
</form>
<!--! DETAILS SECTION. -->
- <div id="details" py:choose="details">
+ <div id="details" py:choose="name">
<py:when test="None">
<div id="details_header">
- <div id="not_selected" py:choose="len(fences)">
+ <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: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(ctrl_url + '/apply?cmd_delete&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
- <a href="${tg.url(ctrl_url + '/apply?cmd_update&name=' + name)}" id="dh_update" title="update"><span class="hide">update</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>
+ <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>
@@ -119,17 +122,17 @@
</thead>
<tbody>
<!--! List all the nodes connected with the current (shared) fence. -->
- <tr py:for="node, node_dict in nodes.iteritems()" class="grid_row" py:if="details['nodes'].has_key(node)">
+ <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', nodeconst.NODE_UNKNOWN)">
+ <py:choose test="node_dict.get('status', cluster_data.NODE_UNKNOWN)">
<!--! 1) Node is active. -->
- <py:when test="nodeconst.NODE_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="nodeconst.NODE_INACTIVE">
+ <py:when test="cluster_data.NODE_INACTIVE">
<td class="icon">
<img src="${tg.url('/images/exclamation.png')}" alt="Node has a problem." />
</td>
@@ -137,7 +140,7 @@
<td class="fence_tnodes_status">${node_dict.get('msg', _('Uknown problem'))}</td>
</py:when>
<!--! 3) Status of the node is unknown. -->
- <py:when test="nodeconst.NODE_UNKNOWN">
+ <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>
diff --git a/luci/templates/node.html b/luci/templates/cluster_part/node.html
similarity index 63%
rename from luci/templates/node.html
rename to luci/templates/cluster_part/node.html
index 4d25a94..ce0ce69 100644
--- a/luci/templates/node.html
+++ b/luci/templates/cluster_part/node.html
@@ -8,18 +8,19 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
- <title>Nodes</title>
+ <title>${title()}</title>
</head>
-<body>
+<body py:with="cluster_data = tmpl_context.cluster_data;
+ form_utils = app_globals.form_utils">
- <form action="${tg.url(ctrl_url + '/apply')}" method="post">
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
<div id="toolbar">
- <input type="submit" name="cmd_reboot" value="reboot" class="toolbar_button" id="tb_reboot" />
- <input type="submit" name="cmd_fence" value="fence" class="toolbar_button" id="tb_fence" />
- <input type="submit" name="cmd_leave" value="leave cluster" class="toolbar_button" id="tb_leave" />
- <input type="submit" name="cmd_delete" value="delete" class="toolbar_button" id="tb_delete" />
- <a href="${tg.url(ctrl_url + '/add')}" class="toolbar_button" id="tb_add">add</a>
+ <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>
<!--! OVERVIEW SECTION. -->
@@ -41,49 +42,49 @@
</thead>
<tbody>
<!--! List all the nodes. -->
- <tr py:for="i, node in enumerate(nodes)"
- py:attrs="node[0]==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
- py:with="identifier=string2id(node[0])">
+ <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[1]">
+ <py:choose test="node_data['status']">
<!--! 1) Node is active. -->
- <py:when test="nodeconst.NODE_ACTIVE">
+ <py:when test="cluster_data.NODE_ACTIVE">
<td class="icon"></td>
<td class="main_id">
- <a href="${tg.url(ctrl_url + '?name=' + node[0])}">
- <span class="entity_ok">${node[0]}</span>
+ <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="nodeconst.NODE_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(ctrl_url + '?name=' + node[0])}">
- <span class="entity_fail">${node[0]}</span>
+ <a href="${tg.url(base_url + '/' + entity_name)}">
+ <span class="entity_fail">${entity_name}</span>
</a>
</td>
- <td class="node_tlist_status">${node[4] and node[4] or _('Uknown problem')}</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="nodeconst.NODE_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(ctrl_url + '?name=' + node[0])}">
- <span class="entity_unknown">${node[0]}</span>
+ <a href="${tg.url(base_url + '/' + entity_name)}">
+ <span class="entity_unknown">${entity_name}</span>
</a>
</td>
- <td class="node_tlist_status">${node[4] and node[4] or _('Status uknown')}</td>
- </py:when>
+ <td class="node_tlist_status">${node_data.get('msg', _('Status uknown'))}</td>
+ </py:when>
</py:choose>
- <td class="node_tlist_load">${node[2]}</td>
- <td class="node_tlist_ip">${node[3]}</td>
+ <td class="node_tlist_load">${node_data['serviceload']}</td>
+ <td class="node_tlist_ip">${node_data['ip']}</td>
<td class="table_space"></td>
</tr>
</tbody>
@@ -92,39 +93,39 @@
</form>
<!--! DETAILS SECTION. -->
- <div id="details" py:choose="details">
+ <div id="details" py:choose="name">
<py:when test="None">
<div id="details_header">
- <div id="not_selected" py:choose="len(nodes)">
+ <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: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(ctrl_url + '/apply?cmd_reboot&name=' + name)}" id="dh_reboot" title="reboot"><span class="hide">reboot</span></a>
- <a href="${tg.url(ctrl_url + '/apply?cmd_fence&name=' + name)}" id="dh_fence" title="fence"><span class="hide">fence</span></a>
- <a href="${tg.url(ctrl_url + '/apply?cmd_leave&name=' + name)}" id="dh_leave" title="leave cluster"><span class="hide">leave cluster</span></a>
- <a href="${tg.url(ctrl_url + '/apply?cmd_delete&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
+ <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="nodeconst.NODE_ACTIVE">${_('OK')}</py:when>
- <py:when test="nodeconst.NODE_INACTIVE">${details.get('msg', _('Uknown problem'))}</py:when>
- <py:when test="nodeconst.NODE_UNKNOWN">${details.get('msg', _('Status uknown'))}</py:when>
+ <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(ctrl_url + '/service')}" class="float_button">Add a Service</a>
+ <a href="${tg.url(cmd_url + '/add_service')}" class="float_button">Add a Service</a>
<h4>Services</h4>
<div class="details_inner">
<table id="node_tservices">
@@ -133,7 +134,7 @@
<!--! Note: "('Service')" is syntactically a string, not a tuple,
so this case is also solved. -->
<tr py:for="service in (type(details['services']) is tuple and details['services'] or (details['services'],) )" class="grid_row">
- <py:with vars="running = services.has_key(service) and (services[service]['running'])">
+ <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>
@@ -148,7 +149,7 @@
<!--! DETAILS - failover domains section. -->
<div class="details_section">
- <a href="${tg.url(ctrl_url + '/service')}" class="float_button">Update settings</a>
+ <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">
@@ -161,7 +162,7 @@
</tr>
</thead>
<tbody>
- <tr py:for="fdom, fdom_dict in fdoms.iteritems()" class="grid_row"
+ <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>
@@ -175,7 +176,7 @@
<!--! DETAILS - fence devices section. -->
<div class="details_section">
- <a href="${tg.url(ctrl_url + '/service')}" class="float_button">Update settings</a>
+ <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">
@@ -188,7 +189,7 @@
</tr>
</thead>
<tbody>
- <tr py:for="fence, fence_dict in fences.iteritems()" class="grid_row"
+ <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>
@@ -202,7 +203,7 @@
<!--! DETAILS - cluster daemons section. -->
<div class="details_section">
- <a href="${tg.url(ctrl_url + '/service')}" class="float_button">Add a Service</a>
+ <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">
diff --git a/luci/templates/service.html b/luci/templates/cluster_part/service.html
similarity index 57%
rename from luci/templates/service.html
rename to luci/templates/cluster_part/service.html
index a38bfa1..a9cefee 100644
--- a/luci/templates/service.html
+++ b/luci/templates/cluster_part/service.html
@@ -8,16 +8,17 @@
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
- <title>Services</title>
+ <title>${title()}</title>
</head>
-<body>
+<body py:with="cluster_data = tmpl_context.cluster_data;
+ form_utils = app_globals.form_utils">
- <form action="${tg.url(ctrl_url + '/apply')}" method="post">
+ <form action="${tg.url(cmd_url + '/apply')}" method="post">
<div id="toolbar">
- <input type="submit" name="cmd_reboot" value="reboot" class="toolbar_button" id="tb_reboot" />
- <input type="submit" name="cmd_delete" value="delete" class="toolbar_button" id="tb_delete" />
- <a href="${tg.url(ctrl_url + '/add')}" class="toolbar_button" id="tb_add">add</a>
+ <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>
<!--! OVERVIEW SECTION. -->
@@ -39,21 +40,21 @@
</thead>
<tbody>
<!--! List all the services. -->
- <tr py:for="i, service in enumerate(services)"
- py:attrs="service[0]==name and {'class': 'chosen'} or (not i%2 and {'class': 'even'} or None)"
- py:with="identifier=string2id(service[0])">
+ <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[1]">
+ <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(ctrl_url + '?name=' + service[0])}">
- <span class="entity_ok">${service[0]}</span>
+ <a href="${tg.url(base_url + '/' + entity_name)}">
+ <span class="entity_ok">${entity_name}</span>
</a>
</td>
- <td class="service_tlist_status">${compactString(_('Running on %s') % service[3], 18)}</td>
+ <td class="service_tlist_status">${form_utils.compactString(_('Running on %s') % service_data['node'], 18)}</td>
</py:when>
<!--! 2) Service is not running. -->
<py:when test="False">
@@ -61,18 +62,18 @@
<img src="${tg.url('/images/exclamation.png')}" alt="Service has a problem." />
</td>
<td class="main_id">
- <a href="${tg.url(ctrl_url + '?name=' + service[0])}">
- <span class="entity_fail">${service[0]}</span>
+ <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[2] and _('Autostart not enabled') or
- (nodes.has_key(service[3]) and nodes[service[3]].get('msg', _('Unknown problem'))
- or _('Internal error'))">${compactString(msg, 18)}</td>
- </py:when>
+ 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)}</td>
+ </py:when>
</py:choose>
<td class="service_tlist_enabled"><input type="checkbox" disabled="disabled"/></td>
- <td class="service_tlist_fdom">${service[4] or _('No/default failover domain')}</td>
+ <td class="service_tlist_fdom">${service_data.get('failover', None) or _('No/default failover domain')}</td>
<td class="table_space"></td>
</tr>
</tbody>
@@ -81,43 +82,43 @@
</form>
<!--! DETAILS SECTION. -->
- <div id="details" py:choose="details">
+ <div id="details" py:choose="name">
<py:when test="None">
<div id="details_header">
- <div id="not_selected" py:choose="len(services)">
+ <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: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(ctrl_url + '/apply?cmd_delete&name=' + name)}" id="dh_delete" title="delete"><span class="hide">delete</span></a>
- <a href="${tg.url(ctrl_url + '/apply?cmd_update&name=' + name)}" id="dh_update" title="update"><span class="hide">update</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>
+ <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>
- <py:with vars="msg = 'bla'">
+ <py:with vars="msg = 'bla'">
<span class="details_header_info_label">status</span> <span class="details_header_info">${msg}</span>
<form style="display: inline; padding-left: 24px; ">
<select style="font-size: small;">
- <py:for each="node in nodes.iterkeys()" py:if="node != details['node']">
- <option value="string2id(node)">${node}</option>
- </py:for>
+ <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>
+ </form>
</py:with>
- </div>
+ </div>
<!--! DETAILS - resources. -->
<div class="details_section">
- <a href="${tg.url(ctrl_url + '/service')}" class="float_button">Manage Global Resources</a>
- <!--a href="${tg.url(ctrl_url + '/service')}" class="float_button">Create New Resource in this Service</a-->
+ <a href="${tg.url(cmd_url + '/service')}" 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">
diff --git a/luci/templates/master.html b/luci/templates/master.html
index aeba83d..f23faa2 100644
--- a/luci/templates/master.html
+++ b/luci/templates/master.html
@@ -7,6 +7,7 @@
<xi:include href="header.html" />
<xi:include href="sidebars.html" />
<xi:include href="footer.html" />
+ <xi:include href="title.html" />
<head py:match="head" py:attrs="select('@*')">
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title py:replace="''">Your title goes here</title>
@@ -21,10 +22,12 @@
<li><a href="${tg.url('/cluster')}">cluster</a></li>
<li><a href="${tg.url('/storage')}">storage</a></li>
<li><a href="${tg.url('/about')}" class="${('', 'active')[defined('page') and page==page=='about']}">About</a></li>
- <li><a href="${tg.url('/nodes')}">Nodes</a></li>
- <li><a href="${tg.url('/services')}">Services</a></li>
- <li><a href="${tg.url('/failovers')}">Failovers</a></li>
- <li><a href="${tg.url('/fences')}">Fences</a></li>
+ <py:if test="'cluster_url' in dir(tmpl_context)">
+ <li><a href="${tg.url(tmpl_context.cluster_url + '/nodes')}">Nodes</a></li>
+ <li><a href="${tg.url(tmpl_context.cluster_url + '/services')}">Services</a></li>
+ <li><a href="${tg.url(tmpl_context.cluster_url + '/failovers')}">Failovers</a></li>
+ <li><a href="${tg.url(tmpl_context.cluster_url +'/fences')}">Fences</a></li>
+ </py:if>
<span py:if="tg.auth_stack_enabled" py:strip="True">
<li py:if="not request.identity" id="login" class="loginlogout"><a href="${tg.url('/login')}">Login</a></li>
<li py:if="request.identity" id="login" class="loginlogout"><a href="${tg.url('/logout_handler')}">Logout</a></li>
diff --git a/luci/templates/title.html b/luci/templates/title.html
new file mode 100644
index 0000000..c49f5a9
--- /dev/null
+++ b/luci/templates/title.html
@@ -0,0 +1,5 @@
+<html xmlns:py="http://genshi.edgewall.org/"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ py:strip="">
+<py:def function="title">Luci | ${' > '.join(tmpl_context.title_list)}</py:def>
+</html>
14 years, 8 months
[luci] - Added support for the "Create a New Cluster"
by Chris Feist
commit 54ceddfeb08e6246eac7b260cc5964685123c8a6
Author: Chris Feist <cfeist(a)redhat.com>
Date: Thu Sep 17 17:29:08 2009 -0500
- Added support for the "Create a New Cluster"
- Submit buttons now don't cause a Turbogears Error
- Dynamic forms now work when adding more than one hostname
luci/controllers/root.py | 12 ++++++++----
luci/templates/.cluster.html.swp | Bin 0 -> 12288 bytes
luci/templates/.homebase.html.swp | Bin 0 -> 12288 bytes
luci/templates/cluster.html | 12 ++++++++++--
luci/templates/homebase.html | 28 +++++++++++++++++++++++++++-
luci/templates/master.html | 3 +--
luci/widgets/add_existing_form.py | 1 -
luci/widgets/add_system_form.py | 9 +++++----
luci/widgets/create_cluster_form.py | 21 +++++++++++++++++++++
luci/widgets/manage_systems_form.py | 11 +++++------
10 files changed, 77 insertions(+), 20 deletions(-)
---
diff --git a/luci/controllers/root.py b/luci/controllers/root.py
index 74c1123..fd6ee74 100644
--- a/luci/controllers/root.py
+++ b/luci/controllers/root.py
@@ -27,6 +27,7 @@ from luci.widgets.add_system_form import create_add_system_form
from luci.widgets.add_existing_form import create_add_existing_form
from luci.widgets.add_user_form import create_add_user_form
from luci.widgets.manage_systems_form import create_manage_systems_form
+from luci.widgets.create_cluster_form import create_cluster_form
__all__ = ['RootController']
@@ -72,7 +73,7 @@ class RootController(BaseController):
return dict(page='about')
@expose('luci.templates.homebase')
- def homebase(self, homebasepage='homebasepage'):
+ def homebase(self, homebasepage='homebasepage', **args):
if homebasepage == 'addsystem':
tmpl_context.form = create_add_system_form
elif homebasepage == 'addexisting':
@@ -82,11 +83,14 @@ class RootController(BaseController):
elif homebasepage == 'managesystems':
tmpl_context.form = create_manage_systems_form
- return dict(page='homebase',homebasepage=homebasepage)
+ return dict(page='homebase',homebasepage=homebasepage,args=args)
@expose('luci.templates.cluster')
- def cluster(self,clusterpage='clusterlist'):
- return dict(page='cluster', clusterpage=clusterpage)
+ def cluster(self,clusterpage='clusterlist', **args):
+ if clusterpage == 'createcluster':
+ tmpl_context.form = create_cluster_form
+
+ return dict(page='cluster', clusterpage=clusterpage)
@expose('luci.templates.storage')
def storage(self,storagepage='systemlist'):
diff --git a/luci/templates/.cluster.html.swp b/luci/templates/.cluster.html.swp
new file mode 100644
index 0000000..7aa344c
Binary files /dev/null and b/luci/templates/.cluster.html.swp differ
diff --git a/luci/templates/.homebase.html.swp b/luci/templates/.homebase.html.swp
new file mode 100644
index 0000000..07639ee
Binary files /dev/null and b/luci/templates/.homebase.html.swp differ
diff --git a/luci/templates/cluster.html b/luci/templates/cluster.html
index f99825f..3236ac3 100644
--- a/luci/templates/cluster.html
+++ b/luci/templates/cluster.html
@@ -23,8 +23,16 @@
</td>
<td valign="top">
<div class="mainpage">
- <b>Cluster</b><br/>
- ${clusterpage}
+ <div py:if="clusterpage == 'clusterlist'">
+ <h3>Choose a cluster to administer</h3>
+ <hr/>
+ </div>
+ <div py:if="clusterpage == 'createcluster'">
+ <div py:replace="tmpl_context.form()">Input Form</div>
+ </div>
+ <div py:if="clusterpage == 'configure'">
+ <h3>Choose a cluster to administer</h3>
+ </div>
</div>
</td>
</tr>
diff --git a/luci/templates/homebase.html b/luci/templates/homebase.html
index c6e9391..4d599b1 100644
--- a/luci/templates/homebase.html
+++ b/luci/templates/homebase.html
@@ -24,7 +24,33 @@
</td>
<td valign="top">
<div class="mainpage">
- <div py:if="homebasepage != 'homebasepage'">
+ <div py:if="homebasepage == 'managesystems'">
+ <h3>Manage Systems and Clusters</h3>
+ <hr/>
+ <fieldset><legend>Reauthenticate</legend>
+ <h3>Reauthenticate to Storage or Cluster Systems</h3><br/>
+ <div py:replace="tmpl_context.form()">Input Form</div>
+ </fieldset>
+ <fieldset><legend>Remove clusters and systems</legend>
+ <h3>Check storage systems and clusters to remove from the Luci management interface.</h3>
+ <h3>Clusters</h3>
+ <h3>Storage Systems</h3>
+ </fieldset>
+ </div>
+ <div py:if="homebasepage == 'addsystem'">
+ <h3>Add a System</h3>
+ <hr/>
+ <div py:replace="tmpl_context.form()">Input Form</div>
+ </div>
+ <div py:if="homebasepage == 'addexisting'">
+ <h3>Add an Existing Cluster</h3>
+ <hr/>
+ <p>Enter one node from the cluster you wish to add to the Luci management interface.</p>
+ <div py:replace="tmpl_context.form()">Input Form</div>
+ </div>
+ <div py:if="homebasepage == 'adduser'">
+ <h3>Add a User</h3>
+ <hr/>
<div py:replace="tmpl_context.form()">Input Form</div>
</div>
<div py:if="homebasepage == 'homebasepage'">
diff --git a/luci/templates/master.html b/luci/templates/master.html
index 95520a5..aeba83d 100644
--- a/luci/templates/master.html
+++ b/luci/templates/master.html
@@ -34,8 +34,7 @@
<div id="content">
<py:if test="defined('page')">
<div class="currentpage">
- Now Viewing: <span py:replace="page"/>
- </div>
+ </div>
</py:if>
<py:with vars="flash=tg.flash_obj.render('flash', use_js=False)">
<div py:if="flash" py:content="XML(flash)" />
diff --git a/luci/widgets/add_existing_form.py b/luci/widgets/add_existing_form.py
index a7f4974..1ade914 100644
--- a/luci/widgets/add_existing_form.py
+++ b/luci/widgets/add_existing_form.py
@@ -3,7 +3,6 @@ from tw.forms import TableForm, TextField, Label
class AddExistingForm(TableForm):
class fields(WidgetsList):
- mylabel = Label(text = 'Enter one node from the cluster you wish to add the Luci management interface.')
system_hostname = TextField()
root_password = TextField()
diff --git a/luci/widgets/add_system_form.py b/luci/widgets/add_system_form.py
index f1dffdc..8a0454c 100644
--- a/luci/widgets/add_system_form.py
+++ b/luci/widgets/add_system_form.py
@@ -1,10 +1,11 @@
from tw.api import WidgetsList
-from tw.forms import TableForm, TextField
+from tw.forms import TableForm, TextField, PasswordField
+import tw.dynforms as twd
-class AddSystemForm(TableForm):
- class fields(WidgetsList):
+class AddSystemForm(twd.GrowingTableForm):
+ class children(WidgetsList):
system_hostname = TextField()
- root_password = TextField()
+ root_password = PasswordField()
create_add_system_form = AddSystemForm("create_add_system_form")
diff --git a/luci/widgets/create_cluster_form.py b/luci/widgets/create_cluster_form.py
new file mode 100644
index 0000000..d17cd27
--- /dev/null
+++ b/luci/widgets/create_cluster_form.py
@@ -0,0 +1,21 @@
+from tw.api import WidgetsList
+from tw.forms import TableForm, TextField, PasswordField, RadioButtonList, RadioButton, CheckBox, CheckBoxTable
+import tw.dynforms as twd
+
+class HostList(twd.GrowingTableFieldSet):
+ class children(WidgetsList):
+ node_hostname = TextField()
+ root_password = PasswordField()
+
+class CreateClusterForm(TableForm):
+ class fields(WidgetsList):
+ cluster_name = TextField()
+ host_list = HostList(suppress_label = True)
+ download_packages = RadioButtonList(options=["Download Packages","Use locally installed packages"], suppress_label = True, default = "Download Packages");
+# use_locally_installed_packages = RadioButton();
+ cb = CheckBoxTable(options=["Enable Shared Storage Support", "Reboot nodes before joining cluster", "Check if node passwords are identical"], suppress_label = True, default = "Enable Shared Storage Support")
+# enable_shared_storage_support = CheckBox()
+# reboot_nodes_before_joining_cluster = CheckBox()
+# check_if_node_passwords_are_identical = CheckBox()
+create_cluster_form = CreateClusterForm("create_cluster_form")
+
diff --git a/luci/widgets/manage_systems_form.py b/luci/widgets/manage_systems_form.py
index b713d99..9c22b0b 100644
--- a/luci/widgets/manage_systems_form.py
+++ b/luci/widgets/manage_systems_form.py
@@ -1,12 +1,11 @@
from tw.api import WidgetsList
-from tw.forms import TableForm, TextField, Label
+from tw.forms import TableForm, TextField, Label, PasswordField
+import tw.dynforms as twd
-class ManageSystemsForm(TableForm):
- class fields(WidgetsList):
-
- mylabel = Label(text='Reauthenticate to Storage or Cluster Systems')
+class ManageSystemsForm(twd.GrowingTableForm):
+ class children(WidgetsList):
system_hostname = TextField()
- root_password = TextField()
+ root_password = PasswordField()
create_manage_systems_form = ManageSystemsForm("create_manage_systems_form")
14 years, 8 months
[luci] Added forms for the homebase page. However they're not hooked to
by Chris Feist
commit 5a5ee9bc121821bb3a7f1740e36803e94d9712ac
Author: Chris Feist <cfeist(a)redhat.com>
Date: Mon Sep 14 17:31:09 2009 -0500
Added forms for the homebase page. However they're not hooked to
anything yet, but it's the first step to getting them functional.
luci/controllers/root.py | 17 +++++++++++++++--
luci/templates/homebase.html | 10 ++++++++--
luci/widgets/add_existing_form.py | 11 +++++++++++
luci/widgets/add_system_form.py | 10 ++++++++++
luci/widgets/add_user_form.py | 11 +++++++++++
luci/widgets/manage_systems_form.py | 12 ++++++++++++
6 files changed, 67 insertions(+), 4 deletions(-)
---
diff --git a/luci/controllers/root.py b/luci/controllers/root.py
index d54e802..74c1123 100644
--- a/luci/controllers/root.py
+++ b/luci/controllers/root.py
@@ -23,6 +23,10 @@ from luci.controllers.fdom import FailoverController
from luci.controllers.fence import FenceController
from luci.controllers.node import NodeController
from luci.controllers.service import ServiceController
+from luci.widgets.add_system_form import create_add_system_form
+from luci.widgets.add_existing_form import create_add_existing_form
+from luci.widgets.add_user_form import create_add_user_form
+from luci.widgets.manage_systems_form import create_manage_systems_form
__all__ = ['RootController']
@@ -68,8 +72,17 @@ class RootController(BaseController):
return dict(page='about')
@expose('luci.templates.homebase')
- def homebase(self, homebasepage='addsystem'):
- return dict(page='homebase',homebasepage=homebasepage)
+ def homebase(self, homebasepage='homebasepage'):
+ if homebasepage == 'addsystem':
+ tmpl_context.form = create_add_system_form
+ elif homebasepage == 'addexisting':
+ tmpl_context.form = create_add_existing_form
+ elif homebasepage == 'adduser':
+ tmpl_context.form = create_add_user_form
+ elif homebasepage == 'managesystems':
+ tmpl_context.form = create_manage_systems_form
+
+ return dict(page='homebase',homebasepage=homebasepage)
@expose('luci.templates.cluster')
def cluster(self,clusterpage='clusterlist'):
diff --git a/luci/templates/homebase.html b/luci/templates/homebase.html
index b8b7d72..c6e9391 100644
--- a/luci/templates/homebase.html
+++ b/luci/templates/homebase.html
@@ -24,8 +24,14 @@
</td>
<td valign="top">
<div class="mainpage">
- <b>Home base</b><br/>
- ${homebasepage}
+ <div py:if="homebasepage != 'homebasepage'">
+ <div py:replace="tmpl_context.form()">Input Form</div>
+ </div>
+ <div py:if="homebasepage == 'homebasepage'">
+ <h2>Luci Homebase</h2>
+ <p>Welcome to Luci.</p>
+ <p>Select an action from the list on the left.</p>
+ </div>
</div>
</td>
</tr>
diff --git a/luci/widgets/__init__.py b/luci/widgets/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/luci/widgets/add_existing_form.py b/luci/widgets/add_existing_form.py
new file mode 100644
index 0000000..a7f4974
--- /dev/null
+++ b/luci/widgets/add_existing_form.py
@@ -0,0 +1,11 @@
+from tw.api import WidgetsList
+from tw.forms import TableForm, TextField, Label
+
+class AddExistingForm(TableForm):
+ class fields(WidgetsList):
+ mylabel = Label(text = 'Enter one node from the cluster you wish to add the Luci management interface.')
+ system_hostname = TextField()
+ root_password = TextField()
+
+create_add_existing_form = AddExistingForm("create_add_existing_form")
+
diff --git a/luci/widgets/add_system_form.py b/luci/widgets/add_system_form.py
new file mode 100644
index 0000000..f1dffdc
--- /dev/null
+++ b/luci/widgets/add_system_form.py
@@ -0,0 +1,10 @@
+from tw.api import WidgetsList
+from tw.forms import TableForm, TextField
+
+class AddSystemForm(TableForm):
+ class fields(WidgetsList):
+ system_hostname = TextField()
+ root_password = TextField()
+
+create_add_system_form = AddSystemForm("create_add_system_form")
+
diff --git a/luci/widgets/add_user_form.py b/luci/widgets/add_user_form.py
new file mode 100644
index 0000000..bbc5a94
--- /dev/null
+++ b/luci/widgets/add_user_form.py
@@ -0,0 +1,11 @@
+from tw.api import WidgetsList
+from tw.forms import TableForm, TextField, PasswordField
+
+class AddUserForm(TableForm):
+ class fields(WidgetsList):
+ user_name = TextField()
+ password = PasswordField()
+ confirm_password = PasswordField()
+
+create_add_user_form = AddUserForm("create_add_user_form")
+
diff --git a/luci/widgets/manage_systems_form.py b/luci/widgets/manage_systems_form.py
new file mode 100644
index 0000000..b713d99
--- /dev/null
+++ b/luci/widgets/manage_systems_form.py
@@ -0,0 +1,12 @@
+from tw.api import WidgetsList
+from tw.forms import TableForm, TextField, Label
+
+class ManageSystemsForm(TableForm):
+ class fields(WidgetsList):
+
+ mylabel = Label(text='Reauthenticate to Storage or Cluster Systems')
+ system_hostname = TextField()
+ root_password = TextField()
+
+create_manage_systems_form = ManageSystemsForm("create_manage_systems_form")
+
14 years, 8 months
[luci] - Added menu placeholders to imitate the current conga configuration.
by Chris Feist
commit 876aee8e1330e9ecfbd7d2fa132e07ba209d8a7e
Author: Chris Feist <cfeist(a)redhat.com>
Date: Fri Sep 11 18:19:16 2009 -0500
- Added menu placeholders to imitate the current conga configuration.
- Temporarily disabled the login screen.
luci/controllers/root.py | 12 ++++++++++++
luci/templates/cluster.html | 33 +++++++++++++++++++++++++++++++++
luci/templates/homebase.html | 34 ++++++++++++++++++++++++++++++++++
luci/templates/index.html | 4 ++--
luci/templates/master.html | 3 +++
luci/templates/storage.html | 31 +++++++++++++++++++++++++++++++
6 files changed, 115 insertions(+), 2 deletions(-)
---
diff --git a/luci/controllers/root.py b/luci/controllers/root.py
index e0af384..d54e802 100644
--- a/luci/controllers/root.py
+++ b/luci/controllers/root.py
@@ -67,6 +67,18 @@ class RootController(BaseController):
"""Handle the 'about' page."""
return dict(page='about')
+ @expose('luci.templates.homebase')
+ def homebase(self, homebasepage='addsystem'):
+ return dict(page='homebase',homebasepage=homebasepage)
+
+ @expose('luci.templates.cluster')
+ def cluster(self,clusterpage='clusterlist'):
+ return dict(page='cluster', clusterpage=clusterpage)
+
+ @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')
diff --git a/luci/templates/cluster.html b/luci/templates/cluster.html
new file mode 100644
index 0000000..f99825f
--- /dev/null
+++ b/luci/templates/cluster.html
@@ -0,0 +1,33 @@
+<!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>Login Form</title>
+</head>
+
+<body>
+ <table>
+ <tr>
+ <td valign="top">
+ <div class="sidebar">
+ <a href="${tg.url('/cluster/clusterlist')}">Cluster List</a><br/>
+ <a href="${tg.url('/cluster/createcluster')}">Create a New Cluster</a><br/>
+ <a href="${tg.url('/cluster/configure')}">Configure</a><br/>
+ </div>
+ </td>
+ <td valign="top">
+ <div class="mainpage">
+ <b>Cluster</b><br/>
+ ${clusterpage}
+ </div>
+ </td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/luci/templates/homebase.html b/luci/templates/homebase.html
new file mode 100644
index 0000000..b8b7d72
--- /dev/null
+++ b/luci/templates/homebase.html
@@ -0,0 +1,34 @@
+<!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>Login Form</title>
+</head>
+
+<body>
+ <table>
+ <tr>
+ <td valign="top">
+ <div class="sidebar">
+ <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/>
+ </div>
+ </td>
+ <td valign="top">
+ <div class="mainpage">
+ <b>Home base</b><br/>
+ ${homebasepage}
+ </div>
+ </td>
+ </tr>
+ </table>
+</body>
+</html>
diff --git a/luci/templates/index.html b/luci/templates/index.html
index 0cc3097..7b2cfcd 100644
--- a/luci/templates/index.html
+++ b/luci/templates/index.html
@@ -12,13 +12,13 @@
</head>
<body>
-<div id="loginform">
+<!--<div id="loginform">
<form action="${tg.url('/login_handler')}" method="POST" class="loginfields">
<h2><span>Login</span></h2>
<label for="login">Username:</label><input type="text" id="login" name="login" class="text"></input><br/>
<label for="password">Password:</label><input type="password" id="password" name="password" class="text"></input>
<input type="submit" id="submit" value="Login" />
</form>
-</div>
+</div>-->
</body>
</html>
diff --git a/luci/templates/master.html b/luci/templates/master.html
index 7fc9b00..95520a5 100644
--- a/luci/templates/master.html
+++ b/luci/templates/master.html
@@ -17,6 +17,9 @@
<body py:match="body" py:attrs="select('@*')">
${header()}
<ul id="mainmenu">
+ <li><a href="${tg.url('/homebase')}">homebase</a></li>
+ <li><a href="${tg.url('/cluster')}">cluster</a></li>
+ <li><a href="${tg.url('/storage')}">storage</a></li>
<li><a href="${tg.url('/about')}" class="${('', 'active')[defined('page') and page==page=='about']}">About</a></li>
<li><a href="${tg.url('/nodes')}">Nodes</a></li>
<li><a href="${tg.url('/services')}">Services</a></li>
diff --git a/luci/templates/storage.html b/luci/templates/storage.html
new file mode 100644
index 0000000..2308d30
--- /dev/null
+++ b/luci/templates/storage.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>Login Form</title>
+</head>
+
+<body>
+ <table>
+ <tr>
+ <td valign="top">
+ <div class="sidebar">
+ <a href="/storage/systemlist">System List</a><br/>
+ </div>
+ </td>
+ <td valign="top">
+ <div class="mainpage">
+ <b>Storage</b><br/>
+ ${storagepage}
+ </div>
+ </td>
+ </tr>
+ </table>
+</body>
+</html>
14 years, 8 months