[Debian-ha-commits] [crmsh] 03/05: Imported Upstream version 2.1.3

Richard Winters devrik-guest at moszumanska.debian.org
Thu Apr 16 07:32:08 UTC 2015


This is an automated email from the git hooks/post-receive script.

devrik-guest pushed a commit to branch master
in repository crmsh.

commit 08a653073f521ccf93c5e334d444b9a30f825a7d
Author: Richard B Winters <rik at mmogp.com>
Date:   Thu Apr 16 03:27:29 2015 -0400

    Imported Upstream version 2.1.3
---
 .gitignore                                     |    8 +
 .hgignore                                      |    7 +
 .travis.yml                                    |    9 +
 AUTHORS                                        |   30 +
 COPYING                                        |  339 ++
 ChangeLog                                      |  525 +++
 Makefile.am                                    |   47 +
 NEWS                                           |    0
 README                                         |   58 +
 README.dev                                     |  263 ++
 TODO                                           |   32 +
 acinclude.m4                                   |   39 +
 autogen.sh                                     |  144 +
 configure.ac                                   |  297 ++
 contrib/Makefile.am                            |   25 +
 contrib/README.vimsyntax                       |   24 +
 contrib/bash_completion.sh                     |  248 ++
 contrib/pacemaker-crm.vim                      |  129 +
 contrib/pcmk.vim                               |  114 +
 crm                                            |   58 +
 crm.conf.in                                    |   51 +
 crmsh-cibadmin_can_patch.patch                 |   23 +
 crmsh.spec                                     |  227 ++
 doc/Makefile.am                                |   43 +
 doc/crm.8.txt                                  | 4336 ++++++++++++++++++++++++
 doc/crmsh_hb_report.8.txt                      |  477 +++
 doc/website-v1/404.txt                         |    9 +
 doc/website-v1/Makefile                        |   88 +
 doc/website-v1/about.txt                       |   19 +
 doc/website-v1/configuration.txt               |  132 +
 doc/website-v1/crm.conf                        |  587 ++++
 doc/website-v1/css/crm.css                     |  478 +++
 doc/website-v1/css/font-awesome.css            | 1338 ++++++++
 doc/website-v1/css/font-awesome.min.css        |    4 +
 doc/website-v1/development.txt                 |   62 +
 doc/website-v1/documentation.txt               |   43 +
 doc/website-v1/faq.txt                         |   60 +
 doc/website-v1/fonts/FontAwesome.otf           |  Bin 0 -> 62856 bytes
 doc/website-v1/fonts/fontawesome-webfont.eot   |  Bin 0 -> 38205 bytes
 doc/website-v1/fonts/fontawesome-webfont.svg   |  414 +++
 doc/website-v1/fonts/fontawesome-webfont.ttf   |  Bin 0 -> 80652 bytes
 doc/website-v1/fonts/fontawesome-webfont.woff  |  Bin 0 -> 44432 bytes
 doc/website-v1/history-guide.txt               |    3 +
 doc/website-v1/img/laptop.png                  |  Bin 0 -> 2569 bytes
 doc/website-v1/img/loader.gif                  |  Bin 0 -> 2545 bytes
 doc/website-v1/img/servers.gif                 |  Bin 0 -> 4513 bytes
 doc/website-v1/index.txt                       |   19 +
 doc/website-v1/installation.txt                |   59 +
 doc/website-v1/make-news.py                    |  137 +
 doc/website-v1/man-1.2.txt                     | 3437 +++++++++++++++++++
 doc/website-v1/man-2.0.txt                     | 4319 +++++++++++++++++++++++
 doc/website-v1/news.txt                        |   11 +
 doc/website-v1/news/2014-06-30-release-2_1.txt |   93 +
 doc/website-v1/postprocess.py                  |  139 +
 doc/website-v1/rsctest-guide.txt               |  238 ++
 doc/website-v1/scripts.txt                     |  445 +++
 doc/website-v1/start-guide.txt                 |  290 ++
 hb_report/Makefile.am                          |   25 +
 hb_report/ha_cf_support.sh                     |   83 +
 hb_report/hb_report.in                         | 1469 ++++++++
 hb_report/openais_conf_support.sh              |   97 +
 hb_report/utillib.sh                           |  752 ++++
 modules/Makefile.am                            |   81 +
 modules/__init__.py                            |    2 +
 modules/cache.py                               |   52 +
 modules/cibconfig.py                           | 3594 ++++++++++++++++++++
 modules/cibstatus.py                           |  403 +++
 modules/cibverify.py                           |   43 +
 modules/clidisplay.py                          |  130 +
 modules/cliformat.py                           |  415 +++
 modules/cmd_status.py                          |   73 +
 modules/command.py                             |  497 +++
 modules/completers.py                          |   78 +
 modules/config.py                              |  411 +++
 modules/constants.py                           |  289 ++
 modules/corosync.py                            |  532 +++
 modules/crm_gv.py                              |  242 ++
 modules/crm_pssh.py                            |  237 ++
 modules/help.py                                |  403 +++
 modules/idmgmt.py                              |  203 ++
 modules/log_patterns.py                        |   77 +
 modules/log_patterns_118.py                    |   75 +
 modules/main.py                                |  389 +++
 modules/msg.py                                 |  283 ++
 modules/options.py                             |   31 +
 modules/ordereddict.py                         |  130 +
 modules/orderedset.py                          |   98 +
 modules/pacemaker.py                           |  398 +++
 modules/parse.py                               | 1600 +++++++++
 modules/ra.py                                  |  836 +++++
 modules/report.py                              | 1676 +++++++++
 modules/rsctest.py                             |  433 +++
 modules/schema.py                              |  146 +
 modules/scripts.py                             |  743 ++++
 modules/template.py                            |  195 ++
 modules/term.py                                |  186 +
 modules/tmpfiles.py                            |   71 +
 modules/ui_assist.py                           |  146 +
 modules/ui_cib.py                              |  234 ++
 modules/ui_cibstatus.py                        |  114 +
 modules/ui_cluster.py                          |  214 ++
 modules/ui_configure.py                        |  806 +++++
 modules/ui_context.py                          |  371 ++
 modules/ui_corosync.py                         |  158 +
 modules/ui_history.py                          |  666 ++++
 modules/ui_node.py                             |  321 ++
 modules/ui_options.py                          |  191 ++
 modules/ui_ra.py                               |  108 +
 modules/ui_report.py                           |   41 +
 modules/ui_resource.py                         |  576 ++++
 modules/ui_root.py                             |  184 +
 modules/ui_script.py                           |   68 +
 modules/ui_site.py                             |   92 +
 modules/ui_template.py                         |  371 ++
 modules/ui_utils.py                            |  174 +
 modules/userdir.py                             |   76 +
 modules/utils.py                               | 1357 ++++++++
 modules/xmlbuilder.py                          |  117 +
 modules/xmlutil.py                             | 1323 ++++++++
 requirements.txt                               |    3 +
 scripts/Makefile.am                            |    2 +
 scripts/add/Makefile.am                        |   28 +
 scripts/add/add.py                             |  128 +
 scripts/add/main.yml                           |   34 +
 scripts/check-uptime/Makefile.am               |   28 +
 scripts/check-uptime/fetch.py                  |    7 +
 scripts/check-uptime/main.yml                  |   17 +
 scripts/check-uptime/report.py                 |   11 +
 scripts/health/Makefile.am                     |   27 +
 scripts/health/collect.py                      |   99 +
 scripts/health/hahealth.py                     |   34 +
 scripts/health/main.yml                        |   12 +
 scripts/health/report.py                       |  125 +
 scripts/init/Makefile.am                       |   28 +
 scripts/init/authkey.py                        |   61 +
 scripts/init/basic.cib.template                |   13 +
 scripts/init/collect.py                        |    7 +
 scripts/init/configure.py                      |  134 +
 scripts/init/corosync.conf.template            |   46 +
 scripts/init/init.py                           |   36 +
 scripts/init/main.yml                          |   52 +
 scripts/init/verify.py                         |   48 +
 scripts/remove/Makefile.am                     |   28 +
 scripts/remove/main.yml                        |   20 +
 scripts/remove/remove.py                       |   46 +
 templates/Makefile.am                          |   26 +
 templates/apache                               |   61 +
 templates/clvm                                 |   59 +
 templates/filesystem                           |   44 +
 templates/gfs2                                 |   74 +
 templates/gfs2-base                            |   46 +
 templates/ocfs2                                |   61 +
 templates/sbd                                  |   34 +
 templates/virtual-ip                           |   39 +
 test/Makefile.am                               |   28 +
 test/README.regression                         |  154 +
 test/cib-tests.sh                              |  105 +
 test/cibtests/001.exp.xml                      |   20 +
 test/cibtests/001.input                        |    6 +
 test/cibtests/002.exp.xml                      |   28 +
 test/cibtests/002.input                        |    8 +
 test/cibtests/003.exp.xml                      |   29 +
 test/cibtests/003.input                        |   11 +
 test/cibtests/004.exp.xml                      |   29 +
 test/cibtests/004.input                        |   11 +
 test/cibtests/Makefile.am                      |   29 +
 test/cibtests/shadow.base                      |   10 +
 test/crm-interface                             |   99 +
 test/defaults                                  |    2 +
 test/descriptions                              |   33 +
 test/evaltest.sh                               |  127 +
 test/history-test.tar.bz2                      |  Bin 0 -> 51085 bytes
 test/list-undocumented-commands.py             |   36 +
 test/regression.sh                             |  213 ++
 test/testcases/Makefile.am                     |   36 +
 test/testcases/acl                             |   60 +
 test/testcases/acl.excl                        |    1 +
 test/testcases/acl.exp                         |   87 +
 test/testcases/basicset                        |   15 +
 test/testcases/commit                          |   40 +
 test/testcases/commit.exp                      |   77 +
 test/testcases/common.excl                     |   20 +
 test/testcases/common.filter                   |    5 +
 test/testcases/confbasic                       |   76 +
 test/testcases/confbasic-xml                   |   72 +
 test/testcases/confbasic-xml.exp               |  169 +
 test/testcases/confbasic.exp                   |  133 +
 test/testcases/delete                          |   64 +
 test/testcases/delete.exp                      |  153 +
 test/testcases/edit                            |   59 +
 test/testcases/edit.excl                       |    1 +
 test/testcases/edit.exp                        |  113 +
 test/testcases/file                            |   14 +
 test/testcases/file.exp                        |   53 +
 test/testcases/history                         |   40 +
 test/testcases/history.excl                    |    1 +
 test/testcases/history.exp                     |  288 ++
 test/testcases/history.post                    |    3 +
 test/testcases/history.pre                     |    3 +
 test/testcases/newfeatures                     |   25 +
 test/testcases/newfeatures.exp                 |   44 +
 test/testcases/node                            |   10 +
 test/testcases/node.exp                        |  162 +
 test/testcases/options                         |   23 +
 test/testcases/options.exp                     |   64 +
 test/testcases/ra                              |    7 +
 test/testcases/ra.exp                          |  140 +
 test/testcases/ra.filter                       |   16 +
 test/testcases/resource                        |   39 +
 test/testcases/resource.exp                    |  734 ++++
 test/testcases/rset                            |   21 +
 test/testcases/rset-xml                        |   19 +
 test/testcases/rset-xml.exp                    |   33 +
 test/testcases/rset.exp                        |   56 +
 test/testcases/shadow                          |   10 +
 test/testcases/shadow.exp                      |   18 +
 test/testcases/xmlonly.sh                      |    5 +
 test/unit-tests.sh                             |   13 +
 test/unittests/__init__.py                     |   54 +
 test/unittests/bug-862577_corosync.conf        |   51 +
 test/unittests/corosync.conf.1                 |   81 +
 test/unittests/corosync.conf.2                 |   58 +
 test/unittests/schemas/acls-1.1.rng            |   66 +
 test/unittests/schemas/acls-1.2.rng            |   66 +
 test/unittests/schemas/constraints-1.0.rng     |  180 +
 test/unittests/schemas/constraints-1.1.rng     |  246 ++
 test/unittests/schemas/constraints-1.2.rng     |  219 ++
 test/unittests/schemas/fencing.rng             |   29 +
 test/unittests/schemas/nvset.rng               |   35 +
 test/unittests/schemas/pacemaker-1.0.rng       |  121 +
 test/unittests/schemas/pacemaker-1.1.rng       |  161 +
 test/unittests/schemas/pacemaker-1.2.rng       |  146 +
 test/unittests/schemas/resources-1.0.rng       |  177 +
 test/unittests/schemas/resources-1.1.rng       |  225 ++
 test/unittests/schemas/resources-1.2.rng       |  225 ++
 test/unittests/schemas/rule.rng                |  137 +
 test/unittests/schemas/score.rng               |   18 +
 test/unittests/schemas/versions.rng            |   24 +
 test/unittests/test.conf                       |   12 +
 test/unittests/test_bugs.py                    |  446 +++
 test/unittests/test_cib.py                     |   41 +
 test/unittests/test_cliformat.py               |  227 ++
 test/unittests/test_corosync.py                |  134 +
 test/unittests/test_objset.py                  |   50 +
 test/unittests/test_parse.py                   |  662 ++++
 test/unittests/test_resource.py                |   46 +
 test/unittests/test_utils.py                   |  137 +
 utils/Makefile.am                              |   27 +
 utils/crm_clean.py                             |   26 +
 utils/crm_init.py                              |  263 ++
 utils/crm_pkg.py                               |  281 ++
 utils/crm_rpmcheck.py                          |   56 +
 utils/crm_script.py                            |  180 +
 version.in                                     |    2 +
 254 files changed, 55590 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..50adcad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+*.pyc
+*~
+#*.*#
+doc/website-v1/gen
+Makefile.in
+autom4te.cache
+patches/*
+
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..1afba4a
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,7 @@
+syntax: glob
+
+*.pyc
+*~
+#*.*#
+doc/gen
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..8a4b651
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,9 @@
+---
+language: python
+python:
+  - "2.6"
+  - "2.7"
+install:
+  - "pip install -r requirements.txt"
+script: ./test/unit-tests.sh --with-coverage
+
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..4dd3dc8
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,30 @@
+NOTE:	The work of everyone on this project is dearly appreciated. If you
+	are not listed here but should be, please notify us!
+
+	afederic <afederic[at]gmail[dot]com>
+	Andrew Beekhof <andrew[at]beekhof[dot]net>
+	Borislav Borisov <borislav[dot]v[dot]borisov[at]gmail[dot]com>
+	Christian Seiler <christian[at]iwakd[dot]de>
+	Dejan Muhamedagic <dejan[at]suse[dot]de>
+	Federica Teodori <federica[dot]teodori[at]googlemail[dot]com>
+	Florian Haas <florian[dot]haas[at]linbit[dot]com>
+	Goldwyn Rodrigues <rgoldwyn[at]novell[dot]com>
+	Hideo Yamauchi <renayama19661014[at]ybb[dot]ne[dot]jp>
+	Holger Teutsch <holger[dot]teutsch[at]web[dot]de>
+	Kazunori INOUE <kazunori[dot]inoue3[at]gmail[dot]com>
+	Keisuke MORI <keisuke[dot]mori+ha[at]gmail[dot]com>
+	Kristoffer Gronlund <kgronlund[at]suse[dot]com>
+	Lars Ellenberg <lars[dot]ellenberg[at]linbit[dot]com>
+	Lars Marowsky-Brée <lmb[at]suse[dot]de>
+	Michael Prokop <devnull[at]localhost>
+	NAKAHIRA Kazutomo <nakahira[dot]kazutomo[at]oss[dot]ntt[dot]co[dot]jp>
+	nozawat <nozawat[at]gmail[dot]com>
+	renayama19661014 <renayama19661014[at]ybb[dot]ne[dot]jp>
+	seabres <rainer[dot]brestan[at]gmx[dot]net>
+	Tim Serong <tserong[at]suse[dot]com>
+	Vincenzo Pii <piiv[at]zhaw[dot]ch>
+	Vladislav Bogdanov <bubble[at]hoster-ok[dot]com>
+	Xia Li <XLi[at]suse[dot]com>
+	Xinwei Hu <xwhu[at]novell[dot]com>
+	Yan Gao <ygao[at]suse[dot]com>
+	Yuusuke IIDA <iidayuus[at]intellilink[dot]co[dot]jp>
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..d511905
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser General
+Public License instead of this License.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644
index 0000000..525a121
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,525 @@
+* Fri Apr 10 2015 Kristoffer Grönlund <kgronlund at suse.com> and many others
+- Release 2.1.3
+- medium: parse: nvpair attributes with no value = <nvpair name=".."/> (#71)
+- doc: Add link to clusterlabs.org
+- medium: report: Convert RE exception to simpler UI output
+- medium: report: Include transitions with configuration changes (bnc#917131)
+- medium: config: Fix case-sensitivity for booleans
+- medium: ra: Handle non-OCF agent meta-data better
+- Medium: cibconf: preserve cib user attributes
+- low: cibconfig: Improved debug output when schema change fails
+- medium: parse: Treat pacemaker-next schema as 2.0+
+- medium: schema: Test if node type is optional via schema
+- medium: schema: Remove extra debug output
+- low: pacemaker: Remove debug output
+- medium: cibconfig: If a change results in no diff, exit silently
+- medium: cibconfig: Allow delete of objects that don't exist without returning error code
+- medium: cibconfig: Allow removal of non-existing elements if --force is set
+- low: allow (0,1) as option booleans
+- low: allow pacemaker 1.0 version detection
+- Low: hb_report: add -Q to usage
+- Low: hb_report: add -X option for extra ssh options
+- doc: Move the main crmsh repository to the ClusterLabs organization on github
+- high: ui_configure: Remove acl_group command (bnc#921056)
+- high: cibconfig: Don't delete valid tickets when removing referenced objects (bnc#922039)
+- high: ui_context: Wait for DC after commit, not before (#85)
+- medium: templates: Clearer descriptions for editing templates (boo#921028)
+- high: cibconfig: Derive id for ops from referenced resource name (boo#921028)
+- medium: ui_template: Always generate id unless explicitly defined (boo#921028)
+- low: template: Add 'new <template>' shortcut
+- medium: ui_template: Make new command more robust (bnc#924641)
+- medium: parse: Disallow location rules without resources
+- high: parse: Don't allow constraints without applicants
+- medium: cliformat: Escape double-quotes in nvpair values
+- low: hb_report: Use crmsh config to find pengine/cib dirs (bsc#926377)
+- low: main: Catch any ValueErrors that may leak through
+
+* Mon Jan 26 2015 Kristoffer Grönlund <kgronlund at suse.com> and many others
+- Release 2.1.2
+- medium: ui_resource: Set probe interval 0 if not set (bnc#905050)
+- doc: Document probe op in resource trace (bnc#905050)
+- high: config: Fix path to system-wide crm.conf (#67)
+- medium: config: Fall back to /etc/crm/crmsh.conf (#67)
+- low: cliformat: Colorize id: as identifier (boo#905338)
+- medium: cibconfig: Don't bump epoch if stripping version
+- medium: ui_context: Lazily import readline
+- medium: config: Add core.ignore_missing_metadata (#68) (boo#905910)
+- medium: cibconfig: Strip digest from v1 diffs (bnc#914098)
+- medium: cibconfig: Detect v1 format and don't patch container changes (bnc#914098)
+- high: xmlutil: Treat node type=member as normal (boo#904698)
+- medium: xmlutil: Use idmgmt when creating new elements (bnc#901543)
+- low: ui_resource: --reprobe and --refresh are deprecated (bnc#905092)
+- doc: Document deprecation of refresh and reprobe (bnc#905092)
+- medium: parse: Support resource-discovery in location constraints
+- medium: Allow removing groups even if is_running (boo#905271)
+- medium: cibconfig: Delete containers first in edits (boo#905268)
+- medium: ui_history: Fix crash using empty object set
+- Low: term: get rid of annying ^O in piped-to-less-R output
+- medium: parse: Allow nvpair with no value using name= syntax (#71)
+- medium: parse: Enable name[=value] for nvpair (#71)
+- medium: utils: Check if path basename is less (#74)
+- medium: utils: crm_daemon_dir is added to PATH in envsetup (#67)
+- medium: cmd_status: Show pending if available, enable extra options
+- high: utils: Locate binaries across sudo boundary (bnc#912483)
+- Medium: history: match error/crit messages of pcmk 1.1.12
+- low: ui_options: Add underscore aliases for legacy options
+- medium: constants: Fix transition start detection
+- medium: constants: Update transition regex (#77)
+- medium: orderedset: Add OrderedSet type
+- medium: cibconfig: Use orderedset to avoid reordering bugs (#79)
+- low: xmlutil: logic bug in sanity_check_nvpairs
+- medium: util: Don't fall back to current time
+- medium: report: Fall back to end_ts = start_ts
+
+* Tue Oct 28 2014 Kristoffer Grönlund <kgronlund at suse.com> and many others
+- Release 2.1.1
+- cibconfig: Clean up output from crm_verify (bnc#893138)
+- high: constants: Add acl_target and acl_group to cib_cli_map (bnc#894041)
+- high: parse: split shortcuts into valid rules
+- medium: Handle broken CIB in find_objects
+- high: scripts: Handle corosync.conf without nodelist in add-node (bnc#862577)
+- medium: config: Assign default path in all cases
+- high: cibconfig: Generate valid CLI syntax for attribute lists (bnc#897462)
+- high: cibconfig: Add tag:<tag> to get all resources in tag
+- doc: Documentation for show tag:<tag>
+- low: report: Sort list of nodes
+- high: parse: Allow empty attribute values in nvpairs (bnc#898625)
+- high: cibconfig: Delay reinitialization after commit
+- low: cibconfig: Improve wording of commit prompt
+- low: cibconfig: Fix vim modeline
+- high: report: Find nodes for any log type (boo#900654)
+- high: hb_report: Collect logs from journald (boo#900654)
+- high: cibconfig: Don't crash if given an invalid pattern (bnc#901714)
+- high: xmlutil: Filter list of referenced resources (bnc#901714)
+- medium: ui_resource: Only act on resources (#64)
+- medium: ui_resource: Flatten, then filter (#64)
+- high: ui_resource: Use correct name for error function (bnc#901453)
+- high: ui_resource: resource trace failed if operation existed (bnc#901453)
+- Improved test suite
+
+* Mon Jun 30 2014 Kristoffer Grönlund <kgronlund at suse.com> and many others
+- Release 2.1
+- Add atom feed to development page
+- Medium: hb_report: dot is not illegal in file names (bnc#884079, debian#715391)
+- Low: history: remove existing report directory on refresh
+- medium: ui_history: Print source if given no argument (bnc#883437)
+- Medium: hb_report: update interface to zypper (bnc#883186)
+- Medium: hb_report: support logs with varied timestamps (bnc#883186)
+- Low: hb_report: getstampproc is global (bnc#883186)
+- Low: hb_report: gdb debug symbols output change (bnc#883186)
+- Low: hb_report: don't restrict debuginfo to cluster stack binaries (zypper) (bnc#883186)
+- high: ui_history: Lazily fetch report data on command (bnc#882959)
+- medium: report: Make setting report period more robust (bnc#882959)
+- medium: ui_resource: Remove empty attrlists when overriding children (bnc#882655)
+- high: cibconfig: Retain empty attribute sets (bnc#882655)
+- Low: report: unpack tarball if it's newer than the existing directory
+- Low: report: get node list based on collected logs, not from cib
+- Low: report: test for ha-log.txt instead of cib.txt when listing nodes
+- Low: report: don't warn on extra nodes in the report
+- medium: ui_configure: Nicer error when pacemaker is not running (bnc#882475)
+- medium: scripts: configure SSH in cluster init (bnc#882476)
+- medium: ui_assist: add template command (bnc#882477)
+- medium: cliformat: Fix CLI formatting for rules and id-refs
+- doc: Update documentation for location constraints (bnc#873781)
+- doc: Document interval suffixes (bnc#873677)
+- medium: ui_node: Fix display of node attributes
+- medium: parse: Allow remote as node type
+- low: cliformat: Don't show extraneous id for acl rules
+- high: cibconfig: Fix bug when copying nvpairs (bnc#881369)
+- high: parse: Try to retain ordering if possible (bnc#880371)
+- high: cibconfig: Enable use of v2 patches in Pacemaker (bnc#880371)
+- medium: pacemaker: Don't hardcode list of supported schemas
+- Medium: resource: modify some command wait options (bnc#880982)
+- high: parse: Support for ACL schema 2.0 (bnc#880371)
+- medium: schema: Fix typo in test_schema()
+- medium: parse: Allow empty property sets (bnc#880632)
+- medium: ui_resource: Also trace promote/demote for multistate resources
+- medium: ui_resource: allow trace of resource without specific operation
+- medium: ui_resource: Make op an optional argument to trace/untrace
+- low: ui_resource: Allow untrace without explicit interval
+- high: cibconfig: adjust attributes when adding operations (bnc#880052)
+- high: parse: Support id-ref in nvpairs (fate#316118)
+- low: ui_configure: Add --force flag to configure delete
+- medium: xmlutil: Limit xpath search to children (bnc#879419)
+- medium: ui: Fix argument check in resource commands (gh#crmsh/crmsh#29)
+- high: xmlutil: Include remote nodes in nodelist (bnc#878112)
+- medium: cibconfig: Detect broken child relationship (bnc#878112)
+- high: cibconfig: Ban containers stealing children (bnc#878112)
+- low: command: Add -h and --help as aliases to help
+- high: parse: Allow role in rule-based location constraints (bnc#878128)
+- medium: report: Return to handling timestamps internally (bnc#877495)
+- medium: ui_resource: Fix race in start/stop/manage/unmanage (bnc#877640)
+- medium: parse: Allow empty attribute lists
+- medium: cibconfig: Fix uses of add_operation
+- medium: report: Make regexp groups non-capturing to avoid limit (bnc#877484)
+- medium: doc: Document rules in attribute lists (bnc#865292)
+- medium: constants: Rename cluster attribute to cluster-name (fate#316118)
+- medium: idmgmt: Fix id assignment and update regression tests (bnc#865292)
+- medium: cibconfig: Enable score for instance_attributes (bnc#865292)
+- high: cibconfig: Support rules in attribute lists (bnc#865292)
+- low: cibconfig: Better error when referring to non-existant template
+- medium: scripts: Handle percent characters in script output (bnc#876882)
+- pacemaker: Support 2.0 schema
+- vars: Rename property: s/site/cluster (fate#316118)
+- Medium: hb_report: fix ssh passwords again (bnc#867365)
+- vars: Add site to list of extra cluster properties (fate#316118)
+- parse: Fix check for action/role in resource set parser (#14)
+- report: More problems with datetime (bnc#874162)
+- report: Resolve datetime/timestamp mixup (bnc#874162)
+- utils: Handle datetime objects in shorttime/shortdate (bnc#874162)
+- main: Fix reference before assignment (#7)
+- crm: Check and complain about python version < 2.6 (#11)
+- parse: Unify API for err(), fix error
+- Fix garbage characters in prompt (issue#7)
+- Medium: cibconf: add comments in the right order (bnc#866434)
+- site: pass --force flag through to crm_ticket (bnc#873200)
+- Low: report: Use subsecond precision if possible (bnc#872932)
+- Low: hb_report: pcmk lib changed permissions (bnc#872958)
+- Low: history: set colours for all nodes found (bnc#872936)
+- ui_resource: Allow setting meta attributes on tags (fate#315101)
+- ui_configure: tag command  (fate#315101)
+- parse: Support cib object tags (fate#315101)
+- cibconfig: Support filename-style globs in show/edit (bnc#864346)
+- ui_resource: Only search in top-level (bnc#865024)
+- ui_resource: Don't create extra nvpairs (bnc#865024)
+- utils: Don't crash on missing reply to y/n question
+- Allow building crmsh without PyYAML
+- Support for pacemaker-1.3 RNG schema
+
+* Thu Apr 4 2014 Kristoffer Grönlund <kgronlund at suse.com> and many others
+- release 2.0
+- Improve output from history explorer when using a crm_report-generated
+  report (bnc#870886)
+- Add journal.log to interesting log files (bnc#870886)
+- make sanity check of node name not case sensitive
+- hb_report: Don't use deprecated ifconfig (bnc#871089)
+- parse: Clean up the CLI syntax display
+- ra: display without class:provider: prefix if possible
+- Better args error handling in configure load/save (bnc#870654)
+- ui_context: Correctly check end_game() return value (bnc#868533)
+- command: Propagate error from auto-commit (bnc#868533)
+- crm_pkg: Add --no-refresh to zypper commands
+- scripts: configure firewall to open cluster ports (bnc#868008)
+- scripts: Improved debug output from cluster scripts (bnc#866636)
+- main: Better descriptions for -d and -R flags.
+- utils: Nicer warning when crm_simulate fails
+- ui: Don't call nonexistent function on unsupported cluster stack
+- xmlutil: fencing-topology used broken comparison (bnc#866639)
+- parse: More liberal parsing of role assignment in constraint rules
+- scripts: corosync uses mcastport - 1 (bnc#868008)
+- utils: ask() did not respect force flag in all cases (bnc#868007)
+- xmlutil: Compare attribute dictionaries properly
+- xmlutil: Fix attribute handling in XML comparison function
+- xmlutil: Fix sorting of attribute keys in xml_cmp
+- xmlutil: Sanitize the CIB a bit less aggressively (bnc#866434)
+- xmlutil: in xml_cmp, s/print/common_debug/
+- xmlutil: Handle XML comments properly in xml_cmp
+- xmlutil: order-independent XML comparison (bnc#866434)
+- scripts: don't modify system unless necessary (bnc#866569)
+- xmlutil: don't crash on degenerate colocations
+- scripts: enable trace logging for cluster scripts (bnc#866636)
+- ui_cluster: use crm_mon -bD1 in wait_for_cluster (bnc#866635)
+- scripts: Disable corosync.log by default (bnc#866569)
+- scripts: Open appropriate ports in firewall (bnc#866569)
+- scripts: Configure quorum based on node count (bnc#866569)
+- utils: Record all calls in regression test output (bnc#862383)
+- ui_resource: Add maintenance command (bnc#863071)
+- parse: Fix resource sets, once and for all (savannah#41617)
+- scripts: Disable strict host key checking (bnc#864238)
+- hb_report: Fix incorrect quotes (bnc#863816)
+- cibconfig: do not format xml tags when requested (bnc#863746)
+- cibconfig: Handle non-string arguments (bnc#863736)
+- ui_root: Rename root level to 'root' (bnc#863583)
+- corosync: Allow tabs in corosync.conf (bnc#862577)
+- parse: Fix sequential=true for resource sets (bnc#862334)
+- cibconfig: fencing_topology warning with stonith templates
+  (savannah#41414)
+- xmlutil: rsc_template has no provider attribute (savannah#41410)
+- ra: Infer provider from RA name (bnc#860754)
+- ui_options: add missing documentation for options set (bnc#860585)
+- ui_cib: correct name of cib import (bnc#860584)
+- ui_ra: Fix problems with ra info command (bnc#860583)
+- ui_resource: Fix crash in resource cleanup (bnc#859570)
+- ui_assist: Add assist sublevel (fate#314917)
+- hb_report: Show progress when processing many transitions
+- report: Open reports output by crm_report (fate#316330)
+- hb_report: Display as 'report'
+- report: Move report creation to root
+- ui_report: Fix invocation of hb_report
+- hb_report: call corosync-blackbox, not corosync-fplay
+- help: Bug in delayed loading of help text
+- corosync: Better parser and more commands
+- scripts: Set PasswordAuthentication=no
+- ui_resource: Fix bug in resource restart
+- ui_cluster: Revised cluster status
+- msg: Don't print ok/info to stderr
+- ui_script: Allow --nodes='..', not only --nodes '..'
+- scripts: Cluster scripts (fate#316464, fate#309206, fate#316332)
+- config: Validate boolean values correctly
+- main: Seed random generator on startup
+- main: More informative error on start failure
+- cluster: Use crm_node -l for node list
+- crm_pssh: Limit scope of glob in pssh/get_output
+- ui_context: Less repetitive error message on unknown command
+- ui_cib: Fix typo in sublevel name: cib.cibconfig -> cib.cibstatus
+- help: Return error if help topic is not found (bug#40821)
+- main: Return more useful error codes
+- crm_gv: Support rsc_template in graphs (bnc#850159)
+- cibconfig: Updated fix for configure load method (bnc#841764)
+- parse: Correct recognition of kind in order constraints
+- history: Fix incorrect argument to level check
+- report: Fix broken call to hb_report
+- parse: Stricter parsing of resource names
+- parse: Resource sets in location constraints (fate#315158).
+- utils: Enable cibadmin -P for 1.1.11
+- parse: rsc_template is not recognized by parser (bnc#854562)
+- vars: Add remote-node as resource attribute (bnc#854552)
+- cibconfig: Add missing config import
+- hb_report: Prefer generating .bz2 archives (bnc#854060)
+- hb_report: Add support for xz compression (bnc#854060)
+- cluster: Implement run using pssh
+- ui_cluster: Cluster sublevel implementation
+- configure: Improved completion for group, clone, ms (bnc#845339)
+- config: Set OCF_ROOT in environ structure (used by ra.py)
+- main: Tab completion for multi-line statements BUG: bnc#845339
+- bash_completion: Add completion installation to spec file
+- ui_resource: Added new resource scores command
+- command: Improved default help for commands
+- crm_gv: Limit graph size to fit on A4
+- config: New configuration file format
+- parse: Support role= in rsc_location
+- msg: Add colors to message output
+- templates: Update OCFS2 template.
+- ui_context: Fix readline completion for empty input
+- ui_configure: Clearer error messages
+- ui_context: Wait if in transit
+- ui_configure: Clearer error messages
+- Enable colorized prompt
+- ui_context: Allow ui stack modifications
+- ui_configure: Completion + help for primitive
+- ui_context: Fix completion with no args to command
+- command: Fix case with no args to completer
+- ui_context: Improve completion
+- ui_ra: Updated completion for info
+- main.compgen: Adapt output to bash completion
+- bash_completion: Improve colon-handling
+- main: Fix issues with ctrl+C and profiling
+- ui_options: add option to print single user preference values
+- bash_completion: fix path to crm
+- Clean up contextual_help
+- Fix help with no argument
+- ui_context: Allow commands that manipulate the stack
+- ui_context: Fix stack handling
+- ui_configure: Add missing return statement
+- Check if command failed
+- Initial bash completion / completion framework
+- Add report level to wrap crmsh_hb_report
+- UI makeover
+- help: Rewritten help subsystem
+- hb_report: exit early if which(1) is missing
+- ui: anonymous temporary shadow CIBs
+- cibconf: fix two fencing top issues (savannah#40173)
+- node: clear state new way since pcmk 1.1.8 (bnc#843699)
+- Integrate hb_report as part of crmsh
+
+* Tue Sep 24 2013 Kristoffer Grönlund <kgronlund at suse.com>, Dejan Muhamedagic <dejan at suse.de>, and many others
+- release 1.2.6
+- cibconf: fix removing cluster properties in edit (bnc#841764)
+- history: improve setting history source
+- cibconf: fix rsc_template referencing (savannah#40011)
+- rsctest: add support for STONITH resources
+- help: fix help for alias commands
+- history: show and allow completion of all primitives and not only
+  top level resources such as groups
+- site: add missing completions
+- rsctest: fix multistate resource testing
+- site: add missing command aliases
+
+* Wed Aug 28 2013 Dejan Muhamedagic <dejan at suse.de> and many others
+- release candidate 1.2.6-rc3
+- cibconf: disable atomic updates until cibadmin gets fixed
+- cibconf: match special ids on configuration edit (fixes
+  disappearing elements on edit)
+- doc: website sources
+
+* Mon Aug  5 2013 Dejan Muhamedagic <dejan at suse.de> and many others
+- release candidate 1.2.6-rc1
+- main: allow starting with a specified CIB shadow
+- main: make sure that tmp files get removed
+- cibconf: replace minidom with lxml
+- cibconf: groups can have the container meta attribute
+- cibconf: do not load CIB automatically in a non-interactive
+  mode (bnc#813045)
+- cibconf: allow single level fencing_topology (savannah#38737)
+- cibconf: improve exit code if a referenced element does not
+  exist (e.g. in the show command)
+- cibconf: add simulate alias for the ptest command
+- cibconf: add -S when running crm_simulate (formerly ptest)
+- cibconf: use cibadmin patch to update live CIB (with pcmk >= 1.1.10)
+- cibconf: node ids are not id but text
+- cibconf: improve elements edit operation
+- resource: trace and untrace (RA) commands
+- resource: prevent whitespace in meta_attributes when setting
+  attributes in nested elements such as groups (bnc#815447)
+- resource: add option for better control of group management
+  (bnc#806901)
+- node/resource: improve lifetime processing
+- node: update interface to crm_node, its usage changed
+  (bnc#805278)
+- node: maintenance/ready commands
+- node: ignore case when looking up nodes
+- node: update interface to crm_node (node delete)
+- node: allow forced node removal
+- shadow: fix regression in cib import (from PE file)
+- shadow: set shadow directory according to the user preference
+- history: fix search for resource messages (bnc#803790)
+- history: refresh live report for commands other than info
+  (bnc#807402)
+- history: use anonymous re groups to prevent out of groups assertion
+- history: fix xpath expression for graphs of resource sets
+- history: skip empty lines (!) when searching logs
+- history: add support for rfc5242 date format in syslog
+- userprefs: add reset command
+- ui: fix exit code of crm status if crm_mon fails (savannah#38702)
+- ui: fix exit code of the help command
+- parse: drop obsolete test for operations
+- performance: do not make unnecessary parameter uniqueness test
+  (bnc#806372)
+- performance: check programs existence with python os module
+  (bnc#806372)
+- performance: improve tests for running resources
+
+* Wed Feb  6 2013 Dejan Muhamedagic <dejan at suse.de> and many others
+- stable release 1.2.5
+- cibconfig: modgroup command
+- cibconfig: directed graph support
+- cibconfig: fix syntax error in ptest
+- history: diff command (between PE inputs)
+- history: show pe commands
+- history: graph command
+- history: reduce number of live updates
+- history: inherit year from the report
+
+* Mon Dec 17 2012 Dejan Muhamedagic <dejan at suse.de> and many others
+- stable release 1.2.4
+- shadow: return proper exit code on I/O errors
+- history: implement transition save (to shadow) subcommand
+- history: fix regression when creating log objects
+- history: detailed transition output
+- history: force refresh on session load
+
+* Tue Dec 11 2012 Dejan Muhamedagic <dejan at suse.de> and many others
+- stable release 1.2.3
+- ra: don't print duplicate RAs in the list command (bnc#793585)
+- history: optimize source refreshing
+
+* Thu Dec  6 2012 Dejan Muhamedagic <dejan at suse.de> and many others
+- stable release 1.2.2
+- cibconfig: don't bail out if filter fails
+- cibconfig: improve id management on element update
+- ra: add support for nagios plugins
+- utils: make sure that there's at least one column (savannah#37658)
+- ui: improve quotes insertion (possible regression)
+- history: adjust log patterns for pacemaker v1.1.8
+- history: fix setting up the timeframe alias for limit
+- history: fix unpacking reports specified without directory
+- history: add log subcommand to transition
+- build: pcmk.pc renamed to pacemaker.pc in pacemaker v1.1.8
+
+* Mon Oct 15 2012 Dejan Muhamedagic <dejan at suse.de> and many others
+- stable release 1.2.1
+- cibconfig: show error message on id in use
+- cibconfig: repair edit for non-vi users
+- cibconfig: update schema separately (don't remove the status section)
+- cibconfig: node type is optional now
+- ui: readd quotes for single-shot commands
+- ra: manage without glue installed (savannah#37560)
+- ra: improve support for RH fencing-agents
+- ra: add support for crm_resource
+- history: remove keyword 'as' which is not compatible with python
+  2.4 (savannah#37534)
+- history: add the exclude (log messages) command
+- history: pacemaker 1.1.8 compatibility code
+- utils: exit code of cibadmin -Q on no section changed in 1.1.8
+- some more pacemaker 1.1.8 compatibility code
+
+* Tue Sep 18 2012 Dejan Muhamedagic <dejan at suse.de> and many others
+- stable release 1.2.0
+- cibconfig: support the kind attribute in orders
+- cibconfig: implement node-attribute in collocations
+- cibconfig: support require-all in resource sets
+- cibconfig: support for fencing-topology
+- cibconfig: new schema command
+- rsctest: resource testing
+- history: implement session saving
+- history: add alias (timeframe) for the limit command
+- xml: support for RNG schema
+- site: ticket standby and activate commands
+- site: update interface to crm_ticket
+- cibstatus: ticket management
+- ui: add vim syntax highlighting support
+- xml: retrieve data from schema (lf#2092)
+- stonith: support rhcs fence-agents (bnc#769724)
+- ticket: fix redirecting rsc references in tickets (bnc#763465)
+- ui: import readline only when needed (don't print ".[?1034h")
+- ui: don't accept non-ascii input (lf#2597)
+- ui: enable wait (option -w) for single-shot configure commands
+- shadow: calculate shadow directory just like crm_shadow (bnc#759056)
+- utils: improve terminal output height calculation (pager)
+- utils: use crm_simulate if ptest is not available
+- utils: repair ptest usage (bnc#736212)
+- utils: prevent spurious error messages if an element doesn't
+  exist in CIB (bnc#723677)
+- cibconfig: drop attributes set to default on cib import
+- cibconfig: support setting attributes in resource sets
+- cibconfig: display referenced attr set ids (lf#2304)
+- cibconfig: don't verify parameters starting with '$'
+- cibconfig: fix meta attributes verify for container elements (lf#2555)
+- cibconfig: test for duplicate monitor intervals (lf#2586)
+- cibconfig: don't skip monitor operations on verify
+- cibconfig: use uname instead of id when listing nodes (cl#5043)
+- cibconfig: repair resource parameter uniqueness test
+- cibconfig: repair ability to manage multiple rsc/op_defaults (bnc#737812)
+- cibconfig: remove also elements which depend on the resource
+  template which is to be deleted (bnc#730404)
+- cibconfig: report error if a referenced template in primitive
+  doesn't exist (bnc#730404)
+- cibconfig: exchange rsc and with-rsc after converting collocation
+  sets to standard constraints (bnc#729628)
+- cibconfig: convert resource sets to standard constraints on
+  resource removal (bnc#729628)
+- ra: don't require certain parameters for rhcs stonith resources
+- ra: use only effective UID when choosing RA interface
+- ra: always use lrmadmin with glue 1.0.10 (cl#5036)
+- ra: fix start/stop interval test
+- completion: add command aliases to completion tables (cl#5013)
+- completion: add templates as possible resource references in
+  constraints
+- history: improve limiting the report time period
+- history: tune resource match patterns
+- history: reset time period when setting source
+- history: add clone/ms resources to events (fixes the transition command)
+- history: expand clones and ms in the resource command (bnc#729631)
+- history: don't assume that a hb_report tarball name matches the
+  top directory name
+- history: handle non-existing source better (bnc#728346)
+- history: fix regression when fetching new PE inputs (bnc#723417)
+- history: use debug severity for repeating messages (bnc#726611)
+- help: page overview help screens
+- help: append slash to levels in overview help screen
+- help: add '?' as alias for help
+- help: add topics to the help system
+- doc: describe deficiency in the configure edit command (bnc#715698)
+- move user files to standard locations (XDG)
+- build: add optional regression testing on rpm build
+- build: fetch the daemon location from glue-config.h
+
+* Wed Oct 19 2011 Dejan Muhamedagic <dejan at suse.de> and many others
+- stable release 1.1.0
+- history/troubleshooting support
+- template support
+- geo-cluster support commands
+- support for configure rsc_ticket
+- support for LRM secrets at the resource level
+- enable removal of unmanaged resources (bnc#696506)
+- split-off from Pacemaker after release 1.1.6
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..4803bc9
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,47 @@
+#
+# shell: Pacemaker code
+#
+# Copyright (C) 2008 Andrew Beekhof
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in aclocal.m4 configure
+
+sbin_SCRIPTS		= crm
+
+EXTRA_DIST		= crm
+
+SUBDIRS = doc modules templates test contrib hb_report utils scripts
+
+doc_DATA = AUTHORS COPYING README ChangeLog
+
+crmversiondir=$(datadir)/@PACKAGE@
+crmversion_DATA = version
+
+# in .spec, set --sysconfdir=/etc
+crmconfdir=$(sysconfdir)/crm
+crmconf_DATA = crm.conf
+
+install-exec-local:
+	$(INSTALL) -d -m 770 $(DESTDIR)/$(CRM_CACHE_DIR)
+	-chown $(CRM_DAEMON_USER):$(CRM_DAEMON_GROUP) $(DESTDIR)/$(CRM_CACHE_DIR)
+
+clean-generic:
+	rm -f $(TARFILE) *.tar.bz2 *.sed
+
+dist-clean-local:
+	rm -f autoconf automake autoheader
+
+.PHONY: rpm pkg handy handy-copy
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..e69de29
diff --git a/README b/README
new file mode 100644
index 0000000..85befd8
--- /dev/null
+++ b/README
@@ -0,0 +1,58 @@
+# INTRODUCTION
+
+crmsh is a command-line interface for High-Availability cluster
+management on GNU/Linux systems, and part of the Clusterlabs
+project. It simplifies the configuration, management and
+troubleshooting of Pacemaker-based clusters, by providing a powerful
+and intuitive set of features.
+
+crmsh can function both as an interactive shell with tab completion
+and inline documentation, and as a command-line tool. It can also be
+used in batch mode to execute commands from files.
+
+# MORE INFORMATION
+
+The website for crmsh is:
+
+  * http://crmsh.github.io/
+
+Documentation for the latest stable release is found at:
+
+  * http://crmsh.github.io/documentation/
+
+# DEVELOPMENT
+
+crmsh is implemented in Python. The source code for crmsh is kept in a
+git source repository. To check out the latest development
+version, install git and run this command:
+
+    git clone https://github.com/ClusterLabs/crmsh
+
+Bugs and issues can be reported here:
+
+ * https://github.com/ClusterLabs/crmsh/issues
+
+Any other questions or comments can be made on the Clusterlabs users
+mailing list at:
+
+ * http://clusterlabs.org/mailman/listinfo/users
+
+# INSTALLING
+
+Autoconf is used to take care of platform dependendent locations.
+It is mainly inherited from the Pacemaker source.
+
+    ./autogen.sh
+    ./configure
+    make
+    make install
+
+# MANIFEST
+
+    ./doc: man page, source for the website and other documentation
+    ./modules: the code
+    ./templates: configuration templates
+    ./test: unit tests and regression tests
+    ./contrib: vim highlighting scripts and other semi-related
+               contributions
+    ./hb_report: log file collection and analysis tool
diff --git a/README.dev b/README.dev
new file mode 100644
index 0000000..aaf5eb3
--- /dev/null
+++ b/README.dev
@@ -0,0 +1,263 @@
+== The manifest
+
+This is the list of all modules including short descriptions.
+
+=== Main
+
+crm::
+
+	The program. Not much here.
+
+modules/main.py::
+
+	Start, verify environment, compatibility with various
+	software versions, read and parse options, load user
+	preferences, parse user's input (lexer is shlex).
+
+modules/levels.py::
+
+	Levels (collections of commands) hierarchy. Takes care of the
+	prompt and moving back and forth through levels.
+
+=== User interface
+
+modules/ui.py::
+
+	User interface. All levels and commands are implemented here.
+	The starting point is the +TopLevel+ class (the root level).
+	For instance, other +UserInterface+ subclasses include
+	+RscMgmt+, +RA+, and +CibConfig+. The code should be mostly
+	straightforward.
+
+modules/completion.py::
+
+	Tab completion for interactive use. The list of all
+	completers is in the +completer_lists+ dictionary. It is used
+	by +Levels+ to create a completion table. Can show parts of
+	the RA metadata or other help texts. Quite convoluted at some
+	spots and otherwise trivial.
+
+modules/help.py::
+
+	Reads help from a text file and presents parts of it in
+	response to the help command. The text file has special
+	anchors to demarcate help topics and command help text. See
+	the +HelpSystem+ class for more information.
+
+doc/crm.8.txt::
+
+	Online help in asciidoc format. Several help topics (search
+	for +[[topic_+) and command reference (search for
+	+[[cmdhelp_+). Every user interface change needs to be
+	reflected here. _Actually, every user interface change has to
+	start here_. A source for the +crm(8)+ man page too.
+
+=== Global variables
+
+modules/config.py.in::
+
+    Compile-time constants defined during the build process.
+
+modules/constants.py::
+
+	Global constants (mostly) and (a few) variables (it would be
+	good to separate the two).
+
+modules/userprefs.py::
+
+	User preferences. Keeps also user options passed on the
+	command line.
+
+=== CIB configuration editing and management
+
+modules/cibconfig.py::
+
+	Configuration (CIB) manager. Implements the configure level.
+	The bigest and the most complex part. There are three major
+	classes:
+
+	+CibFactory+::: operations on the CIB or parts of it.
+
+	+CibObject+::: every CIB element is implemented in a
+	subclass of +CibObject+. The configuration consists of a
+	set of +CibObject+ instances (subclassed, e.g. +CibNode+ or
+	+CibPrimitive+).
+
+	+CibObjectSet+::: enables operations on sets of CIB
+	elements. Two subclasses with CLI and XML presentations
+	of cib elements. Most operations are going via these
+	subclasses (+show+, +edit+, +save+, +filter+).
+
+modules/idmgmt.py::
+
+	CIB id management. Guarantees that all ids are unique.
+	A helper for CibFactory.
+
+modules/parse.py::
+
+	CIB elements CLI parser.
+
+modules/cliformat.py::
+
+	A set of functions to format CIB elements (XML -> CLI
+	converter).
+
+modules/clidisplay.py::
+
+	Embelishment class for the terminal.
+
+modules/crm_gv.py::
+
+	Interface to GraphViz. Generates graph specs for dotty(1).
+
+=== CIB status editing
+
+modules/cibstatus.py::
+
+	CIB status section editor and manipulator (cibstatus
+	level). Interface to crm_simulate.
+
+=== Resource agents
+
+modules/ra.py::
+
+	Resource agents interface.
+
+modules/rsctest.py::
+
+	Resource tester (configure rsctest command).
+
+=== Cluster history
+
+modules/report.py::
+
+	Cluster history. Interface to logs and other artifacts left
+	on disk by the cluster.
+
+modules/log_patterns.py, log_patterns_118.py::
+
+	Pacemaker subsystems' log patterns. For versions earlier than
+	1.1.8 and the latter.
+
+=== Auxiliary
+
+modules/term.py::
+
+	Terminal driver (from activestate).
+
+modules/schema.py, pacemaker.py::
+
+	Support for pacemaker RNG schema.
+
+modules/cache.py::
+	A very rudimentary cache implementation. Used to cache
+	results of expensive operations (i.e. ra meta).
+
+modules/crm_pssh.py::
+
+	Interface to pssh (parallel ssh).
+
+modules/msg.py::
+
+	Messages for users. Can count lines and include line
+	numbers. Needs refinement.
+
+modules/utils.py::
+
+	A bag of useful functions. Needs more order.
+
+modules/xmlutil.py::
+
+	A bag of useful XML functions. Needs more order.
+
+== Code improvements
+
+These are some thoughts on how to improve maintainability and
+make crmsh nicer. Mostly for people looking at the code, the
+users shouldn't notice much (or any) difference.
+
+Everybody's invited to comment and make further suggestions, in
+particular experienced pythonistas.
+
+=== Parser
+
+	- the current parser is just awful
+
+	- the parser should be implemented in one of the existing
+	  python parsing libraries/tools, such as PLY or pyparsing
+	  (need to investigate which would be the easiest to apply
+	  for the crmsh language)
+
+	- proper parser should allow easier updates and easier
+	  language extension (currently, crmsh doesn't support
+	  some date rule constructs)
+
+	- make sure that the new parser is not significantly slower
+	  from the existing!
+
+=== Syntax highlighting
+
+	- syntax highlighting is done before producing output, which
+	  is basically wrong and makes code convoluted; it further
+	  makes extra processing more difficult
+
+	- use a python library (pygments seems to be the best
+	  candidate); that should also allow other output formats
+	  (not only terminal)
+
+	- how to extend pygments to understand a new language? it'd
+	  be good to be able to get this _without_ pushing the parser
+	  upstream (that would take _long_ to propagate to
+	  distributions)
+
+=== Language class
+
+	- the crmsh language is packed just as a list of lists of
+	  lists or thereabouts, which is not very convenient (in
+	  particular for debugging); it actually used to be a dict,
+	  then dicts wouldn't do as they don't guarantee order and I
+	  didn't know at the time about ordered dictionaries
+
+	- a class to capture the language should help
+
+=== Configuration edit is complex
+
+	- at the time it didn't seem like anything less would do, but
+	  it's worth revising
+	  (done in changeset 12acfbfe94c6)
+
+=== XML production is ugly
+
+	- due in part to preserving all XML ids (which may not be
+	  necessary, but makes comparing XMLs for equality easier)
+
+	- some kind of production rules set and a general XML machine
+	  would be preferable
+
+=== CibFactory is huge
+
+	- this is a single central CIB class, it'd be good to have it
+	  split into several smaller classes (how?)
+
+=== The element create/update procedure is complex
+
+	- not sure how to improve this
+
+=== Bad namespace separation
+
+	- xmlutil and utils are just a loose collection of functions,
+	  need to be organized better (get rid of 'from xyz import *')
+
+=== Add more comments
+
+	- in particular describe how CibObjectSet, CibObject, and
+	  CibFactory work together (that's the core of the configure
+	  level)
+
+=== Fix incorrect use of CibFactory.is_cib_sane()
+
+    - This function should only be used internally in CibFactory, and
+      should be called from every entry point into the class: It
+      handles lazy initialization of the cib. Right now it is used
+      from ui.py as well. Also, the error handling if it fails
+      needs to be cleaned up.
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..7dacce6
--- /dev/null
+++ b/TODO
@@ -0,0 +1,32 @@
+Features
+
+. Audit
+
+	- add user auditing, i.e. save all commands that were run
+
+	- save to a local file (distributed DB would probably be an
+	  overkill)
+
+. Cluster documentation
+
+	- one of the more recent features is graph capability
+	  (graphviz) which is a very good step in terms of cluster
+	  documentation; need to extend that with some textual
+	  cluster description and perhaps history and such
+
+	- everybody likes reports (and in particular your boss)
+
+	- this feature needs very careful consideration
+
+. CIB features
+
+    - Support ACL commands in Pacemaker 1.1.12>
+
+. Command features
+
+    - Relative commands: /status from configure, ../resource stop foo
+      from configure, cib/new from configure... for example.
+
+      Tricky part: Have to push/pop levels invisibly, resource
+      commands modify CIB while CIB is edited in configure. Similar
+      races could occur with other commands.
\ No newline at end of file
diff --git a/acinclude.m4 b/acinclude.m4
new file mode 100644
index 0000000..fa8fef2
--- /dev/null
+++ b/acinclude.m4
@@ -0,0 +1,39 @@
+dnl
+dnl local autoconf/automake macros needed for heartbeat
+dnl	Started by David Lee <t.d.lee at durham.ac.uk> February 2006
+dnl
+dnl License: GNU General Public License (GPL)
+
+
+dnl AM_CHECK_PYTHON_HEADERS:  Find location of python include files.
+dnl Taken from:
+dnl	http://source.macgimp.org/
+dnl which is GPL and is attributed to James Henstridge.
+dnl
+dnl AM_CHECK_PYTHON_HEADERS([ACTION-IF-POSSIBLE], [ACTION-IF-NOT-POSSIBLE])
+dnl Imports:
+dnl	$PYTHON
+dnl Exports:
+dnl	PYTHON_INCLUDES
+
+AC_DEFUN([AM_CHECK_PYTHON_HEADERS],
+[AC_REQUIRE([AM_PATH_PYTHON])
+AC_MSG_CHECKING(for headers required to compile python extensions)
+dnl deduce PYTHON_INCLUDES
+py_prefix=`$PYTHON -c "import sys; print sys.prefix"`
+py_exec_prefix=`$PYTHON -c "import sys; print sys.exec_prefix"`
+PYTHON_INCLUDES="-I${py_prefix}/include/python${PYTHON_VERSION}"
+if test "$py_prefix" != "$py_exec_prefix"; then
+  PYTHON_INCLUDES="$PYTHON_INCLUDES -I${py_exec_prefix}/include/python${PYTHON_VERSION}"
+fi
+AC_SUBST(PYTHON_INCLUDES)
+dnl check if the headers exist:
+save_CPPFLAGS="$CPPFLAGS"
+CPPFLAGS="$CPPFLAGS $PYTHON_INCLUDES"
+AC_TRY_CPP([#include <Python.h>],dnl
+[AC_MSG_RESULT(found)
+$1],dnl
+[AC_MSG_RESULT(not found)
+$2])
+CPPFLAGS="$save_CPPFLAGS"
+])
diff --git a/autogen.sh b/autogen.sh
new file mode 100755
index 0000000..f92ecc3
--- /dev/null
+++ b/autogen.sh
@@ -0,0 +1,144 @@
+#!/bin/sh
+#
+#	License: GNU General Public License (GPL)
+#	Copyright 2001 horms <horms at vergenet.net>
+#		(heavily mangled by alanr)
+#
+#	bootstrap: set up the project and get it ready to make
+#
+#	Basically, we run autoconf and automake in the
+#	right way to get things set up for this environment.
+#
+#	We also look and see if those tools are installed, and
+#	tell you where to get them if they're not.
+#
+#	Our goal is to not require dragging along anything
+#	more than we need.  If this doesn't work on your system,
+#	(i.e., your /bin/sh is broken) send us a patch.
+#
+#	This code loosely based on the corresponding named script in
+#	enlightenment, and also on the sort-of-standard autoconf
+#	bootstrap script.
+
+# Run this to generate all the initial makefiles, etc.
+
+testProgram()
+{
+  cmd=$1
+
+  if [ -z "$cmd" ]; then
+    return 1;
+  fi
+
+  arch=`uname -s`
+
+  # Make sure the which is in an if-block... on some platforms it throws exceptions
+  #
+  # The ERR trap is not executed if the failed command is part
+  #   of an until or while loop, part of an if statement, part of a &&
+  #   or  ||  list.
+  if
+     which $cmd  </dev/null >/dev/null 2>&1
+  then
+      :
+  else
+      return 1
+  fi
+
+  # The GNU standard is --version
+  if
+      $cmd --version </dev/null >/dev/null 2>&1
+  then
+      return 0
+  fi
+
+  # Maybe it suppports -V instead
+  if
+      $cmd -V </dev/null >/dev/null 2>&1
+  then
+      return 0
+  fi
+
+  # Nope, the program seems broken
+  return 1
+}
+
+gnu="ftp://ftp.gnu.org/pub/gnu"
+
+for command in autoconf213 autoconf253 autoconf259 autoconf
+do
+  if
+      testProgram $command == 1
+  then
+    autoconf=$command
+    autoheader=`echo  "$autoconf" | sed -e 's/autoconf/autoheader/'`
+    autom4te=`echo  "$autoconf" | sed -e 's/autoconf/autmo4te/'`
+    autoreconf=`echo  "$autoconf" | sed -e 's/autoconf/autoreconf/'`
+    autoscan=`echo  "$autoconf" | sed -e 's/autoconf/autoscan/'`
+    autoupdate=`echo  "$autoconf" | sed -e 's/autoconf/autoupdate/'`
+    ifnames=`echo  "$autoconf" | sed -e 's/autoconf/ifnames/'`
+  fi
+done
+
+for command in automake14 automake-1.4 automake15 automake-1.5 automake17 automake-1.7 automake19 automake-1.9 automake-1.11 automake
+do
+  if
+      testProgram $command
+  then
+    : OK $pkg is installed
+    automake=$command
+    aclocal=`echo  "$automake" | sed -e 's/automake/aclocal/'`
+  fi
+done
+
+if [ -z $autoconf ]; then
+    echo You must have autoconf installed to compile the crmsh package.
+    echo Download the appropriate package for your system,
+    echo or get the source tarball at: $gnu/autoconf/
+    exit 1
+
+elif [ -z $automake ]; then
+    echo You must have automake installed to compile the crmsh package.
+    echo Download the appropriate package for your system,
+    echo or get the source tarball at: $gnu/automake/
+    exit 1
+
+fi
+
+# Create local copies so that the incremental updates will work.
+rm -f ./autoconf ./automake ./autoheader
+ln -s `which $autoconf` ./autoconf
+ln -s `which $automake` ./automake
+ln -s `which $autoheader` ./autoheader
+
+printf "$autoconf:\t"
+$autoconf --version | head -n 1
+
+printf "$automake:\t"
+$automake --version | head -n 1
+
+arch=`uname -s`
+# Disable the errors on FreeBSD until a fix can be found.
+if [ ! "$arch" = "FreeBSD" ]; then
+set -e
+#
+#	All errors are fatal from here on out...
+#	The shell will complain and exit on any "uncaught" error code.
+#
+#
+#	And the trap will ensure sure some kind of error message comes out.
+#
+trap 'echo ""; echo "$0 exiting due to error (sorry!)." >&2' 0
+fi
+
+echo $aclocal $ACLOCAL_FLAGS
+$aclocal $ACLOCAL_FLAGS
+
+echo $automake --add-missing --include-deps --copy
+$automake --add-missing --include-deps --copy
+
+echo $autoconf
+$autoconf
+
+echo Now run ./configure
+trap '' 0
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..747301e
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,297 @@
+dnl
+dnl autoconf for CRM shell
+dnl
+dnl Copyright (C) 2008 Andrew Beekhof
+dnl
+dnl License: GNU General Public License (GPL)
+
+dnl ===============================================
+dnl Bootstrap 
+dnl ===============================================
+AC_PREREQ(2.53)
+
+dnl Suggested structure:
+dnl     information on the package
+dnl     checks for programs
+dnl     checks for libraries
+dnl     checks for header files
+dnl     checks for types
+dnl     checks for structures
+dnl     checks for compiler characteristics
+dnl     checks for library functions
+dnl     checks for system services
+
+AC_INIT([crmsh],[2.1.3],[users at clusterlabs.org])
+
+AC_ARG_WITH(version,
+    [  --with-version=version   Override package version (if you're a packager needing to pretend) ],
+    [ PACKAGE_VERSION="$withval" ])
+
+AC_ARG_WITH(pkg-name,
+    [  --with-pkg-name=name     Override package name (if you're a packager needing to pretend) ],
+    [ PACKAGE_NAME="$withval" ])
+
+AM_INIT_AUTOMAKE($PACKAGE_NAME, $PACKAGE_VERSION)
+AC_DEFINE_UNQUOTED(PACEMAKER_VERSION, "$PACKAGE_VERSION", Current crmsh version)
+
+PACKAGE_SERIES=`echo $PACKAGE_VERSION | awk -F. '{ print $1"."$2 }'`
+AC_SUBST(PACKAGE_SERIES)
+
+dnl automake >= 1.11 offers --enable-silent-rules for suppressing the output from
+dnl normal compilation.  When a failure occurs, it will then display the full 
+dnl command line
+dnl Wrap in m4_ifdef to avoid breaking on older platforms
+m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES])
+
+dnl ===============================================
+dnl Helpers 
+dnl ===============================================
+extract_header_define() {
+	  AC_MSG_CHECKING(for $2 in $1)
+	  Cfile=$srcdir/extract_define.$2.${$}
+	  printf "#include <stdio.h>\n" > ${Cfile}.c
+	  printf "#include <%s>\n" $1 >> ${Cfile}.c
+	  printf "int main(int argc, char **argv) { printf(\"%%s\", %s); return 0; }\n" $2 >> ${Cfile}.c
+	  $CC $CFLAGS ${Cfile}.c -o ${Cfile}
+	  value=`${Cfile}`
+	  AC_MSG_RESULT($value)
+	  printf $value
+	  rm -f ${Cfile}.c ${Cfile}
+	}
+
+dnl ===============================================
+dnl General Processing
+dnl ===============================================
+
+INIT_EXT=""
+echo Our Host OS: $host_os/$host
+
+
+AC_MSG_NOTICE(Sanitizing prefix: ${prefix})
+case $prefix in
+  NONE)	prefix=/usr;;
+esac
+
+AC_MSG_NOTICE(Sanitizing exec_prefix: ${exec_prefix})
+case $exec_prefix in
+  dnl For consistency with Heartbeat, map NONE->$prefix
+  NONE)	  exec_prefix=$prefix;;
+  prefix) exec_prefix=$prefix;;
+esac
+
+AC_MSG_NOTICE(Sanitizing libdir: ${libdir})
+case $libdir in
+  dnl For consistency with Heartbeat, map NONE->$prefix
+  *prefix*|NONE)
+    AC_MSG_CHECKING(which lib directory to use)
+    for aDir in lib64 lib
+    do
+      trydir="${exec_prefix}/${aDir}"
+      if
+        test -d ${trydir}
+      then
+        libdir=${trydir}
+        break
+      fi
+    done
+    AC_MSG_RESULT($libdir);
+    ;;
+esac
+
+dnl Expand autoconf variables so that we dont end up with '${prefix}' 
+dnl in #defines and python scripts
+dnl NOTE: Autoconf deliberately leaves them unexpanded to allow
+dnl    make exec_prefix=/foo install
+dnl No longer being able to do this seems like no great loss to me...
+
+eval prefix="`eval echo ${prefix}`"
+eval exec_prefix="`eval echo ${exec_prefix}`"
+eval bindir="`eval echo ${bindir}`"
+eval sbindir="`eval echo ${sbindir}`"
+eval libexecdir="`eval echo ${libexecdir}`"
+eval datadir="`eval echo ${datadir}`"
+eval sysconfdir="`eval echo ${sysconfdir}`"
+eval sharedstatedir="`eval echo ${sharedstatedir}`"
+eval localstatedir="`eval echo ${localstatedir}`"
+eval libdir="`eval echo ${libdir}`"
+eval infodir="`eval echo ${infodir}`"
+eval mandir="`eval echo ${mandir}`"
+
+dnl Home-grown variables
+eval docdir="`eval echo ${docdir}`"
+if test x"${docdir}" = x""; then
+   docdir=${datadir}/doc/${PACKAGE}-${VERSION}
+   #docdir=${datadir}/doc/packages/${PACKAGE}
+fi
+AC_SUBST(docdir)
+
+CFLAGS="$CFLAGS -I${prefix}/include/heartbeat -I${prefix}/include/pacemaker"
+
+for j in prefix exec_prefix bindir sbindir libexecdir datadir sysconfdir \
+    sharedstatedir localstatedir libdir infodir \
+    mandir docdir
+do 
+  dirname=`eval echo '${'${j}'}'`
+  if
+    test ! -d "$dirname"
+  then
+    AC_MSG_WARN([$j directory ($dirname) does not exist!])
+  fi
+done
+
+AC_CHECK_HEADERS(crm_config.h)
+AC_CHECK_HEADERS(glue_config.h)
+GLUE_HEADER=none
+if test "$ac_cv_header_glue_config_h" = "yes";  then
+   GLUE_HEADER=glue_config.h
+
+elif test "$ac_cv_header_hb_config_h" = "yes"; then
+   GLUE_HEADER=hb_config.h
+
+else
+   AC_MSG_FAILURE(Core development headers were not found)
+fi
+
+dnl Variables needed for substitution
+CRM_DAEMON_USER=`extract_header_define crm_config.h CRM_DAEMON_USER`
+AC_DEFINE_UNQUOTED(CRM_DAEMON_USER,"$CRM_DAEMON_USER", User to run Pacemaker daemons as)
+AC_SUBST(CRM_DAEMON_USER)
+
+CRM_DAEMON_GROUP=`extract_header_define crm_config.h CRM_DAEMON_GROUP`
+AC_DEFINE_UNQUOTED(CRM_DAEMON_GROUP,"$CRM_DAEMON_GROUP", Group to run Pacemaker daemons as)
+AC_SUBST(CRM_DAEMON_GROUP)
+
+CRM_STATE_DIR=`extract_header_define crm_config.h CRM_STATE_DIR`
+AC_DEFINE_UNQUOTED(CRM_STATE_DIR,"$CRM_STATE_DIR", Where to keep state files and sockets)
+AC_SUBST(CRM_STATE_DIR)
+
+PE_STATE_DIR=`extract_header_define crm_config.h PE_STATE_DIR`
+AC_DEFINE_UNQUOTED(PE_STATE_DIR,"$PE_STATE_DIR", Where to keep PEngine outputs)
+AC_SUBST(PE_STATE_DIR)
+
+dnl Eventually move out of the heartbeat dir tree and create compatability code
+CRM_CONFIG_DIR=`extract_header_define crm_config.h CRM_CONFIG_DIR`
+AC_DEFINE_UNQUOTED(CRM_CONFIG_DIR,"$CRM_CONFIG_DIR", Where to keep CIB configuration files)
+AC_SUBST(CRM_CONFIG_DIR)
+
+CRM_DTD_DIRECTORY=`extract_header_define crm_config.h CRM_DTD_DIRECTORY`
+AC_DEFINE_UNQUOTED(CRM_DTD_DIRECTORY,"$CRM_DTD_DIRECTORY", Where to keep CIB configuration files)
+AC_SUBST(CRM_DTD_DIRECTORY)
+
+AC_PATH_PROGS(PKGCONFIG, pkg-config)
+if test x"${PKGCONFIG}" = x""; then
+   AC_MSG_ERROR(You need pkgconfig installed in order to build ${PACKAGE})
+fi
+
+CRM_DAEMON_DIR=`$PKGCONFIG pcmk --variable=daemondir`
+if test "X$CRM_DAEMON_DIR" = X; then
+	CRM_DAEMON_DIR=`$PKGCONFIG pacemaker --variable=daemondir`
+fi
+if test "X$CRM_DAEMON_DIR" = X; then
+	CRM_DAEMON_DIR=`extract_header_define $GLUE_HEADER GLUE_DAEMON_DIR`
+fi
+AC_DEFINE_UNQUOTED(CRM_DAEMON_DIR,"$CRM_DAEMON_DIR", Location for Pacemaker daemons)
+AC_SUBST(CRM_DAEMON_DIR)
+
+CRM_CACHE_DIR=${localstatedir}/cache/crm
+AC_DEFINE_UNQUOTED(CRM_CACHE_DIR,"$CRM_CACHE_DIR", Where crm shell keeps the cache)
+AC_SUBST(CRM_CACHE_DIR)
+
+dnl Needed for the location of hostcache in CTS.py
+HA_VARLIBHBDIR=`extract_header_define $GLUE_HEADER HA_VARLIBHBDIR`
+AC_SUBST(HA_VARLIBHBDIR)
+
+OCF_ROOT_DIR=`extract_header_define $GLUE_HEADER OCF_ROOT_DIR`
+if test "X$OCF_ROOT_DIR" = X; then
+  AC_MSG_ERROR(Could not locate OCF directory)
+fi
+AC_SUBST(OCF_ROOT_DIR)
+
+AC_PATH_PROGS(HG, hg false)
+AC_PATH_PROGS(GIT, git false)
+AC_MSG_CHECKING(build version)
+BUILD_VERSION=unknown
+if test -f $srcdir/.hg_archival.txt; then
+   BUILD_VERSION=`cat $srcdir/.hg_archival.txt | awk '/node:/ { print $2 }'`
+elif test -x $HG -a -d .hg; then
+   BUILD_VERSION=`$HG id -i`
+   if test $? != 0; then
+       BUILD_VERSION=unknown
+   fi
+elif test -x $GIT -a -d .git; then
+   BUILD_VERSION=`$GIT describe`
+   if test $? != 0; then
+       BUILD_VERSION=unknown
+   fi
+fi
+
+AC_DEFINE_UNQUOTED(BUILD_VERSION, "$BUILD_VERSION", Build version)
+AC_MSG_RESULT($BUILD_VERSION)
+AC_SUBST(BUILD_VERSION)
+
+dnl ===============================================
+dnl Program Paths
+dnl ===============================================
+
+PATH="$PATH:/sbin:/usr/sbin:/usr/local/sbin:/usr/local/bin"
+export PATH
+
+dnl Replacing AC_PROG_LIBTOOL with AC_CHECK_PROG because LIBTOOL
+dnl was NOT being expanded all the time thus causing things to fail.
+AM_PATH_PYTHON
+AC_PATH_PROGS(ASCIIDOC, asciidoc)
+AC_PATH_PROGS(SSH, ssh, /usr/bin/ssh)
+AC_PATH_PROGS(SCP, scp, /usr/bin/scp)
+AC_PATH_PROGS(HG, hg, /bin/false)
+AC_PATH_PROGS(TAR, tar)
+AC_PATH_PROGS(MD5, md5)
+AC_PATH_PROGS(TEST, test)
+
+AM_CONDITIONAL(BUILD_ASCIIDOC, test x"${ASCIIDOC}" != x"")
+if test x"${ASCIIDOC}" != x""; then
+   PKG_FEATURES="$PKG_FEATURES ascii-docs"
+fi
+
+dnl The Makefiles and shell scripts we output
+AC_CONFIG_FILES(Makefile \
+doc/Makefile             \
+contrib/Makefile         \
+templates/Makefile       \
+utils/Makefile           \
+scripts/Makefile         \
+scripts/health/Makefile  \
+scripts/check-uptime/Makefile  \
+scripts/init/Makefile  \
+scripts/add/Makefile  \
+scripts/remove/Makefile  \
+test/Makefile            \
+test/testcases/Makefile  \
+test/cibtests/Makefile   \
+modules/Makefile         \
+hb_report/Makefile       \
+hb_report/hb_report      \
+crm.conf                 \
+version                  \
+)
+
+dnl Now process the entire list of files added by previous 
+dnl  calls to AC_CONFIG_FILES()
+AC_OUTPUT()
+
+dnl *****************
+dnl Configure summary
+dnl *****************
+
+AC_MSG_RESULT([])
+AC_MSG_RESULT([$PACKAGE configuration:])
+AC_MSG_RESULT([  Version                  = ${VERSION} (Build: $BUILD_VERSION)])
+AC_MSG_RESULT([  Features                 =${PKG_FEATURES}])
+AC_MSG_RESULT([])
+AC_MSG_RESULT([  Prefix                   = ${prefix}])
+AC_MSG_RESULT([  Executables              = ${sbindir}])
+AC_MSG_RESULT([  Man pages                = ${mandir}])
+AC_MSG_RESULT([  Libraries                = ${libdir}])
+AC_MSG_RESULT([  Header files             = ${includedir}])
+AC_MSG_RESULT([  Arch-independent files   = ${datadir}])
+AC_MSG_RESULT([  State information        = ${localstatedir}])
+AC_MSG_RESULT([  System configuration     = ${sysconfdir}])
diff --git a/contrib/Makefile.am b/contrib/Makefile.am
new file mode 100644
index 0000000..96a33f3
--- /dev/null
+++ b/contrib/Makefile.am
@@ -0,0 +1,25 @@
+#
+# contrib: crmsh contrib
+#
+# Copyright (C) 2013 Dejan Muhamedagic
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+contribdir      = $(docdir)/contrib
+contrib_DATA	= pacemaker-crm.vim  pcmk.vim  README.vimsyntax
+
+EXTRA_DIST		= $(contrib_DATA)
diff --git a/contrib/README.vimsyntax b/contrib/README.vimsyntax
new file mode 100644
index 0000000..fc92fff
--- /dev/null
+++ b/contrib/README.vimsyntax
@@ -0,0 +1,24 @@
+There are two VIM syntax files contributed:
+
+pacemaker-crm.vim
+pcmk.vim
+
+Neither matches colours used in crm configure show and both need
+to be improved. Still, you may want to edit a more colorful
+configuration. To have that in "crm configure edit" do the
+following:
+
+Copy one of them to ~/.vim/syntax/pcmk.vim.
+
+Don't forget to put "syntax on" in the VIM rc file (~/.vimrc or
+~/.exrc).
+
+If you're editing a file directly, just type:
+
+:setf pcmk
+
+Many thanks to the contributors:
+
+Trevor Hemsley <themsley at voiceflex.com>
+Dan Frincu <df.cluster at gmail.com>
+Lars Ellenberg <lars at linbit.com>
diff --git a/contrib/bash_completion.sh b/contrib/bash_completion.sh
new file mode 100644
index 0000000..01501e2
--- /dev/null
+++ b/contrib/bash_completion.sh
@@ -0,0 +1,248 @@
+#-*- mode: shell-script;-*-
+#
+# bash completion support for crmsh.
+#
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+# Conceptually based on gitcompletion (http://gitweb.hawaga.org.uk/).
+# Distributed under the GNU General Public License, version 2.0.
+#
+# To use these routines:
+#
+#    1) Copy this file to somewhere (e.g. ~/.crm-completion.sh).
+#    2) Add the following line to your .bashrc/.zshrc:
+#        source ~/.crm-completion.sh
+
+shopt -s extglob
+
+# The following function is based on code from:
+#
+#   bash_completion - programmable completion functions for bash 3.2+
+#
+#   Copyright © 2006-2008, Ian Macdonald <ian at caliban.org>
+#             © 2009-2010, Bash Completion Maintainers
+#                     <bash-completion-devel at lists.alioth.debian.org>
+#
+#   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, 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.
+#
+#   The latest version of this software can be obtained here:
+#
+#   http://bash-completion.alioth.debian.org/
+#
+#   RELEASE: 2.x
+
+# This function can be used to access a tokenized list of words
+# on the command line:
+#
+#       __git_reassemble_comp_words_by_ref '=:'
+#       if test "${words_[cword_-1]}" = -w
+#       then
+#               ...
+#       fi
+#
+# The argument should be a collection of characters from the list of
+# word completion separators (COMP_WORDBREAKS) to treat as ordinary
+# characters.
+#
+# This is roughly equivalent to going back in time and setting
+# COMP_WORDBREAKS to exclude those characters.  The intent is to
+# make option types like --date=<type> and <rev>:<path> easy to
+# recognize by treating each shell word as a single token.
+#
+# It is best not to set COMP_WORDBREAKS directly because the value is
+# shared with other completion scripts.  By the time the completion
+# function gets called, COMP_WORDS has already been populated so local
+# changes to COMP_WORDBREAKS have no effect.
+#
+# Output: words_, cword_, cur_.
+
+__crm_reassemble_comp_words_by_ref()
+{
+        local exclude i j first
+        # Which word separators to exclude?
+        exclude="${1//[^$COMP_WORDBREAKS]}"
+        cword_=$COMP_CWORD
+        if [ -z "$exclude" ]; then
+                words_=("${COMP_WORDS[@]}")
+                return
+        fi
+        # List of word completion separators has shrunk;
+        # re-assemble words to complete.
+        for ((i=0, j=0; i < ${#COMP_WORDS[@]}; i++, j++)); do
+                # Append each nonempty word consisting of just
+                # word separator characters to the current word.
+                first=t
+                while
+                        [ $i -gt 0 ] &&
+                        [ -n "${COMP_WORDS[$i]}" ] &&
+                        # word consists of excluded word separators
+                        [ "${COMP_WORDS[$i]//[^$exclude]}" = "${COMP_WORDS[$i]}" ]
+                do
+                        # Attach to the previous token,
+                        # unless the previous token is the command name.
+                        if [ $j -ge 2 ] && [ -n "$first" ]; then
+                                ((j--))
+                        fi
+                        first=
+                        words_[$j]=${words_[j]}${COMP_WORDS[i]}
+                        if [ $i = $COMP_CWORD ]; then
+                                cword_=$j
+                        fi
+                        if (($i < ${#COMP_WORDS[@]} - 1)); then
+                                ((i++))
+                        else
+                                # Done.
+                                return
+                        fi
+                done
+                words_[$j]=${words_[j]}${COMP_WORDS[i]}
+                if [ $i = $COMP_CWORD ]; then
+                        cword_=$j
+                fi
+        done
+}
+
+if ! type _get_comp_words_by_ref >/dev/null 2>&1; then
+_get_comp_words_by_ref ()
+{
+        local exclude cur_ words_ cword_
+        if [ "$1" = "-n" ]; then
+                exclude=$2
+                shift 2
+        fi
+        __crm_reassemble_comp_words_by_ref "$exclude"
+        cur_=${words_[cword_]}
+        while [ $# -gt 0 ]; do
+                case "$1" in
+                cur)
+                        cur=$cur_
+                        ;;
+                prev)
+                        prev=${words_[$cword_-1]}
+                        ;;
+                words)
+                        words=("${words_[@]}")
+                        ;;
+                cword)
+                        cword=$cword_
+                        ;;
+                esac
+                shift
+        done
+}
+fi
+
+__crmcompadd ()
+{
+	local i=0
+	for x in $1; do
+		if [[ "$x" == "$3"* ]]; then
+			COMPREPLY[i++]="$2$x$4"
+		fi
+	done
+}
+
+# Generates completion reply, appending a space to possible completion words,
+# if necessary.
+# It accepts 1 to 4 arguments:
+# 1: List of possible completion words.
+# 2: A prefix to be added to each possible completion word (optional).
+# 3: Generate possible completion matches for this word (optional).
+# 4: A suffix to be appended to each possible completion word (optional).
+__crmcomp ()
+{
+	local cur_="${3-$cur}"
+
+	case "$cur_" in
+	--*=)
+		;;
+	*)
+		local c i=0 IFS=$' \t\n'
+		for c in $1; do
+			c="$c${4-}"
+			if [[ $c == "$cur_"* ]]; then
+				case $c in
+				--*=*|*.) ;;
+				*) c="$c " ;;
+				esac
+				COMPREPLY[i++]="${2-}$c"
+			fi
+		done
+		;;
+	esac
+}
+
+# Generates completion reply from newline-separated possible completion words
+# by appending a space to all of them.
+# It accepts 1 to 4 arguments:
+# 1: List of possible completion words, separated by a single newline.
+# 2: A prefix to be added to each possible completion word (optional).
+# 3: Generate possible completion matches for this word (optional).
+# 4: A suffix to be appended to each possible completion word instead of
+#    the default space (optional).  If specified but empty, nothing is
+#    appended.
+__crmcomp_nl ()
+{
+	local IFS=$'\n'
+	__crmcompadd "$1" "${2-}" "${3-$cur}" "${4- }"
+}
+
+__crm_compgen ()
+{
+	local cur_="$cur" cmd="${words[1]}"
+	local pfx=""
+
+	case "$cur_" in
+	*:*)
+		case "$COMP_WORDBREAKS" in
+		*:*) : great ;;
+		*)   pfx="${cur_%%:*}:" ;;
+		esac
+		cur_="${cur_#*:}"
+		;;
+	esac
+
+    __crmcomp_nl "$(2>/dev/null crm --compgen "${COMP_POINT}" "${COMP_LINE}")" "$pfx" "$cur_"
+}
+
+_crm() {
+    local cur words cword prev
+
+	_get_comp_words_by_ref -n =: cur words cword prev
+
+    for ((i=1; $i<=$cword; i++)); do
+        if [[ ${words[i]} != -* ]]; then
+            if [[ ${words[i-1]} != @(-f|--file|-H|--history|-D|--display|-X|-c|--cib) ]]; then
+                arg="${words[i]}"
+                argi=$i
+                break
+            fi
+        fi
+    done
+
+    case $prev in
+        -f|--file|-H|--history|-D|--display|-X|-c|--cib)
+            # use default completion
+            return
+            ;;
+    esac
+
+    if [[ "$cur" == -* ]]; then
+		__crmcomp '-w -h -d -F -R -f --file -H --history -D --display -X -c --cib'
+        return 0
+    fi
+
+	__crm_compgen
+} &&
+complete -o bashdefault -o default -o nospace -F _crm crm || complete -o default -o nospace -F _crm crm
diff --git a/contrib/pacemaker-crm.vim b/contrib/pacemaker-crm.vim
new file mode 100644
index 0000000..50b6691
--- /dev/null
+++ b/contrib/pacemaker-crm.vim
@@ -0,0 +1,129 @@
+" Vim syntax file
+" Language: pacemaker-crm configuration style (http://www.clusterlabs.org/doc/crm_cli.html)
+"" Filename:    pacemaker-crm.vim
+"" Language:    pacemaker crm configuration text
+"" Maintainer:  Lars Ellenberg <lars at linbit.com>
+"" Last Change: Thu, 18 Feb 2010 16:04:36 +0100
+
+"What to do to install this file:
+" $ mkdir -p ~/.vim/syntax
+" $ cp pacemaker-crm.vim ~/.vim/syntax
+" to set the filetype manually, just do :setf pacemaker-crm
+" TODO: autodetection logic, maybe
+" augroup filetypedetect
+" au BufNewFile,BufRead *.pacemaker-crm setf pacemaker-crm
+" augroup END
+"
+"If you do not already have a .vimrc with syntax on then do this:
+" $ echo "syntax on" >>~/.vimrc
+"
+"Now every file with a filename matching *.pacemaker-crm will be edited
+"using these definitions for syntax highlighting.
+
+" TODO: maybe add some indentation rules as well?
+
+
+" For version 5.x: Clear all syntax items
+" For version 6.x: Quit when a syntax file was already loaded
+"if version < 600
+"  syntax clear
+"elseif exists("b:current_syntax")
+"  finish
+"endif
+syn clear
+
+syn sync lines=30
+syn case ignore
+
+syn match	crm_unexpected	/[^ ]\+/
+
+syn match	crm_lspace	transparent /^[ \t]*/ nextgroup=crm_node,crm_container,crm_head
+syn match	crm_tspace_err	/\\[ \t]\+/
+syn match	crm_tspace_err	/\\\n\(primitive\|node\|group\|ms\|order\|location\|colocation\|property\).*/
+syn match	crm_node	transparent /\<node \$id="[^" ]\+" \([a-z0-9.-]\+\)\?/
+			\	contains=crm_head,crm_assign,crm_nodename
+			\	nextgroup=crm_block
+
+syn region	crm_block	transparent keepend contained start=/[ \t]/ skip=/\\$/ end=/$/
+			\	contains=crm_assign,crm_key,crm_meta,crm_tspace_err,crm_ops
+syn region	crm_order_block	transparent keepend contained start=/[ \t]/ skip=/\\$/ end=/$/
+			\	contains=crm_order_ref
+syn region	crm_colo_block	transparent keepend contained start=/[ \t]/ skip=/\\$/ end=/$/
+			\	contains=crm_colo_ref
+syn region	crm_meta	transparent keepend contained start=/[ \t]meta\>/ skip=/\\$/ end=/$/ end=/[ \t]\(params\|op\)[ \t]/
+			\	contains=crm_key,crm_meta_assign
+
+syn keyword	crm_container	contained group clone ms nextgroup=crm_id
+syn keyword	crm_head	contained node
+syn keyword	crm_head	contained property nextgroup=crm_block
+syn keyword	crm_head	contained primitive nextgroup=crm_res_id
+syn keyword	crm_head	contained location nextgroup=crm_id
+syn match 	crm_id		contained nextgroup=crm_ref,crm_block /[ \t]\+\<[a-z0-9_-]\+\>/
+
+syn keyword	crm_head	contained colocation nextgroup=crm_colo_id
+syn match 	crm_colo_id	contained nextgroup=crm_colo_score /[ \t]\+\<[a-z0-9_-]\+\>/
+syn match	crm_colo_score	contained nextgroup=crm_colo_block /[ \t]\+\(-\?inf\|mandatory\|advisory\|#[a-z0-9_-]\+\|[0-9]\+\):/he=e-1
+
+syn keyword	crm_head	contained order nextgroup=crm_order_id
+syn match 	crm_order_id	contained nextgroup=crm_order_score /[ \t]\+\<[a-z0-9_-]\+\>/
+syn match	crm_order_score	contained nextgroup=crm_order_block /[ \t]\+\(-\?inf\|mandatory\|advisory\|#[a-z0-9_-]\+\|[0-9]\+\):/he=e-1
+
+syn match 	crm_ref		contained nextgroup=crm_ref,crm_block /[ \t]\+\<[a-z0-9_-]\+\>/
+syn match 	crm_ref		contained /[ \t]\+\<[a-z0-9_-]\+\>$/
+
+syn match 	crm_order_ref	contained /[ \t]\+\<[a-z0-9_-]\+\>\(:\(start\|stop\|promote\|demote\)\)\?/ contains=crm_ops
+syn match 	crm_colo_ref	contained /[ \t]\+\<[a-z0-9_-]\+\>\(:\(Slave\|Master\|Started\)\)\?/	contains=crm_roles
+
+syn match 	crm_res_id	contained /[ \t]\+\<[a-z0-9_-]\+\>/ nextgroup=crm_RA
+syn match	crm_RA		contained /[ \t]\+\<\(ocf:[a-z0-9_-]\+\|heartbeat\|lsb\):[a-z0-9_-]\+\>/
+			\	contains=crm_ra_class,crm_ocf_vendor
+			\	nextgroup=crm_block
+
+syn match	crm_ra_class	contained /[ \t]\(ocf\|heartbeat\|lsb\)/
+syn keyword	crm_ocf_vendor	contained heartbeat pacemaker linbit
+
+syn keyword	crm_key		contained attributes params meta op operations date attributes rule
+syn keyword	crm_roles	contained Master Slave Started
+syn match	crm_nodename	contained / [a-z0-9.-]\+\>/
+" crm_ops: match, not keyword, to avoid highlighting it inside attribute names
+syn match	crm_ops		contained /\(start\|stop\|monitor\|promote\|demote\)/
+syn match	crm_assign	transparent contained
+	\ /[ \t]\(\$\(id\|role\|id-ref\)\|[a-z0-9_-]\+\)=\("[^"\n]*"\|[^" ]\+\([ \t]\|$\)\)/ms=s+1,me=e-1
+	\ contains=crm_attr_name,crm_attr_value
+syn match	crm_meta_assign	transparent contained
+	\ /[ \t]\(\$\(id\|role\|id-ref\)\|[a-z0-9_-]\+\)=\("[^"\n]*"\|[^" ]\+\([ \t]\|$\)\)/ms=s+1,me=e-1
+	\ contains=crm_mattr_name,crm_attr_value
+syn match	crm_attr_name	contained /[^=]\+=/me=e-1
+syn match	crm_mattr_name	contained /[^=]\+=/me=e-1 contains=crm_m_err
+syn match	crm_m_err	contained /_/
+syn match	crm_attr_value	contained /=\("[^"\n]*"\|[^" ]\+\)/ms=s+1
+
+
+if !exists("did_dic_syntax_inits")
+  "let did_dic_syntax_inits = 1
+  hi link crm_container	Keyword
+  hi link crm_head	Keyword
+  hi link crm_key	Keyword
+  hi link crm_id	Type
+  hi link crm_colo_id	Type
+  hi link crm_order_id	Type
+  hi link crm_colo_score	Special
+  hi link crm_order_score	Special
+  hi link crm_ref	Identifier
+  hi link crm_colo_ref	Identifier
+  hi link crm_order_ref	Identifier
+  hi link crm_res_id	Identifier
+  hi link crm_nodename	Identifier
+  hi link crm_attr_name Identifier
+  hi link crm_mattr_name Identifier
+  hi link crm_tspace_err	Error
+  hi link crm_m_err	Error
+  hi link crm_attr_value	String
+  hi link crm_RA	Function
+  hi link crm_ra_class	keyword
+  hi link crm_ocf_vendor	Type
+  hi link crm_unexpected	Error
+  hi link crm_ops	Special
+  hi link crm_roles	Special
+endif
+
diff --git a/contrib/pcmk.vim b/contrib/pcmk.vim
new file mode 100644
index 0000000..19a93b6
--- /dev/null
+++ b/contrib/pcmk.vim
@@ -0,0 +1,114 @@
+" Vim syntax file
+" Author: Trevor Hemsley <themsley at voiceflex.com>
+" Author: Dan Frincu <df.cluster at gmail.com>
+" Language:     pcmk
+" Filenames:    *.pcmk
+
+" For version 5.x: Clear all syntax items
+" For version 6.x: Quit when a syntax file was already loaded
+if version < 600
+  syntax clear
+elseif exists("b:current_syntax")
+  finish
+endif
+
+set modeline
+
+" setlocal iskeyword+=-
+
+" Errors
+syn match    pcmkParErr     ")"
+syn match    pcmkBrackErr   "]"
+syn match    pcmkBraceErr   "}"
+
+" Enclosing delimiters
+syn region   pcmkEncl transparent matchgroup=pcmkParEncl   start="("  matchgroup=pcmkParEncl   end=")"  contains=ALLBUT,pcmkParErr
+syn region   pcmkEncl transparent matchgroup=pcmkBrackEncl start="\[" matchgroup=pcmkBrackEncl end="\]" contains=ALLBUT,pcmkBrackErr
+syn region   pcmkEncl transparent matchgroup=pcmkBraceEncl start="{"  matchgroup=pcmkBraceEncl end="}"  contains=ALLBUT,pcmkBraceErr
+
+" Comments
+syn region   pcmkComment start="//"  end="$"   contains=pcmkComment,pcmkTodo
+syn region   pcmkComment start="/\*" end="\*/" contains=pcmkComment,pcmkTodo
+syn keyword  pcmkTodo    contained TODO FIXME XXX
+
+" Strings
+syn region   pcmkString    start=+"+ skip=+\\\\\|\\"+ end=+"+
+
+" General keywords
+syn keyword  pcmkKeyword  node primitive property rsc_defaults op_defaults group clone nextgroup=pcmkName skipwhite
+syn keyword  pcmkKey2     location nextgroup=pcmkResource skipwhite
+syn keyword  pcmkKey3     colocation order nextgroup=pcmkName3 skipwhite
+syn match    pcmkResource /\<\f\+\>/ nextgroup=pcmkName2 skipwhite
+syn match    pcmkName     /\<\f\+\>/
+syn match    pcmkName2    /\<\f\+\>/ nextgroup=pcmkPrio skipwhite
+syn match    pcmkName3    /\<\f\+\>/ nextgroup=pcmkPrio skipwhite
+syn match    pcmkPrio     /\<\w\+\>/
+syn match    pcmkNumbers  /[[:digit:]]\+\:/
+syn match    pcmkInf      /inf\:/
+
+" Graph attributes
+syn keyword  pcmkType attributes params op meta
+syn keyword  pcmkTag monitor start stop migrate_from migrate_to notify demote promote Master Slave
+
+" Special chars
+"syn match    pcmkKeyChar  "="
+syn match    pcmkKeyChar  ";"
+syn match    pcmkKeyChar  "->"
+syn match    pcmkKeyChar  "\$"
+"syn match    pcmkKeyChar  "\\"
+syn match    pcmkKeyChar  ":"
+syn match    pcmkKeyChar  "-"
+syn match    pcmkKeyChar  "+"
+
+" Identifier
+syn match    pcmkIdentifier /\<\w\+\>/
+syn match    pcmkKeyword    "^ms\s*" nextgroup=pcmkName skipwhite
+
+" Synchronization
+syn sync minlines=50
+syn sync maxlines=500
+
+" Define the default highlighting.
+" For version 5.7 and earlier: only when not done already
+" For version 5.8 and later: only when an item doesn't have highlighting yet
+if version >= 508 || !exists("did_pcmk_syntax_inits")
+  if version < 508
+    let did_pcmk_syntax_inits = 1
+    command -nargs=+ HiLink hi link <args>
+  else
+    command -nargs=+ HiLink hi def link <args>
+  endif
+
+  HiLink pcmkParErr      Error
+  HiLink pcmkBraceErr    Error
+  HiLink pcmkBrackErr    Error
+
+  HiLink pcmkComment     Comment
+  HiLink pcmkTodo        Todo
+
+  HiLink pcmkParEncl     Keyword
+  HiLink pcmkBrackEncl   Keyword
+  HiLink pcmkBraceEncl   Keyword
+
+  HiLink pcmkKeyword     Keyword
+  HiLink pcmkKey2        Keyword
+  HiLink pcmkKey3        Keyword
+  HiLink pcmkType        Keyword
+  HiLink pcmkKeyChar     Keyword
+
+"  hi Normal ctermfg=yellow ctermbg=NONE cterm=NONE
+  HiLink pcmkString     String
+  HiLink pcmkIdentifier Identifier
+  HiLink pcmkTag        Tag
+  HiLink pcmkName       Type
+  HiLink pcmkName2      Tag
+  HiLink pcmkName3      Type
+  HiLink pcmkResource   Type
+  HiLink pcmkPrio       Number
+  HiLink pcmkNumbers    String
+  HiLink pcmkInf        String
+
+  delcommand HiLink
+endif
+
+let b:current_syntax = "pcmk"
diff --git a/crm b/crm
new file mode 100755
index 0000000..31c5ff2
--- /dev/null
+++ b/crm
@@ -0,0 +1,58 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+minimum_version = '2.6'
+import sys
+
+from distutils import version
+v_min = version.StrictVersion(minimum_version)
+v_this = version.StrictVersion(sys.version[:3])
+if v_min > v_this:
+    sys.stderr.write("abort: minimum python version support is %s\n" %
+                     minimum_version)
+    sys.exit(-1)
+
+try:
+    from crmsh import main
+except ImportError, msg:
+    try:
+        # Perhaps we're running from the source directory
+        from modules import main
+    except ImportError, msg2:
+        sys.stderr.write('''Fatal error:
+    %s
+    %s
+
+Failed to start the crm shell! This is likely due to
+a broken installation or a missing dependency.
+
+If you are using a packaged version of the crm shell,
+please try reinstalling the package. Also check your
+PYTHONPATH and make sure that the crmsh module is
+reachable.
+
+Please file an issue describing your installation at
+https://github.com/Clusterlabs/crmsh/issues/ .
+''' % (msg, msg2))
+        sys.exit(-1)
+
+rc = main.run()
+sys.exit(rc)
+# vim:ts=4:sw=4:et:
diff --git a/crm.conf.in b/crm.conf.in
new file mode 100644
index 0000000..78cf297
--- /dev/null
+++ b/crm.conf.in
@@ -0,0 +1,51 @@
+; crmsh configuration file
+; To override per user, create a file ~/.config/crm/crm.conf
+;
+; [core]
+; editor = $EDITOR
+; pager = $PAGER
+; user =
+; skill_level = expert
+; sort_elements = yes
+; check_frequency = always
+; check_mode = strict
+; wait = no
+; add_quotes = yes
+; manage_children = ask
+; force = no
+; debug = no
+; ptest = ptest, crm_simulate
+; dotty = dotty
+; dot = dot
+
+[path]
+sharedir = @datadir@/@PACKAGE@
+cache = @CRM_CACHE_DIR@
+crm_config = @CRM_CONFIG_DIR@
+crm_daemon_dir = @CRM_DAEMON_DIR@
+crm_daemon_user = @CRM_DAEMON_USER@
+ocf_root = @OCF_ROOT_DIR@
+crm_dtd_dir = @CRM_DTD_DIRECTORY@
+pe_state_dir = @PE_STATE_DIR@
+heartbeat_dir = @HA_VARLIBHBDIR@
+hb_delnode = @datadir@/heartbeat/hb_delnode
+nagios_plugins = @libdir@/nagios/plugins
+
+; [color]
+; style = color
+; error = red bold
+; ok = green bold
+; warn = yellow bold
+; info = cyan
+; help_keyword = blue bold underline
+; help_header = normal bold
+; help_topic = yellow bold
+; help_block = cyan
+; keyword = yellow
+; identifier = normal
+; attr_name = cyan
+; attr_value = red
+; resource_reference = green
+; id_reference = green
+; score = magenta
+; ticket = magenta
diff --git a/crmsh-cibadmin_can_patch.patch b/crmsh-cibadmin_can_patch.patch
new file mode 100644
index 0000000..f90530e
--- /dev/null
+++ b/crmsh-cibadmin_can_patch.patch
@@ -0,0 +1,23 @@
+commit 043a73a179116619bff65c46e3f6ac693dd57d3f
+Author: Kristoffer Grönlund <krig at koru.se>
+Date:   Thu Dec 12 15:06:21 2013 +0100
+
+    Medium: utils: Enable CIB patches for 1.1.10>
+    
+    Enable CIB patches on patched 1.1.10 systems.
+
+diff --git a/modules/utils.py b/modules/utils.py
+index 624fcbf0d841..45277fb10003 100644
+--- a/modules/utils.py
++++ b/modules/utils.py
+@@ -1097,8 +1097,8 @@ def cibadmin_features():
+ 
+ 
+ def cibadmin_can_patch():
+-    # cibadmin -P doesn't handle comments in <1.1.11 (unless patched)
+-    return is_min_pcmk_ver("1.1.11")
++    # cibadmin -P doesn't handle comments in <1.1.10 (unless patched)
++    return is_min_pcmk_ver("1.1.10")
+ 
+ 
+ # quote function from python module shlex.py in python 3.3
diff --git a/crmsh.spec b/crmsh.spec
new file mode 100644
index 0000000..5f9e484
--- /dev/null
+++ b/crmsh.spec
@@ -0,0 +1,227 @@
+#
+# spec file for package crmsh
+#
+# Copyright (c) 2015 SUSE LINUX GmbH, Nuernberg, Germany.
+#
+# All modifications and additions to the file contributed by third parties
+# remain the property of their copyright owners, unless otherwise agreed
+# upon. The license for this file, and modifications and additions to the
+# file, is the same license as for the pristine package itself (unless the
+# license for the pristine package is not an Open Source License, in which
+# case the license is the MIT License). An "Open Source License" is a
+# license that conforms to the Open Source Definition (Version 1.9)
+# published by the Open Source Initiative.
+
+# Please submit bugfixes or comments via http://bugs.opensuse.org/
+#
+
+
+%global gname haclient
+%global uname hacluster
+%global crmsh_docdir %{_defaultdocdir}/%{name}
+
+%global upstream_version tip
+%global upstream_prefix crmsh
+%global crmsh_release 1
+
+%if 0%{?fedora_version} || 0%{?centos_version} || 0%{?rhel_version} || 0%{?rhel} || 0%{?fedora}
+%define pkg_group System Environment/Daemons
+%else
+%define pkg_group Productivity/Clustering/HA
+%endif
+
+# Compatibility macros for distros (fedora) that don't provide Python macros by default
+# Do this instead of trying to conditionally include {_rpmconfigdir}/macros.python
+%{!?py_ver:     %{expand: %%global py_ver      %%(echo `python -c "import sys; print sys.version[:3]"`)}}
+%{!?py_prefix:  %{expand: %%global py_prefix   %%(echo `python -c "import sys; print sys.prefix"`)}}
+%{!?py_libdir:  %{expand: %%global py_libdir   %%{expand:%%%%{py_prefix}/%%%%{_lib}/python%%%%{py_ver}}}}
+%{!?py_sitedir: %{expand: %%global py_sitedir  %%{expand:%%%%{py_libdir}/site-packages}}}
+
+Name:           crmsh
+Summary:        High Availability cluster command-line interface
+License:        GPL-2.0+
+Group:          %{pkg_group}
+Version:        2.0
+Release:        %{?crmsh_release}%{?dist}
+Url:            http://crmsh.github.io
+Source0:        crmsh.tar.bz2
+# PATCH-FEATURE-OPENSUSE crmsh-cibadmin_can_patch.patch
+# dejan at suse.de -- enable atomic CIB updates here, because our
+# pacemaker version has been fixed in the meantime
+Patch11:        crmsh-cibadmin_can_patch.patch
+BuildRoot:      %{_tmppath}/%{name}-%{version}-build
+Requires(pre):  pacemaker
+Requires:       /usr/bin/which
+Requires:       pssh
+Requires:       python >= 2.6
+Requires:       python-dateutil
+Requires:       python-lxml
+BuildRequires:  python-lxml
+
+%if 0%{?suse_version}
+Requires:       python-PyYAML
+BuildRequires:  python-PyYAML
+# Suse splits this off into a separate package
+Requires:       python-curses
+BuildRequires:  fdupes
+BuildRequires:  libglue-devel
+BuildRequires:  libpacemaker-devel
+BuildRequires:  python-curses
+%else
+BuildRequires:  cluster-glue-libs-devel
+BuildRequires:  pacemaker-libs-devel
+%endif
+
+%if 0%{?fedora_version}
+Requires:       PyYAML
+BuildRequires:  PyYAML
+%endif
+
+# Required for core functionality
+BuildRequires:  asciidoc
+BuildRequires:  autoconf
+BuildRequires:  automake
+BuildRequires:  pkgconfig
+BuildRequires:  python
+
+%if 0%{?suse_version} > 1210
+# xsltproc is necessary for manpage generation; this is split out into
+# libxslt-tools as of openSUSE 12.2.  Possibly strictly should be
+# required by asciidoc
+BuildRequires:  libxslt-tools
+%endif
+
+%description
+The crm shell is a command-line interface for High-Availability
+cluster management on GNU/Linux systems. It simplifies the
+configuration, management and troubleshooting of Pacemaker-based
+clusters, by providing a powerful and intuitive set of features.
+
+Authors: Dejan Muhamedagic <dejan at suse.de> and many others
+
+%package test
+Summary:        Test package for crmsh
+Group:          %{pkg_group}
+Requires:       crmsh
+%if 0%{?with_regression_tests}
+BuildRequires:  corosync
+BuildRequires:  procps
+BuildRequires:  python-dateutil
+BuildRequires:  python-nose
+BuildRequires:  vim
+Requires:       pacemaker
+Requires:       pssh
+%endif
+%description test
+The crm shell is a command-line interface for High-Availability
+cluster management on GNU/Linux systems. It simplifies the
+configuration, management and troubleshooting of Pacemaker-based
+clusters, by providing a powerful and intuitive set of features.
+
+Authors: Dejan Muhamedagic <dejan at suse.de> and many others
+
+%prep
+%setup -q -n %{upstream_prefix}
+%patch11 -p1
+
+# Force the local time
+#
+# 'hg archive' sets the file date to the date of the last commit.
+# This can result in files having been created in the future
+# when building on machines in timezones 'behind' the one the
+# commit occurred in - which seriously confuses 'make'
+find . -exec touch \{\} \;
+
+%build
+./autogen.sh
+
+# RHEL <= 5 does not support --docdir
+# SLES <= 10 does not support ./configure --docdir=,
+# hence, use this ugly hack
+%if 0%{?suse_version} < 1020
+export docdir=%{crmsh_docdir}
+%{configure}            \
+    --sysconfdir=%{_sysconfdir} \
+    --localstatedir=%{_var}             \
+    --with-pkg-name=%{name} \
+    --with-version=%{version}-%{release}
+%else
+%{configure}            \
+    --sysconfdir=%{_sysconfdir} \
+    --localstatedir=%{_var}             \
+    --with-pkg-name=%{name}     \
+    --with-version=%{version}-%{release}    \
+    --docdir=%{crmsh_docdir}
+%endif
+
+make %{_smp_mflags} docdir=%{crmsh_docdir}
+
+%if 0%{?with_regression_tests}
+	./test/unit-tests.sh --quiet
+    if [ ! $? ]; then
+        echo "Unit tests failed."
+        exit 1
+    fi
+%endif
+
+%install
+make DESTDIR=%{buildroot} docdir=%{crmsh_docdir} install
+install -Dm0644 contrib/bash_completion.sh %{buildroot}%{_sysconfdir}/bash_completion.d/crm.sh
+%if 0%{?suse_version}
+%fdupes %{buildroot}
+%endif
+
+%clean
+rm -rf %{buildroot}
+
+# Run regression tests after installing the package
+# NB: this is called twice by OBS, that's why we touch the file
+%if 0%{?with_regression_tests}
+%post test
+if [ ! -e /tmp/.crmsh_regression_tests_ran ]; then
+    touch /tmp/.crmsh_regression_tests_ran
+    if ! %{_datadir}/%{name}/tests/regression.sh ; then
+        echo "Regression tests failed."
+        cat crmtestout/regression.out
+        exit 1
+    fi
+	cd %{_datadir}/%{name}/tests
+	./cib-tests.sh
+	result2=$?
+	[ $result1 -ne 0 ] && (echo "Regression tests failed."; cat ${buildroot}/crmtestout/regression.out)
+	[ $result2 -ne 0 ] && echo "CIB tests failed."
+	[ $result1 -eq 0 -a $result2 -eq 0 ]
+fi
+%endif
+
+%files
+###########################################################
+%defattr(-,root,root)
+
+%{_sbindir}/crm
+%{py_sitedir}/crmsh
+
+%{_datadir}/%{name}
+%exclude %{_datadir}/%{name}/tests
+
+%doc %{_mandir}/man8/*
+%{crmsh_docdir}/COPYING
+%{crmsh_docdir}/AUTHORS
+%{crmsh_docdir}/crm.8.html
+%{crmsh_docdir}/crmsh_hb_report.8.html
+%{crmsh_docdir}/ChangeLog
+%{crmsh_docdir}/README
+%{crmsh_docdir}/contrib/*
+
+%config %{_sysconfdir}/crm
+
+%dir %{crmsh_docdir}
+%dir %{crmsh_docdir}/contrib
+%dir %attr (770, %{uname}, %{gname}) %{_var}/cache/crm
+%config %{_sysconfdir}/bash_completion.d/crm.sh
+
+%files test
+%defattr(-,root,root)
+%{_datadir}/%{name}/tests
+
+%changelog
diff --git a/doc/Makefile.am b/doc/Makefile.am
new file mode 100644
index 0000000..3451068
--- /dev/null
+++ b/doc/Makefile.am
@@ -0,0 +1,43 @@
+#
+# doc: Pacemaker code
+#
+# Copyright (C) 2008 Andrew Beekhof
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+helpdir     = $(datadir)/$(PACKAGE)
+
+asciiman	= crm.8.txt crmsh_hb_report.8.txt
+help_DATA	= crm.8.txt
+doc_DATA	= $(generated_docs)
+
+generated_docs	=
+generated_mans	=
+
+if BUILD_ASCIIDOC
+generated_docs	+= $(ascii:%.txt=%.html) $(asciiman:%.txt=%.html)
+generated_mans	+= $(asciiman:%.8.txt=%.8)
+$(generated_mans): $(asciiman)
+man8_MANS	= $(generated_mans)
+endif
+
+%.html: %.txt
+	$(ASCIIDOC) --unsafe --backend=xhtml11 $<
+%.8: %.8.txt
+	a2x -f manpage $<
+clean-local:
+	-rm -rf $(generated_docs) $(generated_mans)
diff --git a/doc/crm.8.txt b/doc/crm.8.txt
new file mode 100644
index 0000000..70fb1bf
--- /dev/null
+++ b/doc/crm.8.txt
@@ -0,0 +1,4336 @@
+:man source:   crm
+:man version:  2.1.3
+:man manual:   crmsh documentation
+
+crm(8)
+======
+
+NAME
+----
+crm - Pacemaker command line interface for configuration and management
+
+
+SYNOPSIS
+--------
+*crm* [OPTIONS] [SUBCOMMAND ARGS...]
+
+
+[[topics_Description,Program description]]
+DESCRIPTION
+-----------
+The `crm` shell is a command-line based cluster configuration and
+management tool. Its goal is to assist as much as possible with the
+configuration and maintenance of Pacemaker-based High Availability
+clusters.
+
+For more information on Pacemaker clusters, see http://clusterlabs.org/.
+
+`crm` works both as a command-line tool to be called directly from the
+system shell, and as an interactive shell with extensive tab
+completion and help.
+
+The primary focus of the `crm` shell is to provide a simplified and
+consistent interface to Pacemaker, but it also provides tools for
+managing the creation and configuration of High Availability clusters
+from scratch. To learn more about this aspect of `crm`, see the
+`cluster` section below.
+
+The Pacemaker configuration is stored in something called a CIB file,
+where CIB stands for Cluster Information Base. The CIB is a set of
+instructions coded in XML which is synchronized across the cluster.
+
+Editing the CIB is a challenge, not only due to its complexity and
+wide variety of options, but also because XML is more computer than
+user friendly. To help with this task, the `crm` shell provides a
+small and simple line-oriented configuration language consistent with
+the other commands available in the shell. For more information about
+this language and how to use it, see the `configure` section below.
+
+`crm` provides a consistent and well-documented interface to most of
+the management tools included in Pacemaker, for example
+`crm_resource(8)` or `crm_attribute(8)`. Instead of having to remember
+the various flags and options available for each tool, `crm` hides all
+of the arcane detail.
+
+`crm` can also function as a cluster scripting tool, and can be fed
+multi-line sets of commands either directly from standard input or via
+a file. Templates with ready made configurations may help newbies
+learn about the cluster configuration or facilitate testing
+procedures.
+
+The `crm` shell is line oriented: every command must start and finish
+on the same line. It is possible to use a continuation character (+\+)
+to write one command in two or more lines. The continuation character
+is commonly used when displaying configurations.
+
+[[topics_CommandLine,Command line options]]
+OPTIONS
+-------
+*-f, --file*='FILE'::
+    Load commands from the given file. If a dash +-+ is used in place
+    of a file name, `crm` will read commands from the shell standard
+    input (`stdin`).
+
+*-c, --cib*='CIB'::
+    Start the session using the given shadow CIB file.
+    Equivalent to +cib use <CIB>+.
+
+*-D, --display=*'OUTPUT_TYPE'::
+    Choose one of the output options: +plain+, +color+, or
+    +uppercase+. The default is +color+ if the terminal emulation
+    supports colors. Otherwise, +plain+ is used.
+
+*-F, --force*::
+    Make `crm` proceed with applying changes where it would normally
+    ask the user to confirm before proceeding. This option is mainly
+    useful in scripts, and should be used with care.
+
+*-w, --wait*::
+    Make `crm` wait for the cluster transition to finish (for the
+    changes to take effect) after each processed line.
+
+*-H, --history*='DIR|FILE'::
+    The `history` commands can either work directly on the live
+    cluster (default), or on a report generated by the `report`
+    command. Use this option to specify a directory or file containing
+    the previously generated report.
+
+*-h, --help*::
+    Print help page.
+
+*--version*::
+    Print crmsh version and build information (Mercurial Hg changeset
+    hash).
+
+*-d, --debug*::
+    Print verbose debugging information.
+
+*-R, --regression-tests*::
+    Enables extra verbose trace logging used by the regression
+    tests. Logs all external calls made by crmsh.
+
+*--scriptdir*='DIR'::
+    Extra directory where crm looks for cluster scripts. Can be a semi-colon
+    separated list of directories.
+
+[[topics_Introduction,Introduction]]
+== Introduction
+
+This section of the user guide covers general topics about the user
+interface and describes some of the features of `crmsh` in detail.
+
+[[topics_Introduction_Interface,User interface]]
+=== User interface
+
+The main purpose of `crmsh` is to provide a simple yet powerful
+interface to the cluster stack. There are two main modes of operation
+with the user interface of `crmsh`:
+
+* Command line (single-shot) use - Use `crm` as a regular UNIX command
+  from your usual shell. `crm` has full bash completion built in, so
+  using it in this manner should be as comfortable and familiar as
+  using any other command-line tool.
+
+* Interactive mode - By calling `crm` without arguments, or by calling
+  it with only a sublevel as argument, `crm` enters the interactive
+  mode. In this mode, it acts as its own command shell, which
+  remembers which sublevel you are currently in and allows for rapid
+  and convenient execution of multiple commands within the same
+  sublevel. This mode also has full tab completion, as well as
+  built-in interactive help and syntax highlighting.
+
+Here are a few examples of using `crm` both as a command-line tool and
+as an interactive shell:
+
+.Command line (one-shot) use:
+........
+# crm resource stop www_app
+........
+
+.Interactive use:
+........
+# crm
+crm(live)# resource
+crm(live)resource# unmanage tetris_1
+crm(live)resource# up
+crm(live)# node standby node4
+........
+
+.Cluster configuration:
+........
+# crm configure<<EOF
+  #
+  # resources
+  #
+  primitive disk0 iscsi \
+    params portal=192.168.2.108:3260 target=iqn.2008-07.com.suse:disk0
+  primitive fs0 Filesystem \
+    params device=/dev/disk/by-label/disk0 directory=/disk0 fstype=ext3
+  primitive internal_ip IPaddr params ip=192.168.1.101
+  primitive apache apache \
+    params configfile=/disk0/etc/apache2/site0.conf
+  primitive apcfence stonith:apcsmart \
+    params ttydev=/dev/ttyS0 hostlist="node1 node2" \
+    op start timeout=60s
+  primitive pingd pingd \
+    params name=pingd dampen=5s multiplier=100 host_list="r1 r2"
+  #
+  # monitor apache and the UPS
+  #
+  monitor apache 60s:30s
+  monitor apcfence 120m:60s
+  #
+  # cluster layout
+  #
+  group internal_www \
+    disk0 fs0 internal_ip apache
+  clone fence apcfence \
+    meta globally-unique=false clone-max=2 clone-node-max=1
+  clone conn pingd \
+    meta globally-unique=false clone-max=2 clone-node-max=1
+  location node_pref internal_www \
+    rule 50: #uname eq node1 \
+    rule pingd: defined pingd
+  #
+  # cluster properties
+  #
+  property stonith-enabled=true
+  commit
+EOF
+........
+
+The `crm` interface is hierarchical, with commands organized into
+separate levels by functionality. To list the available levels and
+commands, either execute +help <level>+, or, if at the top level of
+the shell, simply typing `help` will provide an overview of all
+available levels and commands.
+
+The +(live)+ string in the `crm` prompt signifies that the current CIB
+in use is the cluster live configuration. It is also possible to
+work with so-called <<topics_Features_Shadows,shadow CIBs>>. These are separate, inactive
+configurations stored in files, that can be applied and thereby
+replace the live configuration at any time.
+
+[[topics_Introcution_Completion,Tab completion]]
+=== Tab completion
+
+The `crm` makes extensive use of tab completion. The completion
+is both static (i.e. for `crm` commands) and dynamic. The latter
+takes into account the current status of the cluster or
+information from installed resource agents. Sometimes, completion
+may also be used to get short help on resource parameters. Here
+are a few examples:
+
+...............
+crm(live)resource# <TAB><TAB>
+bye           failcount     move          restart       unmigrate
+cd            help          param         show          unmove
+cleanup       list          promote       start         up
+demote        manage        quit          status        utilization
+end           meta          refresh       stop
+exit          migrate       reprobe       unmanage
+
+crm(live)configure# primitive fence-1 <TAB><TAB>
+heartbeat:  lsb:    ocf:    stonith:
+
+crm(live)configure# primitive fence-1 stonith:<TAB><TAB>
+apcmaster                external/ippower9258     fence_legacy
+apcmastersnmp            external/kdumpcheck      ibmhmc
+apcsmart                 external/libvirt         ipmilan
+
+crm(live)configure# primitive fence-1 stonith:ipmilan params <TAB><TAB>
+auth=      hostname=  ipaddr=    login=     password=  port=      priv=
+
+crm(live)configure# primitive fence-1 stonith:ipmilan params auth=<TAB><TAB>
+auth* (string)
+    The authorization type of the IPMI session ("none", "straight", "md2", or "md5")
+...............
+
+`crmsh` also comes with bash completion usable directly from the
+system shell. This should be installed automatically with the command
+itself.
+
+[[topics_Features,Features]]
+== Features
+
+The feature set of crmsh covers a wide range of functionality, and
+understanding how and when to use the various features of the shell
+can be difficult. This section of the guide describes some of the
+features and use cases of `crmsh` in more depth. The intention is to
+provide a deeper understanding of these features, but also to serve as
+a guide to using them.
+
+[[topics_Features_Shadows,Shadow CIB usage]]
+=== Shadow CIB usage
+
+A Shadow CIB is a normal cluster configuration stored in a file.
+They may be manipulated in much the same way as the _live_ CIB, with
+the key difference that changes to a shadow CIB have no effect on the
+actual cluster resources. An administrator may choose to apply any of
+them to the cluster, thus replacing the running configuration with the
+one found in the shadow CIB.
+
+The `crm` prompt always contains the name of the configuration which
+is currently in use, or the string _live_ if using the live cluster
+configuration.
+
+When editing the configuration in the `configure` level, no changes
+are actually applied until the `commit` command is executed. It is
+possible to start editing a configuration as usual, but instead of
+committing the changes to the active CIB, save them to a shadow CIB.
+
+The following example `configure` session demonstrates how this can be
+done:
+...............
+crm(live)configure# cib new test-2
+INFO: test-2 shadow CIB created
+crm(test-2)configure# commit
+...............
+
+[[topics_Features_Checks,Configuration semantic checks]]
+=== Configuration semantic checks
+
+Resource definitions may be checked against the meta-data
+provided with the resource agents. These checks are currently
+carried out:
+
+- are required parameters set
+- existence of defined parameters
+- timeout values for operations
+
+The parameter checks are obvious and need no further explanation.
+Failures in these checks are treated as configuration errors.
+
+The timeouts for operations should be at least as long as those
+recommended in the meta-data. Too short timeout values are a
+common mistake in cluster configurations and, even worse, they
+often slip through if cluster testing was not thorough. Though
+operation timeouts issues are treated as warnings, make sure that
+the timeouts are usable in your environment. Note also that the
+values given are just _advisory minimum_---your resources may
+require longer timeouts.
+
+User may tune the frequency of checks and the treatment of errors
+by the <<cmdhelp_options_check-frequency,`check-frequency`>> and
+<<cmdhelp_options_check-mode,`check-mode`>> preferences.
+
+Note that if the +check-frequency+ is set to +always+ and the
++check-mode+ to +strict+, errors are not tolerated and such
+configuration cannot be saved.
+
+[[topics_Features_Templates,Configuration templates]]
+=== Configuration templates
+
+Configuration templates are ready made configurations created by
+cluster experts. They are designed in such a way so that users
+may generate valid cluster configurations with minimum effort.
+If you are new to Pacemaker, templates may be the best way to
+start.
+
+We will show here how to create a simple yet functional Apache
+configuration:
+...............
+# crm configure
+crm(live)configure# template
+crm(live)configure template# list templates
+apache       filesystem   virtual-ip
+crm(live)configure template# new web <TAB><TAB>
+apache       filesystem   virtual-ip
+crm(live)configure template# new web apache
+INFO: pulling in template apache
+INFO: pulling in template virtual-ip
+crm(live)configure template# list
+web2-d       web2     vip2     web3     vip      web
+...............
+
+We enter the `template` level from `configure`. Use the `list`
+command to show templates available on the system. The `new`
+command creates a configuration from the +apache+ template. You
+can use tab completion to pick templates. Note that the apache
+template depends on a virtual IP address which is automatically
+pulled along. The `list` command shows the just created +web+
+configuration, among other configurations (I hope that you,
+unlike me, will use more sensible and descriptive names).
+
+The `show` command, which displays the resulting configuration,
+may be used to get an idea about the minimum required changes
+which have to be done. All +ERROR+ messages show the line numbers
+in which the respective parameters are to be defined:
+...............
+crm(live)configure template# show
+ERROR: 23: required parameter ip not set
+ERROR: 61: required parameter id not set
+ERROR: 65: required parameter configfile not set
+crm(live)configure template# edit
+...............
+
+The `edit` command invokes the preferred text editor with the
++web+ configuration. At the top of the file, the user is advised
+how to make changes. A good template should require from the user
+to specify only parameters. For example, the +web+ configuration
+we created above has the following required and optional
+parameters (all parameter lines start with +%%+):
+...............
+$ grep -n ^%% ~/.crmconf/web
+23:%% ip
+31:%% netmask
+35:%% lvs_support
+61:%% id
+65:%% configfile
+71:%% options
+76:%% envfiles
+...............
+
+These lines are the only ones that should be modified. Simply
+append the parameter value at the end of the line. For instance,
+after editing this template, the result could look like this (we
+used tabs instead of spaces to make the values stand out):
+...............
+$ grep -n ^%% ~/.crmconf/web
+23:%% ip        192.168.1.101
+31:%% netmask
+35:%% lvs_support
+61:%% id        websvc
+65:%% configfile    /etc/apache2/httpd.conf
+71:%% options
+76:%% envfiles
+...............
+
+As you can see, the parameter line format is very simple:
+...............
+%% <name> <value>
+...............
+
+After editing the file, use `show` again to display the
+configuration:
+...............
+crm(live)configure template# show
+primitive virtual-ip IPaddr \
+    params ip=192.168.1.101
+primitive apache apache \
+    params configfile="/etc/apache2/httpd.conf"
+monitor apache 120s:60s
+group websvc \
+    apache virtual-ip
+...............
+
+The target resource of the apache template is a group which we
+named +websvc+ in this sample session.
+
+This configuration looks exactly as you could type it at the
+`configure` level. The point of templates is to save you some
+typing. It is important, however, to understand the configuration
+produced.
+
+Finally, the configuration may be applied to the current
+crm configuration (note how the configuration changed slightly,
+though it is still equivalent, after being digested at the
+`configure` level):
+...............
+crm(live)configure template# apply
+crm(live)configure template# cd ..
+crm(live)configure# show
+node xen-b
+node xen-c
+primitive apache apache \
+    params configfile="/etc/apache2/httpd.conf" \
+    op monitor interval=120s timeout=60s
+primitive virtual-ip IPaddr \
+    params ip=192.168.1.101
+group websvc apache virtual-ip
+...............
+
+Note that this still does not commit the configuration to the CIB
+which is used in the shell, either the running one (+live+) or
+some shadow CIB. For that you still need to execute the `commit`
+command.
+
+To complete our example, we should also define the preferred node
+to run the service:
+...............
+crm(live)configure# location websvc-pref websvc 100: xen-b
+...............
+
+If you are not happy with some resource names which are provided
+by default, you can rename them now:
+...............
+crm(live)configure# rename virtual-ip intranet-ip
+crm(live)configure# show
+node xen-b
+node xen-c
+primitive apache apache \
+    params configfile="/etc/apache2/httpd.conf" \
+    op monitor interval=120s timeout=60s
+primitive intranet-ip IPaddr \
+    params ip=192.168.1.101
+group websvc apache intranet-ip
+location websvc-pref websvc 100: xen-b
+...............
+
+To summarize, working with templates typically consists of the
+following steps:
+
+- `new`: create a new configuration from templates
+- `edit`: define parameters, at least the required ones
+- `show`: see if the configuration is valid
+- `apply`: apply the configuration to the `configure` level
+
+[[topics_Features_Testing,Resource testing]]
+=== Resource testing
+
+The amount of detail in a cluster makes all configurations prone
+to errors. By far the largest number of issues in a cluster is
+due to bad resource configuration. The shell can help quickly
+diagnose such problems. And considerably reduce your keyboard
+wear.
+
+Let's say that we entered the following configuration:
+...............
+node xen-b
+node xen-c
+node xen-d
+primitive fencer stonith:external/libvirt \
+    params hypervisor_uri="qemu+tcp://10.2.13.1/system" \
+        hostlist="xen-b xen-c xen-d" \
+    op monitor interval=2h
+primitive svc Xinetd \
+    params service=systat \
+    op monitor interval=30s
+primitive intranet-ip IPaddr2 \
+    params ip=10.2.13.100 \
+    op monitor interval=30s
+primitive apache apache \
+    params configfile="/etc/apache2/httpd.conf" \
+    op monitor interval=120s timeout=60s
+group websvc apache intranet-ip
+location websvc-pref websvc 100: xen-b
+...............
+
+Before typing `commit` to submit the configuration to the cib we
+can make sure that all resources are usable on all nodes:
+...............
+crm(live)configure# rsctest websvc svc fencer
+...............
+
+It is important that resources being tested are not running on
+any nodes. Otherwise, the `rsctest` command will refuse to do
+anything. Of course, if the current configuration resides in a
+CIB shadow, then a `commit` is irrelevant. The point being that
+resources are not running on any node.
+
+.Note on stopping all resources
+****************************
+Alternatively to not committing a configuration, it is also
+possible to tell Pacemaker not to start any resources:
+
+...............
+crm(live)configure# property stop-all-resources=yes
+...............
+Almost none---resources of class stonith are still started. But
+shell is not as strict when it comes to stonith resources.
+****************************
+
+Order of resources is significant insofar that a resource depends
+on all resources to its left. In most configurations, it's
+probably practical to test resources in several runs, based on
+their dependencies.
+
+Apart from groups, `crm` does not interpret constraints and
+therefore knows nothing about resource dependencies. It also
+doesn't know if a resource can run on a node at all in case of an
+asymmetric cluster. It is up to the user to specify a list of
+eligible nodes if a resource is not meant to run on every node.
+
+[[topics_Features_Security,Access Control Lists (ACL)]]
+=== Access Control Lists (ACL)
+
+.Note on ACLs in Pacemaker 1.1.12
+****************************
+The support for ACLs has been revised in Pacemaker version 1.1.12 and
+up. Depending on which version you are using, the information in this
+section may no longer be accurate. Look for the `acl_target`
+configuration element for more details on the new syntax.
+****************************
+
+By default, the users from the +haclient+ group have full access
+to the cluster (or, more precisely, to the CIB). Access control
+lists allow for finer access control to the cluster.
+
+Access control lists consist of an ordered set of access rules.
+Each rule allows read or write access or denies access
+completely. Rules are typically combined to produce a specific
+role. Then, users may be assigned a role.
+
+For instance, this is a role which defines a set of rules
+allowing management of a single resource:
+
+...............
+role bigdb_admin \
+    write meta:bigdb:target-role \
+    write meta:bigdb:is-managed \
+    write location:bigdb \
+    read ref:bigdb
+...............
+
+The first two rules allow modifying the +target-role+ and
++is-managed+ meta attributes which effectively enables users in
+this role to stop/start and manage/unmanage the resource. The
+constraints write access rule allows moving the resource around.
+Finally, the user is granted read access to the resource
+definition.
+
+For proper operation of all Pacemaker programs, it is advisable
+to add the following role to all users:
+
+...............
+role read_all \
+    read cib
+...............
+
+For finer grained read access try with the rules listed in the
+following role:
+
+...............
+role basic_read \
+    read node attribute:uname \
+    read node attribute:type \
+    read property \
+    read status
+...............
+
+It is however possible that some Pacemaker programs (e.g.
+`ptest`) may not function correctly if the whole CIB is not
+readable.
+
+Some of the ACL rules in the examples above are expanded by the
+shell to XPath specifications. For instance, 
++meta:bigdb:target-role+ expands to:
+
+........
+//primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']
+........
+
+You can see the expansion by showing XML:
+
+...............
+crm(live) configure# show xml bigdb_admin
+...
+<acls>
+  <acl_role id="bigdb_admin">
+      <write id="bigdb_admin-write"
+      xpath="//primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']"/>
+...............
+
+Many different XPath expressions can have equal meaning. For
+instance, the following two are equal, but only the first one is
+going to be recognized as shortcut:
+
+...............
+//primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']
+//resources/primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']
+...............
+
+XPath is a powerful language, but you should try to keep your ACL
+xpaths simple and the builtin shortcuts should be used whenever
+possible.
+
+[[topics_Features_Resourcesets,Syntax: Resource sets]]
+=== Syntax: Resource sets
+
+Using resource sets can be a bit confusing unless one knows the
+details of the implementation in Pacemaker as well as how to interpret
+the syntax provided by `crmsh`.
+
+Three different types of resource sets are provided by `crmsh`, and
+each one implies different values for the two resource set attributes,
++sequential+ and +require-all+.
+
++sequential+::
+  If true, the resources in the set do not depend on each other
+  internally. Setting +sequential+ to +true+ implies a strict order of
+  dependency within the set.
+
++require-all+::
+  If false, only one resource in the set is required to fulfil the
+  requirements of the set. The set of A, B and C with +require-all+
+  set to +false+ is be read as "A OR B OR C" when its dependencies
+  are resolved.
+
+The three types of resource sets modify the attributes in the
+following way:
+
+1. Implicit sets (no brackets). +sequential=true+, +require-all=true+
+2. Parenthesis set (+(+ ... +)+). +sequential=false+, +require-all=true+
+3. Bracket set (+[+ ... +]+). +sequential=false+, +require-all=false+
+
+To create a set with the properties +sequential=true+ and
++require-all=false+, explicitly set +sequential+ in a bracketed set,
++[ A B C sequential=true ]+.
+
+To create multiple sets with both +sequential+ and +require-all+ set to
+true, explicitly set +sequential+ in a parenthesis set:
++A B ( C D sequential=true )+.
+
+[[topics_Features_AttributeListReferences,Syntax: Attribute list references]]
+=== Syntax: Attribute list references
+
+Attribute lists are used to set attributes and parameters for
+resources, constraints and property definitions. For example, to set
+the virtual IP used by an +IPAddr2+ resource the attribute +ip+ can be
+set in an attribute list for that resource.
+
+Attribute lists can have identifiers that name them, and other
+resources can reuse the same attribute list by referring to that name
+using an +$id-ref+. For example, the following statement defines a
+simple dummy resource with an attribute list which sets the parameter
++state+ to the value 1 and sets the identifier for the attribute list
+to +on-state+:
+
+..............
+primitive dummy-1 Dummy params $id=on-state state=1
+..............
+
+To refer to this attribute list from a different resource, refer to
+the +on-state+ name using an id-ref:
+
+..............
+primitive dummy-2 Dummy params $id-ref=on-state
+..............
+
+The resource +dummy-2+ will now also have the parameter +state+ set to the value 1.
+
+[[topics_Features_AttributeReferences,Syntax: Attribute references]]
+=== Syntax: Attribute references
+
+In some cases, referencing complete attribute lists is too
+coarse-grained, for example if two different parameters with different
+names should have the same value set. Instead of having to copy the
+value in multiple places, it is possible to create references to
+individual attributes in attribute lists.
+
+To name an attribute in order to be able to refer to it later, prefix
+the attribute name with a +$+ character (as seen above with the
+special names +$id+ and +$id-ref+:
+
+............
+primitive dummy-1 Dummy params $state=1
+............
+
+The identifier +state+ can now be used to refer to this attribute from other
+primitives, using the +@<id>+ syntax:
+
+............
+primitive dummy-2 Dummy params @state
+............
+
+In some cases, using the attribute name as the identifier doesn't work
+due to name clashes. In this case, the syntax +$<id>:<name>=<value>+
+can be used to give the attribute a different identifier:
+
+............
+primitive dummy-1 params $dummy-state-on:state=1
+primitive dummy-2 params @dummy-state-on
+............
+
+There is also the possibility that two resources both use the same
+attribute value but with different names. For example, a web server
+may have a parameter +server_ip+ for setting the IP address where it
+listens for incoming requests, and a virtual IP resource may have a
+parameter called +ip+ which sets the IP address it creates. To
+configure these two resources with an IP without repeating the value,
+the reference can be given a name using the syntax +@<id>:<name>+.
+
+Example:
+............
+primitive virtual-ip IPaddr2 params $vip:ip=192.168.1.100
+primitive webserver apache params @vip:server_ip
+............
+
+[[topics_Reference,Command reference]]
+== Command reference
+
+We define a small and simple language. Most commands consist of
+just a list of simple tokens. The only complex constructs are
+found at the `configure` level.
+
+The syntax is described in a somewhat informal manner: `<>`
+denotes a string, `[]` means that the construct is optional, the
+ellipsis (`...`) signifies that the previous construct may be
+repeated, `|` means pick one of many, and the rest are literals
+(strings, `:`, `=`).
+
+[[cmdhelp_root_status,Cluster status]]
+=== `status`
+
+Show cluster status. The status is displayed by `crm_mon`. Supply
+additional arguments for more information or different format.
+See `crm_mon(8)` for more details.
+
+Usage:
+...............
+status [<option> ...]
+
+option :: bynode | inactive | ops | timing | failcounts
+...............
+
+[[cmdhelp_cluster,Cluster setup and management]]
+=== `cluster`
+
+Whole-cluster configuration management with High Availability
+awareness.
+
+The commands on the cluster level allows configuration and
+modification of the underlying cluster infrastructure, and also
+supplies tools to do whole-cluster systems management.
+
+These commands enable easy installation and maintenance of a HA
+cluster, by providing support for package installation, configuration
+of the cluster messaging layer, file system setup and more.
+
+[[cmdhelp_cluster_start,Start cluster services]]
+==== `start`
+
+Starts the cluster-related system services on this node.
+
+Usage:
+.........
+start
+.........
+
+[[cmdhelp_cluster_stop,Stop cluster services]]
+==== `stop`
+
+Stops the cluster-related system services on this node.
+
+Usage:
+.........
+stop
+.........
+
+[[cmdhelp_cluster_init,Initializes a new HA cluster]]
+==== `init`
+
+Installs and configures a basic HA cluster on a set of nodes.
+
+Usage:
+........
+init node1 node2 node3
+init --dry-run node1 node2 node3
+........
+
+[[cmdhelp_cluster_add,Add a new node to the cluster]]
+==== `add`
+
+This command simplifies the process of adding a new node to a running
+cluster. The new node will be installed and configured with the
+packages and configuration files needed to run the cluster
+resources. If a cluster file system is used, the new node will be set
+up to host the file system.
+
+This command should be executed from a node already in the cluster.
+
+Usage:
+...............
+add <node>
+...............
+
+[[cmdhelp_cluster_remove,Remove a node from the cluster]]
+==== `remove`
+
+This command simplifies the process of removing a node from the
+cluster, moving any resources hosted by that node to other nodes.
+
+Usage:
+...............
+remove <node>
+...............
+
+[[cmdhelp_cluster_status,Cluster status check]]
+==== `status`
+
+Reports the status for the cluster messaging layer on the local
+node.
+
+Usage:
+...............
+status
+...............
+
+[[cmdhelp_cluster_health,Cluster health check]]
+==== `health`
+
+Runs a larger set of tests and queries on all nodes in the cluster to
+verify the general system health and detect potential problems.
+
+Usage:
+...............
+health
+...............
+
+[[cmdhelp_cluster_wait_for_startup,Wait for cluster to start]]
+==== `wait_for_startup`
+
+Mostly useful in scripts or automated workflows, this command will
+attempt to connect to the local cluster node repeatedly. The command
+will keep trying until the cluster node responds, or the `timeout`
+elapses. The timeout can be changed by supplying a value in seconds as
+an argument.
+
+Usage:
+........
+wait_for_startup
+........
+
+[[cmdhelp_cluster_run,Execute an arbitrary command on all nodes]]
+==== `run`
+
+This command takes a shell statement as argument, executes that
+statement on all nodes in the cluster, and reports the result.
+
+Usage:
+...............
+run <command>
+...............
+
+Example:
+...............
+run "cat /proc/uptime"
+...............
+
+[[cmdhelp_script,Cluster script management]]
+=== `script`
+
+Cluster scripts can perform cluster-wide configuration,
+validation and management. See the `list` command for
+an overview of available scripts.
+
+[[cmdhelp_script_list,List available scripts]]
+==== `list`
+
+Lists the available cluster scripts.
+
+Usage:
+............
+list
+............
+
+[[cmdhelp_script_verify,Verify the cluster script]]
+==== `verify`
+
+Mainly useful when creating new scripts, this command
+verifies that the script definition has all necessary
+fields and that the referenced actions exist.
+
+Usage:
+............
+verify <script>
+............
+
+[[cmdhelp_script_describe,Describe the cluster script]]
+==== `describe`
+
+Prints a description and short summary of the cluster script, with
+descriptions of all parameters, both required and optional.
+
+Usage:
+............
+describe <script>
+............
+
+[[cmdhelp_script_steps,List steps in cluster script]]
+==== `steps`
+
+List the names of all steps in the cluster script.
+
+This command is intended for use by automated tools
+and the web frontend.
+
+Usage:
+............
+steps <script>
+............
+
+
+[[cmdhelp_script_run,Execute the cluster script]]
+==== `run`
+
+Runs a cluster script. Can optionally take at least two arguments:
+* `nodes=<nodes>`: List of nodes that the script runs over
+* `dry_run=yes|no`: If set, the script will not perform any modifications.
+
+Additional arguments may be available depending on the cluster
+script. Use the `describe` command to see what arguments are
+provided.
+
+Usage:
+.............
+run <script> [args...]
+.............
+
+Example:
+.............
+run health dry_run=yes verbose=yes
+run init nodes="node-1 node-2 node-3"
+.............
+
+[[cmdhelp_corosync,Corosync management]]
+=== `corosync`
+
+Corosync is the underlying messaging layer for most HA clusters.
+This level provides commands for editing and managing the corosync
+configuration.
+
+[[cmdhelp_corosync_status,Display the corosync status]]
+==== `status`
+
+Displays the status of Corosync, including the votequorum state.
+
+Usage:
+.........
+status
+.........
+
+[[cmdhelp_corosync_show,Display the corosync configuration]]
+==== `show`
+
+Displays the corosync configuration on the current node.
+
+.........
+show
+.........
+
+[[cmdhelp_corosync_edit,Edit the corosync configuration]]
+==== `edit`
+
+Opens the Corosync configuration file in an editor.
+
+Usage:
+.........
+edit
+.........
+
+[[cmdhelp_corosync_log,Show the corosync log file]]
+==== `log`
+
+Opens the log file specified in the corosync configuration file. If no
+log file is configured, this command returns an error.
+
+The pager used can be configured either using the PAGER
+environment variable or in `crm.conf`.
+
+Usage:
+.........
+log
+.........
+
+[[cmdhelp_corosync_reload,Reload the corosync configuration]]
+==== `reload`
+
+Tells all instances of corosync in this cluster to reload
+`corosync.conf`.
+
+After pushing a new configuration to all cluster nodes, call this
+command to make corosync use the new configuration.
+
+Usage:
+.........
+reload
+.........
+
+[[cmdhelp_corosync_push,Push the corosync configuration]]
+==== `push`
+
+Pushes the corosync configuration file on this node to
+the list of nodes provided. If no target nodes are given,
+the configuration is pushed to all other nodes in the cluster.
+
+It is recommended to use `csync2` to distribute the cluster
+configuration files rather than relying on this command.
+
+Usage:
+.........
+push [node] ...
+.........
+
+Example:
+.........
+push node-2 node-3
+.........
+
+[[cmdhelp_corosync_pull,Pulls the corosync configuration]]
+==== `pull`
+
+Gets the corosync configuration from another node and copies
+it to this node.
+
+Usage:
+.........
+pull <node>
+.........
+
+[[cmdhelp_corosync_diff,Diffs the corosync configuration]]
+==== `diff`
+
+Diffs the corosync configurations on different nodes. If no nodes are
+given as arguments, the corosync configurations on all nodes in the
+cluster are compared.
+
+`diff` takes an option argument `--checksum`, to force checksum mode.
+
+If the number of nodes to compare are greater than two, `diff`
+automatically switches to checksum mode.
+
+Usage:
+.........
+diff [--checksum] [node...]
+.........
+
+[[cmdhelp_corosync_add-node,Add a corosync node]]
+==== `add-node`
+
+Adds a node to the corosync configuration. This is used with the `udpu`
+type configuration in corosync.
+
+A nodeid for the added node is generated automatically.
+
+Note that this command assumes that only a single ring is used, and
+sets only the address for ring0.
+
+Usage:
+.........
+add-node <addr>
+.........
+
+[[cmdhelp_corosync_del-node,Remove a corosync node]]
+==== `del-node`
+
+Removes a node from the corosync configuration. The argument given is
+the `ring0_addr` address set in the configuration file.
+
+Usage:
+.........
+del-node <addr>
+.........
+
+[[cmdhelp_corosync_get,Get a corosync configuration value]]
+==== `get`
+
+Returns the value configured in `corosync.conf`, which is not
+necessarily the value used in the running configuration. See `reload`
+for telling corosync about configuration changes.
+
+The argument is the complete dot-separated path to the value.
+
+If there are multiple values configured with the same path, the
+command returns all values for that path. For example, to get all
+configured `ring0_addr` values, use this command:
+
+Example:
+........
+get nodelist.node.ring0_addr
+........
+
+[[cmdhelp_corosync_set,Set a corosync configuration value]]
+==== `set`
+
+Sets the value identified by the given path. If the value does not
+exist in the configuration file, it will be added. However, if the
+section containing the value does not exist, the command will fail.
+
+Usage:
+.........
+set quorum.expected_votes 2
+.........
+
+[[cmdhelp_cib,CIB shadow management]]
+=== `cib` (shadow CIBs)
+
+This level is for management of shadow CIBs. It is available both
+at the top level and the `configure` level.
+
+All the commands are implemented using `cib_shadow(8)` and the
+`CIB_shadow` environment variable. The user prompt always
+includes the name of the currently active shadow or the live CIB.
+
+[[cmdhelp_cib_new,create a new shadow CIB]]
+==== `new`
+
+Create a new shadow CIB. The live cluster configuration and
+status is copied to the shadow CIB.
+
+If the name of the shadow is omitted, we create a temporary CIB
+shadow. It is useful if multiple level sessions are desired
+without affecting the cluster. A temporary CIB shadow is short
+lived and will be removed either on `commit` or on program exit.
+Note that if the temporary shadow is not committed all changes in
+the temporary shadow are lost.
+
+Specify `withstatus` if you want to edit the status section of
+the shadow CIB (see the <<cmdhelp_cibstatus,cibstatus section>>).
+Add `force` to force overwriting the existing shadow CIB.
+
+To start with an empty configuration that is not copied from the live
+CIB, specify the `empty` keyword. (This also allows a shadow CIB to be
+created in case no cluster is running.)
+
+Usage:
+...............
+new [<cib>] [withstatus] [force] [empty]
+...............
+
+[[cmdhelp_cib_delete,delete a shadow CIB]]
+==== `delete`
+
+Delete an existing shadow CIB.
+
+Usage:
+...............
+delete <cib>
+...............
+
+[[cmdhelp_cib_reset,copy live cib to a shadow CIB]]
+==== `reset`
+
+Copy the current cluster configuration into the shadow CIB.
+
+Usage:
+...............
+reset <cib>
+...............
+
+[[cmdhelp_cib_commit,copy a shadow CIB to the cluster]]
+==== `commit`
+
+Apply a shadow CIB to the cluster. If the shadow name is omitted
+then the current shadow CIB is applied.
+
+Temporary shadow CIBs are removed automatically on commit.
+
+Usage:
+...............
+commit [<cib>]
+...............
+
+[[cmdhelp_cib_use,change working CIB]]
+==== `use`
+
+Choose a CIB source. If you want to edit the status from the
+shadow CIB specify `withstatus` (see <<cmdhelp_cibstatus,`cibstatus`>>).
+Leave out the CIB name to switch to the running CIB.
+
+Usage:
+...............
+use [<cib>] [withstatus]
+...............
+
+[[cmdhelp_cib_diff,diff between the shadow CIB and the live CIB]]
+==== `diff`
+
+Print differences between the current cluster configuration and
+the active shadow CIB.
+
+Usage:
+...............
+diff
+...............
+
+[[cmdhelp_cib_list,list all shadow CIBs]]
+==== `list`
+
+List existing shadow CIBs.
+
+Usage:
+...............
+list
+...............
+
+[[cmdhelp_cib_import,import a CIB or PE input file to a shadow]]
+==== `import`
+
+At times it may be useful to create a shadow file from the
+existing CIB. The CIB may be specified as file or as a PE input
+file number. The shell will look up files in the local directory
+first and then in the PE directory (typically `/var/lib/pengine`).
+Once the CIB file is found, it is copied to a shadow and this
+shadow is immediately available for use at both `configure` and
+`cibstatus` levels.
+
+If the shadow name is omitted then the target shadow is named
+after the input CIB file.
+
+Note that there are often more than one PE input file, so you may
+need to specify the full name.
+
+Usage:
+...............
+import {<file>|<number>} [<shadow>]
+...............
+Examples:
+...............
+import pe-warn-2222
+import 2289 issue2
+...............
+
+[[cmdhelp_cib_cibstatus,CIB status management and editing]]
+==== `cibstatus`
+
+Enter edit and manage the CIB status section level. See the
+<<cmdhelp_cibstatus,CIB status management section>>.
+
+[[cmdhelp_ra,Resource Agents (RA) lists and documentation]]
+=== `ra`
+
+This level contains commands which show various information about
+the installed resource agents. It is available both at the top
+level and at the `configure` level.
+
+[[cmdhelp_ra_classes,list classes and providers]]
+==== `classes`
+
+Print all resource agents' classes and, where appropriate, a list
+of available providers.
+
+Usage:
+...............
+classes
+...............
+
+[[cmdhelp_ra_list,list RA for a class (and provider)]]
+==== `list`
+
+List available resource agents for the given class. If the class
+is `ocf`, supply a provider to get agents which are available
+only from that provider.
+
+Usage:
+...............
+list <class> [<provider>]
+...............
+Example:
+...............
+list ocf pacemaker
+...............
+
+[[cmdhelp_ra_info,show meta data for a RA]]
+==== `info` (`meta`)
+
+Show the meta-data of a resource agent type. This is where users
+can find information on how to use a resource agent. It is also
+possible to get information from some programs: `pengine`,
+`crmd`, `cib`, and `stonithd`. Just specify the program name
+instead of an RA.
+
+Usage:
+...............
+info [<class>:[<provider>:]]<type>
+info <type> <class> [<provider>] (obsolete)
+...............
+Example:
+...............
+info apache
+info ocf:pacemaker:Dummy
+info stonith:ipmilan
+info pengine
+...............
+
+[[cmdhelp_ra_providers,show providers for a RA and a class]]
+==== `providers`
+
+List providers for a resource agent type. The class parameter
+defaults to `ocf`.
+
+Usage:
+...............
+providers <type> [<class>]
+...............
+Example:
+...............
+providers apache
+...............
+
+[[cmdhelp_resource,Resource management]]
+=== `resource`
+
+At this level resources may be managed.
+
+All (or almost all) commands are implemented with the CRM tools
+such as `crm_resource(8)`.
+
+[[cmdhelp_resource_status,show status of resources]]
+==== `status` (`show`, `list`)
+
+Print resource status. If the resource parameter is left out
+status of all resources is printed.
+
+Usage:
+...............
+status [<rsc>]
+...............
+
+[[cmdhelp_resource_start,start a resource]]
+==== `start`
+
+Start a resource by setting the `target-role` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `target-role` attributes
+are removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+start <rsc>
+...............
+
+[[cmdhelp_resource_stop,stop a resource]]
+==== `stop`
+
+Stop a resource using the `target-role` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `target-role` attributes
+are removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+stop <rsc>
+...............
+
+[[cmdhelp_resource_restart,restart a resource]]
+==== `restart`
+
+Restart a resource. This is essentially a shortcut for resource
+stop followed by a start. The shell is first going to wait for
+the stop to finish, that is for all resources to really stop, and
+only then to order the start action. Due to this command
+entailing a whole set of operations, informational messages are
+printed to let the user see some progress.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+restart <rsc>
+...............
+Example:
+...............
+# crm resource restart g_webserver
+INFO: ordering g_webserver to stop
+waiting for stop to finish .... done
+INFO: ordering g_webserver to start
+#
+...............
+
+[[cmdhelp_resource_promote,promote a master-slave resource]]
+==== `promote`
+
+Promote a master-slave resource using the `target-role`
+attribute.
+
+Usage:
+...............
+promote <rsc>
+...............
+
+[[cmdhelp_resource_demote,demote a master-slave resource]]
+==== `demote`
+
+Demote a master-slave resource using the `target-role`
+attribute.
+
+Usage:
+...............
+demote <rsc>
+...............
+
+[[cmdhelp_resource_manage,put a resource into managed mode]]
+==== `manage`
+
+Manage a resource using the `is-managed` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `is-managed` attributes are
+removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+manage <rsc>
+...............
+
+[[cmdhelp_resource_unmanage,put a resource into unmanaged mode]]
+==== `unmanage`
+
+Unmanage a resource using the `is-managed` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `is-managed` attributes are
+removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+unmanage <rsc>
+...............
+
+[[cmdhelp_resource_migrate,migrate a resource to another node]]
+==== `migrate` (`move`)
+
+Migrate a resource to a different node. If node is left out, the
+resource is migrated by creating a constraint which prevents it from
+running on the current node. Additionally, you may specify a
+lifetime for the constraint---once it expires, the location
+constraint will no longer be active.
+
+Usage:
+...............
+migrate <rsc> [<node>] [<lifetime>] [force]
+...............
+
+[[cmdhelp_resource_unmigrate,unmigrate a resource to another node]]
+==== `unmigrate` (`unmove`)
+
+Remove the constraint generated by the previous migrate command.
+
+.Note
+****************************
+`refresh` has been deprecated and is now
+an alias for `cleanup`.
+****************************
+
+Usage:
+...............
+unmigrate <rsc>
+...............
+
+[[cmdhelp_resource_maintenance,Enable/disable per-resource maintenance mode]]
+==== `maintenance`
+
+Enables or disables the per-resource maintenance mode. When this mode
+is enabled, no monitor operations will be triggered for the resource.
+
+.Note
+****************************
+`reprobe` has been deprecated and is now
+an alias for `cleanup`.
+****************************
+
+Usage:
+..................
+maintenance <resource> [on|off|true|false]
+..................
+
+Example:
+..................
+maintenance rsc1
+maintenance rsc2 off
+..................
+
+[[cmdhelp_resource_param,manage a parameter of a resource]]
+==== `param`
+
+Show/edit/delete a parameter of a resource.
+
+Usage:
+...............
+param <rsc> set <param> <value>
+param <rsc> delete <param>
+param <rsc> show <param>
+...............
+Example:
+...............
+param ip_0 show ip
+...............
+
+[[cmdhelp_resource_secret,manage sensitive parameters]]
+==== `secret`
+
+Sensitive parameters can be kept in local files rather than CIB
+in order to prevent accidental data exposure. Use the `secret`
+command to manage such parameters. `stash` and `unstash` move the
+value from the CIB and back to the CIB respectively. The `set`
+subcommand sets the parameter to the provided value. `delete`
+removes the parameter completely. `show` displays the value of
+the parameter from the local file. Use `check` to verify if the
+local file content is valid.
+
+Usage:
+...............
+secret <rsc> set <param> <value>
+secret <rsc> stash <param>
+secret <rsc> unstash <param>
+secret <rsc> delete <param>
+secret <rsc> show <param>
+secret <rsc> check <param>
+...............
+Example:
+...............
+secret fence_1 show password
+secret fence_1 stash password
+secret fence_1 set password secret_value
+...............
+
+[[cmdhelp_resource_meta,manage a meta attribute]]
+==== `meta`
+
+Show/edit/delete a meta attribute of a resource. Currently, all
+meta attributes of a resource may be managed with other commands
+such as `resource stop`.
+
+Usage:
+...............
+meta <rsc> set <attr> <value>
+meta <rsc> delete <attr>
+meta <rsc> show <attr>
+...............
+Example:
+...............
+meta ip_0 set target-role stopped
+...............
+
+[[cmdhelp_resource_utilization,manage a utilization attribute]]
+==== `utilization`
+
+Show/edit/delete a utilization attribute of a resource. These
+attributes describe hardware requirements. By setting the
+`placement-strategy` cluster property appropriately, it is
+possible then to distribute resources based on resource
+requirements and node size. See also <<cmdhelp_node_utilization,node utilization attributes>>.
+
+Usage:
+...............
+utilization <rsc> set <attr> <value>
+utilization <rsc> delete <attr>
+utilization <rsc> show <attr>
+...............
+Example:
+...............
+utilization xen1 set memory 4096
+...............
+
+[[cmdhelp_resource_failcount,manage failcounts]]
+==== `failcount`
+
+Show/edit/delete the failcount of a resource.
+
+Usage:
+...............
+failcount <rsc> set <node> <value>
+failcount <rsc> delete <node>
+failcount <rsc> show <node>
+...............
+Example:
+...............
+failcount fs_0 delete node2
+...............
+
+[[cmdhelp_resource_cleanup,cleanup resource status]]
+==== `cleanup`
+
+Cleanup resource status. Typically done after the resource has
+temporarily failed. If a node is omitted, cleanup on all nodes.
+If there are many nodes, the command may take a while.
+
+Usage:
+...............
+cleanup <rsc> [<node>]
+...............
+
+[[cmdhelp_resource_refresh,refresh CIB from the LRM status]]
+==== `refresh`
+
+Refresh CIB from the LRM status.
+
+Usage:
+...............
+refresh [<node>]
+...............
+
+[[cmdhelp_resource_reprobe,probe for resources not started by the CRM]]
+==== `reprobe`
+
+Probe for resources not started by the CRM.
+
+Usage:
+...............
+reprobe [<node>]
+...............
+
+[[cmdhelp_resource_trace,start RA tracing]]
+==== `trace`
+
+Start tracing RA for the given operation. The trace files are
+stored in `$HA_VARLIB/trace_ra`. If the operation to be traced is
+monitor, note that the number of trace files can grow very
+quickly.
+
+If no operation name is given, crmsh will attempt to trace all
+operations for the RA. This includes any configured operations, start
+and stop as well as promote/demote for multistate resources.
+
+To trace the probe operation which exists for all resources, either
+set a trace for `monitor` with interval `0`, or use `probe` as the
+operation name.
+
+Usage:
+...............
+trace <rsc> [<op> [<interval>] ]
+...............
+Example:
+...............
+trace fs start
+trace webserver
+trace webserver probe
+trace fs monitor 0
+...............
+
+[[cmdhelp_resource_untrace,stop RA tracing]]
+==== `untrace`
+
+Stop tracing RA for the given operation. If no operation name is
+given, crmsh will attempt to stop tracing all operations in resource.
+
+Usage:
+...............
+untrace <rsc> [<op> [<interval>] ]
+...............
+Example:
+...............
+untrace fs start
+untrace webserver
+...............
+
+[[cmdhelp_resource_scores,Display resource scores]]
+=== `scores`
+
+Display the allocation scores for all resources.
+
+Usage:
+................
+scores
+................
+
+[[cmdhelp_node,Nodes management]]
+=== `node`
+
+Node management and status commands.
+
+[[cmdhelp_node_status,show nodes' status as XML]]
+==== `status`
+
+Show nodes' status as XML. If the node parameter is omitted then
+all nodes are shown.
+
+Usage:
+...............
+status [<node>]
+...............
+
+[[cmdhelp_node_show,show node]]
+==== `show`
+
+Show a node definition. If the node parameter is omitted then all
+nodes are shown.
+
+Usage:
+...............
+show [<node>]
+...............
+
+[[cmdhelp_node_standby,put node into standby]]
+==== `standby`
+
+Set a node to standby status. The node parameter defaults to the
+node where the command is run.
+
+Additionally, you may specify a lifetime for the standby---if set to
+`reboot`, the node will be back online once it reboots. `forever` will
+keep the node in standby after reboot. The life time defaults to
+`forever`.
+
+Usage:
+...............
+standby [<node>] [<lifetime>]
+
+lifetime :: reboot | forever
+...............
+
+Example:
+...............
+standby bob reboot
+...............
+
+
+[[cmdhelp_node_online,set node online]]
+==== `online`
+
+Set a node to online status.
+
+The node parameter defaults to the node where the command is run.
+
+Usage:
+...............
+online [<node>]
+...............
+
+[[cmdhelp_node_maintenance,put node into maintenance mode]]
+==== `maintenance`
+
+Set the node status to maintenance. This is equivalent to the
+cluster-wide `maintenance-mode` property but puts just one node
+into the maintenance mode.
+
+The node parameter defaults to the node where the command is run.
+
+Usage:
+...............
+maintenance [<node>]
+...............
+
+[[cmdhelp_node_ready,put node into ready mode]]
+==== `ready`
+
+Set the node's maintenance status to `off`. The node should be
+now again fully operational and capable of running resource
+operations.
+
+The node parameter defaults to the node where the command is run.
+
+Usage:
+...............
+ready [<node>]
+...............
+
+[[cmdhelp_node_fence,fence node]]
+==== `fence`
+
+Make CRM fence a node. This functionality depends on stonith
+resources capable of fencing the specified node. No such stonith
+resources, no fencing will happen.
+
+Usage:
+...............
+fence <node>
+...............
+
+[[cmdhelp_node_clearstate,Clear node state]]
+==== `clearstate`
+
+Resets and clears the state of the specified node. This node is
+afterwards assumed clean and offline. This command can be used to
+manually confirm that a node has been fenced (e.g., powered off).
+
+Be careful! This can cause data corruption if you confirm that a node is
+down that is, in fact, not cleanly down - the cluster will proceed as if
+the fence had succeeded, possibly starting resources multiple times.
+
+Usage:
+...............
+clearstate <node>
+...............
+
+[[cmdhelp_node_delete,delete node]]
+==== `delete`
+
+Delete a node. This command will remove the node from the CIB
+and, in case the cluster stack is running, use the appropriate
+program (`crm_node` or `hb_delnode`) to remove the node from the
+membership.
+
+If the node is still listed as active and a member of our
+partition we refuse to remove it. With the global force option
+(`-F`) we will try to delete the node anyway.
+
+Usage:
+...............
+delete <node>
+...............
+
+[[cmdhelp_node_attribute,manage attributes]]
+==== `attribute`
+
+Edit node attributes. This kind of attribute should refer to
+relatively static properties, such as memory size.
+
+Usage:
+...............
+attribute <node> set <attr> <value>
+attribute <node> delete <attr>
+attribute <node> show <attr>
+...............
+Example:
+...............
+attribute node_1 set memory_size 4096
+...............
+
+[[cmdhelp_node_utilization,manage utilization attributes]]
+==== `utilization`
+
+Edit node utilization attributes. These attributes describe
+hardware characteristics as integer numbers such as memory size
+or the number of CPUs. By setting the `placement-strategy`
+cluster property appropriately, it is possible then to distribute
+resources based on resource requirements and node size. See also
+<<cmdhelp_resource_utilization,resource utilization attributes>>.
+
+Usage:
+...............
+utilization <node> set <attr> <value>
+utilization <node> delete <attr>
+utilization <node> show <attr>
+...............
+Examples:
+...............
+utilization node_1 set memory 16384
+utilization node_1 show cpu
+...............
+
+[[cmdhelp_node_status-attr,manage status attributes]]
+==== `status-attr`
+
+Edit node attributes which are in the CIB status section, i.e.
+attributes which hold properties of a more volatile nature. One
+typical example is attribute generated by the `pingd` utility.
+
+Usage:
+...............
+status-attr <node> set <attr> <value>
+status-attr <node> delete <attr>
+status-attr <node> show <attr>
+...............
+Example:
+...............
+status-attr node_1 show pingd
+...............
+
+[[cmdhelp_site,site support]]
+=== `site`
+
+A cluster may consist of two or more subclusters in different and
+distant locations. This set of commands supports such setups.
+
+[[cmdhelp_site_ticket,manage site tickets]]
+==== `ticket`
+
+Tickets are cluster-wide attributes. They can be managed at the
+site where this command is executed.
+
+It is then possible to constrain resources depending on the
+ticket availability (see the <<cmdhelp_configure_rsc_ticket,`rsc_ticket`>> command
+for more details).
+
+Usage:
+...............
+ticket {grant|revoke|standby|activate|show|time|delete} <ticket>
+...............
+Example:
+...............
+ticket grant ticket1
+...............
+
+[[cmdhelp_options,user preferences]]
+=== `options`
+
+The user may set various options for the crm shell itself.
+
+[[cmdhelp_options_skill-level,set skill level]]
+==== `skill-level`
+
+Based on the skill-level setting, the user is allowed to use only
+a subset of commands. There are three levels: operator,
+administrator, and expert. The operator level allows only
+commands at the `resource` and `node` levels, but not editing
+or deleting resources. The administrator may do that and may also
+configure the cluster at the `configure` level and manage the
+shadow CIBs. The expert may do all.
+
+Usage:
+...............
+skill-level <level>
+
+level :: operator | administrator | expert
+...............
+
+.Note on security
+****************************
+The `skill-level` option is advisory only. There is nothing
+stopping any users change their skill level (see
+<<topics_Features_Security,Access Control Lists (ACL)>> on how to enforce
+access control).
+****************************
+
+[[cmdhelp_options_user,set the cluster user]]
+==== `user`
+
+Sufficient privileges are necessary in order to manage a
+cluster: programs such as `crm_verify` or `crm_resource` and,
+ultimately, `cibadmin` have to be run either as `root` or as the
+CRM owner user (typically `hacluster`). You don't have to worry
+about that if you run `crm` as `root`. A more secure way is to
+run the program with your usual privileges, set this option to
+the appropriate user (such as `hacluster`), and setup the
+`sudoers` file.
+
+Usage:
+...............
+user system-user
+...............
+Example:
+...............
+user hacluster
+...............
+
+[[cmdhelp_options_editor,set preferred editor program]]
+==== `editor`
+
+The `edit` command invokes an editor. Use this to specify your
+preferred editor program. If not set, it will default to either
+the value of the `EDITOR` environment variable or to one of the
+standard UNIX editors (`vi`,`emacs`,`nano`).
+
+Usage:
+...............
+editor program
+...............
+Example:
+...............
+editor vim
+...............
+
+[[cmdhelp_options_pager,set preferred pager program]]
+==== `pager`
+
+The `view` command displays text through a pager. Use this to
+specify your preferred pager program. If not set, it will default
+to either the value of the `PAGER` environment variable or to one
+of the standard UNIX system pagers (`less`,`more`,`pg`).
+
+[[cmdhelp_options_sort-elements,sort CIB elements]]
+==== `sort-elements`
+
+`crm` by default sorts CIB elements. If you want them appear in
+the order they were created, set this option to `no`.
+
+Usage:
+...............
+sort-elements {yes|no}
+...............
+Example:
+...............
+sort-elements no
+...............
+
+[[cmdhelp_options_wait,synchronous operation]]
+==== `wait`
+
+In normal operation, `crm` runs a command and gets back
+immediately to process other commands or get input from the user.
+With this option set to `yes` it will wait for the started
+transition to finish. In interactive mode dots are printed to
+indicate progress.
+
+Usage:
+...............
+wait {yes|no}
+...............
+Example:
+...............
+wait yes
+...............
+
+[[cmdhelp_options_output,set output type]]
+==== `output`
+
+`crm` can adorn configurations in two ways: in color (similar to
+for instance the `ls --color` command) and by showing keywords in
+upper case. Possible values are `plain`, `color`, and
+'uppercase'. It is possible to combine the latter two in order to
+get an upper case xmass tree. Just set this option to
+`color,uppercase`.
+
+[[cmdhelp_options_colorscheme,set colors for output]]
+==== `colorscheme`
+
+With `output` set to `color`, a comma separated list of colors
+from this option are used to emphasize:
+
+- keywords
+- object ids
+- attribute names
+- attribute values
+- scores
+- resource references
+
+`crm` can show colors only if there is curses support for python
+installed (usually provided by the `python-curses` package). The
+colors are whatever is available in your terminal. Use `normal`
+if you want to keep the default foreground color.
+
+This user preference defaults to
+`yellow,normal,cyan,red,green,magenta` which is good for
+terminals with dark background. You may want to change the color
+scheme and save it in the preferences file for other color
+setups.
+
+Example:
+...............
+colorscheme yellow,normal,blue,red,green,magenta
+...............
+
+[[cmdhelp_options_check-frequency,when to perform semantic check]]
+==== `check-frequency`
+
+Semantic check of the CIB or elements modified or created may be
+done on every configuration change (`always`), when verifying
+(`on-verify`) or `never`. It is by default set to `always`.
+Experts may want to change the setting to `on-verify`.
+
+The checks require that resource agents are present. If they are
+not installed at the configuration time set this preference to
+`never`.
+
+See <<topics_Features_Checks,Configuration semantic checks>> for more details.
+
+[[cmdhelp_options_check-mode,how to treat semantic errors]]
+==== `check-mode`
+
+Semantic check of the CIB or elements modified or created may be
+done in the `strict` mode or in the `relaxed` mode. In the former
+certain problems are treated as configuration errors. In the
+`relaxed` mode all are treated as warnings. The default is `strict`.
+
+See <<topics_Features_Checks,Configuration semantic checks>> for more details.
+
+[[cmdhelp_options_add-quotes,add quotes around parameters containing spaces]]
+==== `add-quotes`
+
+The shell (as in `/bin/sh`) parser strips quotes from the command
+line. This may sometimes make it really difficult to type values
+which contain white space. One typical example is the configure
+filter command. The crm shell will supply extra quotes around
+arguments which contain white space. The default is `yes`.
+
+.Note on quotes use
+****************************
+Adding quotes around arguments automatically has been introduced
+with version 1.2.2 and it is technically a regression. Being a
+regression is the only reason the `add-quotes` option exists. If
+you have custom shell scripts which would break, just set the
+`add-quotes` option to `no`.
+
+For instance, with adding quotes enabled, it is possible to do
+the following:
+...............
+# crm configure primitive d1 Dummy \
+    meta description="some description here"
+# crm configure filter 'sed "s/hostlist=./&node-c /"' fencing
+...............
+****************************
+
+[[cmdhelp_options_manage-children,how to handle children resource attributes]]
+==== `manage-children`
+
+Some resource management commands, such as `resource stop`, when
+the target resource is a group, may not always produce desired
+result. Each element, group and the primitive members, can have a
+meta attribute and those attributes may end up with conflicting
+values. Consider the following construct:
+...............
+crm(live)# configure show svc fs virtual-ip
+primitive fs Filesystem \
+    params device="/dev/drbd0" directory="/srv/nfs" fstype=ext3 \
+    op monitor interval=10s \
+    meta target-role=Started
+primitive virtual-ip IPaddr2 \
+    params ip=10.2.13.110 iflabel=1 \
+    op monitor interval=10s \
+    op start interval=0 \
+    meta target-role=Started
+group svc fs virtual-ip \
+    meta target-role=Stopped
+...............
+
+Even though the element +svc+ should be stopped, the group is
+actually running because all its members have the +target-role+
+set to +Started+:
+...............
+crm(live)# resource show svc
+resource svc is running on: xen-f
+...............
+
+Hence, if the user invokes +resource stop svc+ the intention is
+not clear. This preference gives the user an opportunity to
+better control what happens if attributes of group members have
+values which are in conflict with the same attribute of the group
+itself.
+
+Possible values are +ask+ (the default), +always+, and +never+.
+If set to +always+, the crm shell removes all children attributes
+which have values different from the parent. If set to +never+,
+all children attributes are left intact. Finally, if set to
++ask+, the user will be asked for each member what is to be done.
+
+[[cmdhelp_options_show,show current user preference]]
+==== `show`
+
+Display all current settings.
+
+Given an option name as argument, `show` will display only the value
+of that argument.
+
+Given +all+ as argument, `show` displays all available user options.
+
+Usage:
+........
+show [all|<option>]
+........
+
+Example:
+........
+show
+show skill-level
+show all
+........
+
+[[cmdhelp_options_set,Set the value of a given option]]
+==== `set`
+
+Sets the value of an option. Takes the fully qualified
+name of the option as argument, as displayed by +show all+.
+
+The modified option value is stored in the user-local
+configuration file, usually found in +~/.config/crm/crm.conf+.
+
+Usage:
+........
+set <option> <value>
+........
+
+Example:
+........
+set color.warn "magenta bold"
+set editor nano
+........
+
+[[cmdhelp_options_save,save the user preferences to the rc file]]
+==== `save`
+
+Save current settings to the rc file (+$HOME/.config/crm/rc+). On
+further `crm` runs, the rc file is automatically read and parsed.
+
+[[cmdhelp_options_reset,reset user preferences to factory defaults]]
+==== `reset`
+
+This command resets all user options to the defaults. If used as
+a single-shot command, the rc file (+$HOME/.config/crm/rc+) is
+reset to the defaults too.
+
+[[cmdhelp_configure,CIB configuration]]
+=== `configure`
+
+This level enables all CIB object definition commands.
+
+The configuration may be logically divided into four parts:
+nodes, resources, constraints, and (cluster) properties and
+attributes.  Each of these commands support one or more basic CIB
+objects.
+
+Nodes and attributes describing nodes are managed using the
+`node` command.
+
+Commands for resources are:
+
+- `primitive`
+- `monitor`
+- `group`
+- `clone`
+- `ms`/`master` (master-slave)
+
+In order to streamline large configurations, it is possible to
+define a template which can later be referenced in primitives:
+
+- `rsc_template`
+
+In that case the primitive inherits all attributes defined in the
+template.
+
+There are three types of constraints:
+
+- `location`
+- `colocation`
+- `order`
+
+It is possible to define fencing order (stonith resource
+priorities):
+
+- `fencing_topology`
+
+Finally, there are the cluster properties, resource meta
+attributes defaults, and operations defaults. All are just a set
+of attributes. These attributes are managed by the following
+commands:
+
+- `property`
+- `rsc_defaults`
+- `op_defaults`
+
+In addition to the cluster configuration, the Access Control
+Lists (ACL) can be setup to allow access to parts of the CIB for
+users other than +root+ and +hacluster+. The following commands
+manage ACL:
+
+- `user`
+- `role`
+
+In Pacemaker 1.1.12 and up, this command replaces the `user` command
+for handling ACLs:
+
+- `acl_target`
+
+The changes are applied to the current CIB only on ending the
+configuration session or using the `commit` command.
+
+Comments start with +#+ in the first line. The comments are tied
+to the element which follows. If the element moves, its comments
+will follow.
+
+[[cmdhelp_configure_node,define a cluster node]]
+==== `node`
+
+The node command describes a cluster node. Nodes in the CIB are
+commonly created automatically by the CRM. Hence, you should not
+need to deal with nodes unless you also want to define node
+attributes. Note that it is also possible to manage node
+attributes at the `node` level.
+
+Usage:
+...............
+node [$id=<id>] <uname>[:<type>]
+  [description=<description>]
+  [attributes [$id=<id>] [<score>:] [rule...]
+    <param>=<value> [<param>=<value>...]] | $id-ref=<ref>
+  [utilization [$id=<id>] [<score>:] [rule...]
+    <param>=<value> [<param>=<value>...]] | $id-ref=<ref>
+
+type :: normal | member | ping | remote
+...............
+Example:
+...............
+node node1
+node big_node attributes memory=64
+...............
+
+[[cmdhelp_configure_primitive,define a resource]]
+==== `primitive`
+
+The primitive command describes a resource. It may be referenced
+only once in group, clone, or master-slave objects. If it's not
+referenced, then it is placed as a single resource in the CIB.
+
+Operations may be specified anonymously, as a group or by reference:
+
+* "Anonymous", as a list of +op+ specifications. Use this
+  method if you don't need to reference the set of operations
+  elsewhere. This is the most common way to define operations.
+
+* If reusing operation sets is desired, use the +operations+ keyword
+  along with an id to give the operations set a name. Use the
+  +operations+ keyword and an id-ref value set to the id of another
+  operations set, to apply the same set of operations to this
+  primitive.
+
+Operation attributes which are not recognized are saved as
+instance attributes of that operation. A typical example is
++OCF_CHECK_LEVEL+.
+
+For multistate resources, roles are specified as +role=<role>+.
+
+A template may be defined for resources which are of the same
+type and which share most of the configuration. See
+<<cmdhelp_configure_rsc_template,`rsc_template`>> for more information.
+
+Attributes containing time values, such as the +interval+ attribute on
+operations, are configured either as a plain number, which is
+interpreted as a time in seconds, or using one of the following
+suffixes:
+
+* +s+, +sec+ - time in seconds (same as no suffix)
+* +ms+, +msec+ - time in milliseconds
+* +us+, +usec+ - time in microseconds
+* +m+, +min+ - time in minutes
+* +h+, +hr+ - time in hours
+
+Usage:
+...............
+primitive <rsc> {[<class>:[<provider>:]]<type>|@<template>}
+  [description=<description>]
+  [params attr_list]
+  [meta attr_list]
+  [utilization attr_list]
+  [operations id_spec]
+    [op op_type [<attribute>=<value>...] ...]
+
+attr_list :: [$id=<id>] [<score>:] [rule...]
+             <attr>=<val> [<attr>=<val>...]] | $id-ref=<id>
+id_spec :: $id=<id> | $id-ref=<id>
+op_type :: start | stop | monitor
+...............
+Example:
+...............
+primitive apcfence stonith:apcsmart \
+  params ttydev=/dev/ttyS0 hostlist="node1 node2" \
+  op start timeout=60s \
+  op monitor interval=30m timeout=60s
+
+primitive www8 apache \
+  params configfile=/etc/apache/www8.conf \
+  operations $id-ref=apache_ops
+
+primitive db0 mysql \
+  params config=/etc/mysql/db0.conf \
+  op monitor interval=60s \
+  op monitor interval=300s OCF_CHECK_LEVEL=10
+
+primitive r0 ocf:linbit:drbd \
+  params drbd_resource=r0 \
+  op monitor role=Master interval=60s \
+  op monitor role=Slave interval=300s
+
+primitive xen0 @vm_scheme1 \
+  params xmfile=/etc/xen/vm/xen0
+
+primitive mySpecialRsc Special \
+  params 3: rule #uname eq node1 interface=eth1 \
+  params 2: rule #uname eq node2 interface=eth2 port=8888 \
+  params 1: interface=eth0 port=9999
+
+...............
+
+[[cmdhelp_configure_monitor,add monitor operation to a primitive]]
+==== `monitor`
+
+Monitor is by far the most common operation. It is possible to
+add it without editing the whole resource. Also, long primitive
+definitions may be a bit uncluttered. In order to make this
+command as concise as possible, less common operation attributes
+are not available. If you need them, then use the `op` part of
+the `primitive` command.
+
+Usage:
+...............
+monitor <rsc>[:<role>] <interval>[:<timeout>]
+...............
+Example:
+...............
+monitor apcfence 60m:60s
+...............
+
+Note that after executing the command, the monitor operation may
+be shown as part of the primitive definition.
+
+[[cmdhelp_configure_group,define a group]]
+==== `group`
+
+The `group` command creates a group of resources. This can be useful
+when resources depend on other resources and require that those
+resources start in order on the same node. A commmon use of resource
+groups is to ensure that a server and a virtual IP are located
+together, and that the virtual IP is started before the server.
+
+Grouped resources are started in the order they appear in the group,
+and stopped in the reverse order. If a resource in the group cannot
+run anywhere, resources following it in the group will not start.
+
+`group` can be passed the "container" meta attribute, to indicate that
+it is to be used to group VM resources monitored using Nagios. The
+resource referred to by the container attribute must be of type
+`ocf:heartbeat:Xen`, `oxf:heartbeat:VirtualDomain` or `ocf:heartbeat:lxc`.
+
+Usage:
+...............
+group <name> <rsc> [<rsc>...]
+  [description=<description>]
+  [meta attr_list]
+  [params attr_list]
+
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+group internal_www disk0 fs0 internal_ip apache \
+  meta target_role=stopped
+
+group vm-and-services vm vm-sshd meta container="vm"
+...............
+
+[[cmdhelp_configure_clone,define a clone]]
+==== `clone`
+
+The `clone` command creates a resource clone. It may contain a
+single primitive resource or one group of resources.
+
+Usage:
+...............
+clone <name> <rsc>
+  [description=<description>]
+  [meta attr_list]
+  [params attr_list]
+
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+clone cl_fence apc_1 \
+  meta clone-node-max=1 globally-unique=false
+...............
+
+[[cmdhelp_configure_ms,define a master-slave resource]]
+==== `ms` (`master`)
+
+The `ms` command creates a master/slave resource type. It may contain a
+single primitive resource or one group of resources.
+
+Usage:
+...............
+ms <name> <rsc>
+  [description=<description>]
+  [meta attr_list]
+  [params attr_list]
+
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+ms disk1 drbd1 \
+  meta notify=true globally-unique=false
+...............
+
+.Note on `id-ref` usage
+****************************
+Instance or meta attributes (`params` and `meta`) may contain
+a reference to another set of attributes. In that case, no other
+attributes are allowed. Since attribute sets' ids, though they do
+exist, are not shown in the `crm`, it is also possible to
+reference an object instead of an attribute set. `crm` will
+automatically replace such a reference with the right id:
+
+...............
+crm(live)configure# primitive a2 www-2 meta $id-ref=a1
+crm(live)configure# show a2
+primitive a2 apache \
+    meta $id-ref=a1-meta_attributes
+    [...]
+...............
+It is advisable to give meaningful names to attribute sets which
+are going to be referenced.
+****************************
+
+[[cmdhelp_configure_rsc_template,define a resource template]]
+==== `rsc_template`
+
+The `rsc_template` command creates a resource template. It may be
+referenced in primitives. It is used to reduce large
+configurations with many similar resources.
+
+Usage:
+...............
+rsc_template <name> [<class>:[<provider>:]]<type>
+  [description=<description>]
+  [params attr_list]
+  [meta attr_list]
+  [utilization attr_list]
+  [operations id_spec]
+    [op op_type [<attribute>=<value>...] ...]
+
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+id_spec :: $id=<id> | $id-ref=<id>
+op_type :: start | stop | monitor
+...............
+Example:
+...............
+rsc_template public_vm Xen \
+  op start timeout=300s \
+  op stop timeout=300s \
+  op monitor interval=30s timeout=60s \
+  op migrate_from timeout=600s \
+  op migrate_to timeout=600s
+primitive xen0 @public_vm \
+  params xmfile=/etc/xen/xen0
+primitive xen1 @public_vm \
+  params xmfile=/etc/xen/xen1
+...............
+
+[[cmdhelp_configure_location,a location preference]]
+==== `location`
+
+`location` defines the preference of nodes for the given
+resource. The location constraints consist of one or more rules
+which specify a score to be awarded if the rule matches.
+
+The resource referenced by the location constraint can be one of the
+following:
+
+* Plain resource reference: +location loc1 webserver 100: node1+
+* Resource set in curly brackets: +location loc1 { virtual-ip webserver } 100: node1+
+* Tag containing resource ids: +location loc1 tag1 100: node1+
+* Resource pattern: +location loc1 /web.*/ 100: node1+
+
+The syntax for resource sets is described in detail for <<cmdhelp_configure_colocation,`colocation`>>.
+
+For more details on how to configure resource sets, see
+<<topics_Features_Resourcesets,`Syntax: Resource sets`>>.
+
+Usage:
+...............
+location <id> rsc [role=<role>] {node_pref|rules}
+
+rsc :: /<rsc-pattern>/
+        | { resource_sets }
+        | <rsc>
+
+node_pref :: <score>: <node>
+
+rules ::
+  rule [id_spec] [$role=<role>] <score>: <expression>
+  [rule [id_spec] [$role=<role>] <score>: <expression> ...]
+
+id_spec :: $id=<id> | $id-ref=<id>
+score :: <number> | <attribute> | [-]inf
+expression :: <simple_exp> [bool_op <simple_exp> ...]
+bool_op :: or | and
+simple_exp :: <attribute> [type:]<binary_op> <value>
+          | <unary_op> <attribute>
+          | date <date_expr>
+type :: string | version | number
+binary_op :: lt | gt | lte | gte | eq | ne
+unary_op :: defined | not_defined
+
+date_expr :: lt <end>
+         | gt <start>
+         | in start=<start> end=<end>
+         | in start=<start> <duration>
+         | spec <date_spec>
+duration|date_spec ::
+         hours=<value>
+         | monthdays=<value>
+         | weekdays=<value>
+         | yearsdays=<value>
+         | months=<value>
+         | weeks=<value>
+         | years=<value>
+         | weekyears=<value>
+         | moon=<value>
+...............
+Examples:
+...............
+location conn_1 internal_www 100: node1
+
+location conn_1 internal_www \
+  rule 50: #uname eq node1 \
+  rule pingd: defined pingd
+
+location conn_2 dummy_float \
+  rule -inf: not_defined pingd or pingd number:lte 0
+...............
+
+[[cmdhelp_configure_colocation,colocate resources]]
+==== `colocation` (`collocation`)
+
+This constraint expresses the placement relation between two
+or more resources. If there are more than two resources, then the
+constraint is called a resource set.
+
+The score is used to indicate the priority of the constraint. A
+positive score indicates that the resources should run on the same
+node. A negative score that they should not run on the same
+node. Values of positive or negative +infinity+ indicate a mandatory
+constraint.
+
+In the two resource form, the cluster will place +<with-rsc>+ first,
+and then decide where to put the +<rsc>+ resource.
+
+Collocation resource sets have an extra attribute (+sequential+)
+to allow for sets of resources which don't depend on each other
+in terms of state. The shell syntax for such sets is to put
+resources in parentheses.
+
+Sets cannot be nested.
+
+The optional +node-attribute+ references an attribute in nodes'
+instance attributes.
+
+For more details on how to configure resource sets, see
+<<topics_Features_Resourcesets,`Syntax: Resource sets`>>.
+
+Usage:
+...............
+colocation <id> <score>: <rsc>[:<role>] <with-rsc>[:<role>]
+  [node-attribute=<node_attr>]
+
+colocation <id> <score>: resource_sets
+  [node-attribute=<node_attr>]
+
+resource_sets :: resource_set [resource_set ...]
+
+resource_set :: ["("|"["] <rsc>[:<role>] [<rsc>[:<role>] ...] \
+                [attributes]  [")"|"]"]
+
+attributes :: [require-all=(true|false)] [sequential=(true|false)]
+
+...............
+Example:
+...............
+colocation never_put_apache_with_dummy -inf: apache dummy
+colocation c1 inf: A ( B C )
+...............
+
+[[cmdhelp_configure_order,order resources]]
+==== `order`
+
+This constraint expresses the order of actions on two resources
+or more resources. If there are more than two resources, then the
+constraint is called a resource set.
+
+Ordered resource sets have an extra attribute to allow for sets
+of resources whose actions may run in parallel. The shell syntax
+for such sets is to put resources in parentheses.
+
+If the subsequent resource can start or promote after any one of the
+resources in a set has done, enclose the set in brackets (+[+ and +]+).
+
+Sets cannot be nested.
+
+Three strings are reserved to specify a kind of order constraint:
++Mandatory+, +Optional+, and +Serialize+. It is preferred to use
+one of these settings instead of score. Previous versions mapped
+scores +0+ and +inf+ to keywords +advisory+ and +mandatory+.
+That is still valid but deprecated.
+
+For more details on how to configure resource sets, see
+<<topics_Features_Resourcesets,`Syntax: Resource sets`>>.
+
+Usage:
+...............
+order <id> [{kind|<score>}:] first then [symmetrical=<bool>]
+
+order <id> [{kind|<score>}:] resource_sets [symmetrical=<bool>]
+
+kind :: Mandatory | Optional | Serialize
+
+first :: <rsc>[:<action>]
+
+then ::  <rsc>[:<action>]
+
+resource_sets :: resource_set [resource_set ...]
+
+resource_set :: ["["|"("] <rsc>[:<action>] [<rsc>[:<action>] ...] \
+                [attributes] ["]"|")"]
+
+attributes :: [require-all=(true|false)] [sequential=(true|false)]
+
+...............
+Example:
+...............
+order o-1 Mandatory: apache:start ip_1
+order o-2 Serialize: A ( B C )
+order o-3 inf: [ A B ] C
+order o-4 first-resource then-resource
+...............
+
+[[cmdhelp_configure_rsc_ticket,resources ticket dependency]]
+==== `rsc_ticket`
+
+This constraint expresses dependency of resources on cluster-wide
+attributes, also known as tickets. Tickets are mainly used in
+geo-clusters, which consist of multiple sites. A ticket may be
+granted to a site, thus allowing resources to run there.
+
+The +loss-policy+ attribute specifies what happens to the
+resource (or resources) if the ticket is revoked. The default is
+either +stop+ or +demote+ depending on whether a resource is
+multi-state.
+
+See also the <<cmdhelp_site_ticket,`site`>> set of commands.
+
+Usage:
+...............
+rsc_ticket <id> <ticket_id>: <rsc>[:<role>] [<rsc>[:<role>] ...]
+  [loss-policy=<loss_policy_action>]
+
+loss_policy_action :: stop | demote | fence | freeze
+...............
+Example:
+...............
+rsc_ticket ticket-A_public-ip ticket-A: public-ip
+rsc_ticket ticket-A_bigdb ticket-A: bigdb loss-policy=fence
+rsc_ticket ticket-B_storage ticket-B: drbd-a:Master drbd-b:Master
+...............
+
+
+[[cmdhelp_configure_property,set a cluster property]]
+==== `property`
+
+Set the cluster (+crm_config+) options.
+
+Usage:
+...............
+property [$id=<set_id>] [rule ...] <option>=<value> [<option>=<value> ...]
+...............
+Example:
+...............
+property stonith-enabled=true
+property rule date spec years=2014 stonith-enabled=false
+...............
+
+[[cmdhelp_configure_rsc_defaults,set resource defaults]]
+==== `rsc_defaults`
+
+Set defaults for the resource meta attributes.
+
+Usage:
+...............
+rsc_defaults [$id=<set_id>] [rule ...] <option>=<value> [<option>=<value> ...]
+...............
+Example:
+...............
+rsc_defaults failure-timeout=3m
+...............
+
+[[cmdhelp_configure_fencing_topology,node fencing order]]
+==== `fencing_topology`
+
+If multiple fencing (stonith) devices are available capable of
+fencing a node, their order may be specified by +fencing_topology+.
+The order is specified per node.
+
+Stonith resources can be separated by +,+ in which case all of
+them need to succeed. If they fail, the next stonith resource (or
+set of resources) is used. In other words, use comma to separate
+resources which all need to succeed and whitespace for serial
+order. It is not allowed to use whitespace around comma.
+
+If the node is left out, the order is used for all nodes.
+That should reduce the configuration size in some stonith setups.
+
+Usage:
+...............
+fencing_topology stonith_resources [stonith_resources ...]
+fencing_topology fencing_order [fencing_order ...]
+
+fencing_order :: <node>: stonith_resources [stonith_resources ...]
+
+stonith_resources :: <rsc>[,<rsc>...]
+...............
+Example:
+...............
+fencing_topology poison-pill power
+fencing_topology \
+    node-a: poison-pill power
+    node-b: ipmi serial
+...............
+
+[[cmdhelp_configure_role,define role access rights]]
+==== `role`
+
+An ACL role is a set of rules which describe access rights to
+CIB. Rules consist of an access right +read+, +write+, or +deny+
+and a specification denoting part of the configuration to which
+the access right applies. The specification can be an XPath or a
+combination of tag and id references. If an attribute is
+appended, then the specification applies only to that attribute
+of the matching element.
+
+There is a number of shortcuts for XPath specifications. The
++meta+, +params+, and +utilization+ shortcuts reference resource
+meta attributes, parameters, and utilization respectively. The
+`location` may be used to specify location constraints most of
+the time to allow resource `move` and `unmove` commands. The
+`property` references cluster properties. The `node` allows
+reading node attributes. +nodeattr+ and +nodeutil+ reference node
+attributes and node capacity (utilization). The `status` shortcut
+references the whole status section of the CIB. Read access to
+status is necessary for various monitoring tools such as
+`crm_mon(8)` (aka `crm status`).
+
+Usage:
+...............
+role <role-id> rule [rule ...]
+
+rule :: acl-right cib-spec [attribute:<attribute>]
+
+acl-right :: read | write | deny
+
+cib-spec :: xpath-spec | tag-ref-spec
+xpath-spec :: xpath:<xpath> | shortcut
+tag-ref-spec :: tag:<tag> | ref:<id> | tag:<tag> ref:<id>
+
+shortcut :: meta:<rsc>[:<attr>]
+        params:<rsc>[:<attr>]
+        utilization:<rsc>
+        location:<rsc>
+        property[:<attr>]
+        node[:<node>]
+        nodeattr[:<attr>]
+        nodeutil[:<node>]
+        status
+...............
+Example:
+...............
+role app1_admin \
+    write meta:app1:target-role \
+    write meta:app1:is-managed \
+    write location:app1 \
+    read ref:app1
+...............
+
+[[cmdhelp_configure_user,define user access rights]]
+==== `user`
+
+Users which normally cannot view or manage cluster configuration
+can be allowed access to parts of the CIB. The access is defined
+by a set of +read+, +write+, and +deny+ rules as in role
+definitions or by referencing roles. The latter is considered
+best practice.
+
+Usage:
+...............
+user <uid> {roles|rules}
+
+roles :: role:<role-ref> [role:<role-ref> ...]
+rules :: rule [rule ...]
+...............
+Example:
+...............
+user joe \
+    role:app1_admin \
+    role:read_all
+...............
+
+[[cmdhelp_configure_acl_target,Define target access rights]]
+==== `acl_target`
+
+Defines an ACL target.
+
+Usage:
+................
+acl_target <tid> [<role> ...]
+................
+Example:
+................
+acl_target joe resource_admin constraint_editor
+................
+
+[[cmdhelp_configure_op_defaults,set resource operations defaults]]
+==== `op_defaults`
+
+Set defaults for the operations meta attributes.
+
+Usage:
+...............
+op_defaults [$id=<set_id>] [rule ...] <option>=<value> [<option>=<value> ...]
+...............
+Example:
+...............
+op_defaults record-pending=true
+...............
+
+[[cmdhelp_configure_tag,Define resource tags]]
+==== `tag`
+
+Define a resource tag. A tag is an id referring to one or more
+resources, without implying any constraints between the tagged
+resources. This can be useful for grouping conceptually related
+resources.
+
+Usage:
+...............
+tag <tag-name>: <rsc> [<rsc> ...]
+...............
+Example:
+...............
+tag web: p-webserver p-vip
+...............
+
+
+[[cmdhelp_configure_schema,set or display current CIB RNG schema]]
+==== `schema`
+
+CIB's content is validated by a RNG schema. Pacemaker supports
+several, depending on version. At least the following schemas are
+accepted by `crmsh`:
+
+* +pacemaker-1.0+
+* +pacemaker-1.1+
+* +pacemaker-1.2+
+* +pacemaker-1.3+
+* +pacemaker-2.0+
+
+Use this command to display or switch to another RNG schema.
+
+Usage:
+...............
+schema [<schema>]
+...............
+Example:
+...............
+schema pacemaker-1.1
+...............
+
+[[cmdhelp_configure_show,display CIB objects]]
+==== `show`
+
+The `show` command displays objects. It may display all objects
+or a set of objects. The user may also choose to see only objects
+which were changed.
+
+Optionally, the XML code may be displayed instead of the CLI
+representation by passing `xml` as the first argument.
+
+To show all objects of a certain type, use the +type:+ prefix.
+To show all objects in a given tag, use the +tag:+ prefix.
+
+Usage:
+...............
+show [xml] [<id> ...]
+show [xml] changed
+...............
+
+Example:
+...............
+show webapp
+show type:primitive
+show xml type:node
+show tag:web
+...............
+
+[[cmdhelp_configure_edit,edit CIB objects]]
+==== `edit`
+
+This command invokes the editor with the object description. As
+with the `show` command, the user may choose to edit all objects
+or a set of objects.
+
+If the user insists, he or she may edit the XML edition of the
+object. If you do that, don't modify any id attributes.
+
+Usage:
+...............
+edit [xml] [<id> ...]
+edit [xml] changed
+...............
+
+.Note on renaming element ids
+****************************
+The edit command sometimes cannot properly handle modifying
+element ids. In particular for elements which belong to group or
+ms resources. Group and ms resources themselves also cannot be
+renamed. Please use the `rename` command instead.
+****************************
+
+[[cmdhelp_configure_filter,filter CIB objects]]
+==== `filter`
+
+This command filters the given CIB elements through an external
+program. The program should accept input on `stdin` and send
+output to `stdout` (the standard UNIX filter conventions). As
+with the `show` command, the user may choose to filter all or
+just a subset of elements.
+
+It is possible to filter the XML representation of objects, but
+probably not as useful as the configuration language. The
+presentation is somewhat different from what would be displayed
+by the `show` command---each element is shown on a single line,
+i.e. there are no backslashes and no other embelishments.
+
+Don't forget to put quotes around the filter if it contains
+spaces.
+
+Usage:
+...............
+filter <prog> [xml] [<id> ...]
+filter <prog> [xml] changed
+...............
+Examples:
+...............
+filter "sed '/^primitive/s/target-role=[^ ]*//'"
+# crm configure filter "sed '/^primitive/s/target-role=[^ ]*//'"
+crm configure <<END
+  filter "sed '/threshold=\"1\"/s/=\"1\"/=\"0\"/g'"
+END
+...............
+
+.Note on quotation marks
+**************************
+Filter commands which feature a blend of quotation marks can be
+difficult to get right, especially when used directly from bash, since
+bash does its own quotation parsing. In these cases, it can be easier
+to supply the filter command as standard input. See the last example
+above.
+**************************
+
+[[cmdhelp_configure_delete,delete CIB objects]]
+==== `delete`
+
+Delete one or more objects. If an object to be deleted belongs to
+a container object, such as a group, and it is the only resource
+in that container, then the container is deleted as well. Any
+related constraints are removed as well.
+
+If the object is a started resource, it will not be deleted unless the
++--force+ flag is passed to the command, or the +force+ option is set.
+
+Usage:
+...............
+delete [--force] <id> [<id>...]
+...............
+
+[[cmdhelp_configure_default-timeouts,set timeouts for operations to minimums from the meta-data]]
+==== `default-timeouts`
+
+This command takes the timeouts from the actions section of the
+resource agent meta-data and sets them for the operations of the
+primitive.
+
+Usage:
+...............
+default-timeouts <id> [<id>...]
+...............
+
+.Note on `default-timeouts`
+****************************
+You may be happy using this, but your applications may not. And
+it will tell you so at the worst possible moment. You have been
+warned.
+****************************
+
+[[cmdhelp_configure_rename,rename a CIB object]]
+==== `rename`
+
+Rename an object. It is recommended to use this command to rename
+a resource, because it will take care of updating all related
+constraints and a parent resource. Changing ids with the edit
+command won't have the same effect.
+
+If you want to rename a resource, it must be in the stopped state.
+
+Usage:
+...............
+rename <old_id> <new_id>
+...............
+
+[[cmdhelp_configure_modgroup,modify group]]
+==== `modgroup`
+
+Add or remove primitives in a group. The `add` subcommand appends
+the new group member by default. Should it go elsewhere, there
+are `after` and `before` clauses.
+
+Usage:
+...............
+modgroup <id> add <id> [after <id>|before <id>]
+modgroup <id> remove <id>
+...............
+Examples:
+...............
+modgroup share1 add storage2 before share1-fs
+...............
+
+[[cmdhelp_configure_refresh,refresh from CIB]]
+==== `refresh`
+
+Refresh the internal structures from the CIB. All changes made
+during this session are lost.
+
+Usage:
+...............
+refresh
+...............
+
+[[cmdhelp_configure_erase,erase the CIB]]
+==== `erase`
+
+The `erase` clears all configuration. Apart from nodes. To remove
+nodes, you have to specify an additional keyword `nodes`.
+
+Note that removing nodes from the live cluster may have some
+strange/interesting/unwelcome effects.
+
+Usage:
+...............
+erase [nodes]
+...............
+
+[[cmdhelp_configure_ptest,show cluster actions if changes were committed]]
+==== `ptest` (`simulate`)
+
+Show PE (Policy Engine) motions using `ptest(8)` or
+`crm_simulate(8)`.
+
+A CIB is constructed using the current user edited configuration
+and the status from the running CIB. The resulting CIB is run
+through `ptest` (or `crm_simulate`) to show changes which would
+happen if the configuration is committed.
+
+The status section may be loaded from another source and modified
+using the <<cmdhelp_cibstatus,`cibstatus`>> level commands. In that case, the
+`ptest` command will issue a message informing the user that the
+Policy Engine graph is not calculated based on the current status
+section and therefore won't show what would happen to the
+running but some imaginary cluster.
+
+If you have graphviz installed and X11 session, `dotty(1)` is run
+to display the changes graphically.
+
+Add a string of +v+ characters to increase verbosity. `ptest`
+can also show allocation scores. +utilization+ turns on
+information about the remaining capacity of nodes. With the
++actions+ option, `ptest` will print all resource actions.
+
+The `ptest` program has been replaced by `crm_simulate` in newer
+Pacemaker versions. In some installations both could be
+installed. Use `simulate` to enfore using `crm_simulate`.
+
+Usage:
+...............
+ptest [nograph] [v...] [scores] [actions] [utilization]
+...............
+Examples:
+...............
+ptest scores
+ptest vvvvv
+simulate actions
+...............
+
+[[cmdhelp_configure_rsctest,test resources as currently configured]]
+==== `rsctest`
+
+Test resources with current resource configuration. If no nodes
+are specified, tests are run on all known nodes.
+
+The order of resources is significant: it is assumed that later
+resources depend on earlier ones.
+
+If a resource is multi-state, it is assumed that the role on
+which later resources depend is master.
+
+Tests are run sequentially to prevent running the same resource
+on two or more nodes. Tests are carried out only if none of the
+specified nodes currently run any of the specified resources.
+However, it won't verify whether resources run on the other
+nodes.
+
+Superuser privileges are obviously required: either run this as
+root or setup the `sudoers` file appropriately.
+
+Note that resource testing may take some time.
+
+Usage:
+...............
+rsctest <rsc_id> [<rsc_id> ...] [<node_id> ...]
+...............
+Examples:
+...............
+rsctest my_ip websvc
+rsctest websvc nodeB
+...............
+
+[[cmdhelp_configure_cib,CIB shadow management]]
+=== `cib` (shadow CIBs)
+
+This level is for management of shadow CIBs. It is available at
+the `configure` level to enable saving intermediate changes to a
+shadow CIB instead of to the live cluster. This short excerpt
+shows how:
+...............
+crm(live)configure# cib new test-2
+INFO: test-2 shadow CIB created
+crm(test-2)configure# commit
+...............
+Note how the current CIB in the prompt changed from +live+ to
++test-2+ after issuing the `cib new` command. See also the
+<<cmdhelp_cib,CIB shadow management>> for more information.
+
+[[cmdhelp_configure_cibstatus,CIB status management and editing]]
+==== `cibstatus`
+
+Enter edit and manage the CIB status section level. See the
+<<cmdhelp_cibstatus,CIB status management section>>.
+
+[[cmdhelp_configure_template,edit and import a configuration from a template]]
+==== `template`
+
+The specified template is loaded into the editor. It's up to the
+user to make a good CRM configuration out of it. See also the
+<<cmdhelp_template,template section>>.
+
+Usage:
+...............
+template [xml] url
+...............
+Example:
+...............
+template two-apaches.txt
+...............
+
+[[cmdhelp_configure_commit,commit the changes to the CIB]]
+==== `commit`
+
+Commit the current configuration to the CIB in use. As noted
+elsewhere, commands in a configure session don't have immediate
+effect on the CIB. All changes are applied at one point in time,
+either using `commit` or when the user leaves the configure
+level. In case the CIB in use changed in the meantime, presumably
+by somebody else, the crm shell will refuse to apply the changes.
+
+If you know that it's fine to still apply them, add +force+ to the
+command line.
+
+Usage:
+...............
+commit [force]
+...............
+
+[[cmdhelp_configure_verify,verify the CIB with crm_verify]]
+==== `verify`
+
+Verify the contents of the CIB which would be committed.
+
+Usage:
+...............
+verify
+...............
+
+[[cmdhelp_configure_upgrade,upgrade the CIB to version 1.0]]
+==== `upgrade`
+
+If you get the `CIB not supported` error, which typically means
+that the current CIB version is coming from the older release,
+you may try to upgrade it to the latest revision. The command
+to perform the upgrade is:
+...............
+# cibadmin --upgrade --force
+...............
+
+If we don't recognize the current CIB as the old one, but you're
+sure that it is, you may force the command.
+
+Usage:
+...............
+upgrade [force]
+...............
+
+[[cmdhelp_configure_save,save the CIB to a file]]
+==== `save`
+
+Save the current configuration to a file. Optionally, as XML. Use
++-+ instead of file name to write the output to `stdout`.
+
+Usage:
+...............
+save [xml] <file>
+...............
+Example:
+...............
+save myfirstcib.txt
+...............
+
+[[cmdhelp_configure_load,import the CIB from a file]]
+==== `load`
+
+Load a part of configuration (or all of it) from a local file or
+a network URL. The +replace+ method replaces the current
+configuration with the one from the source. The +update+ tries to
+import the contents into the current configuration.
+The file may be a CLI file or an XML file.
+
+Usage:
+...............
+load [xml] <method> URL
+
+method :: replace | update
+...............
+Example:
+...............
+load xml update myfirstcib.xml
+load xml replace http://storage.big.com/cibs/bigcib.xml
+...............
+
+[[cmdhelp_configure_graph,generate a directed graph]]
+==== `graph`
+
+Create a graphviz graphical layout from the current cluster
+configuration.
+
+Currently, only `dot` (directed graph) is supported. It is
+essentially a visualization of resource ordering.
+
+The graph may be saved to a file which can be used as source for
+various graphviz tools (by default it is displayed in the user's
+X11 session). Optionally, by specifying the format, one can also
+produce an image instead.
+
+For more or different graphviz attributes, it is possible to save
+the default set of attributes to an ini file. If this file exists
+it will always override the builtin settings. The +exportsettings+
+subcommand also prints the location of the ini file.
+
+Usage:
+...............
+graph [<gtype> [<file> [<img_format>]]]
+graph exportsettings
+
+gtype :: dot
+img_format :: `dot` output format (see the +-T+ option)
+...............
+Example:
+...............
+graph dot
+graph dot clu1.conf.dot
+graph dot clu1.conf.svg svg
+...............
+
+[[cmdhelp_configure_xml,raw xml]]
+==== `xml`
+
+Even though we promissed no xml, it may happen, but hopefully
+very very seldom, that an element from the CIB cannot be rendered
+in the configuration language. In that case, the element will be
+shown as raw xml, prefixed by this command. That element can then
+be edited like any other. If the shell finds out that after the
+change it can digest it, then it is going to be converted into
+the normal configuration language. Otherwise, there is no need to
+use `xml` for configuration.
+
+Usage:
+...............
+xml <xml>
+...............
+
+[[cmdhelp_template,edit and import a configuration from a template]]
+=== `template`
+
+User may be assisted in the cluster configuration by templates
+prepared in advance. Templates consist of a typical ready
+configuration which may be edited to suit particular user needs.
+
+This command enters a template level where additional commands
+for configuration/template management are available.
+
+[[cmdhelp_template_new,create a new configuration from templates]]
+==== `new`
+
+Create a new configuration from one or more templates. Note that
+configurations and templates are kept in different places, so it
+is possible to have a configuration name equal a template name.
+
+If you already know which parameters are required, you can set
+them directly on the command line.
+
+The parameter name +id+ is set by default to the name of the
+configuration.
+
+Usage:
+...............
+new <config> <template> [<template> ...] [params name=value ...]
+...............
+
+Example:
+...............
+new vip virtual-ip
+new bigfs ocfs2 params device=/dev/sdx8 directory=/bigfs
+...............
+
+[[cmdhelp_template_load,load a configuration]]
+==== `load`
+
+Load an existing configuration. Further `edit`, `show`, and
+`apply` commands will refer to this configuration.
+
+Usage:
+...............
+load <config>
+...............
+
+[[cmdhelp_template_edit,edit a configuration]]
+==== `edit`
+
+Edit current or given configuration using your favourite editor.
+
+Usage:
+...............
+edit [<config>]
+...............
+
+[[cmdhelp_template_delete,delete a configuration]]
+==== `delete`
+
+Remove a configuration. The loaded (active) configuration may be
+removed by force.
+
+Usage:
+...............
+delete <config> [force]
+...............
+
+[[cmdhelp_template_list,list configurations/templates]]
+==== `list`
+
+List existing configurations or templates.
+
+Usage:
+...............
+list [templates]
+...............
+
+[[cmdhelp_template_apply,process and apply the current configuration to the current CIB]]
+==== `apply`
+
+Copy the current or given configuration to the current CIB. By
+default, the CIB is replaced, unless the method is set to
+"update".
+
+Usage:
+...............
+apply [<method>] [<config>]
+
+method :: replace | update
+...............
+
+[[cmdhelp_template_show,show the processed configuration]]
+==== `show`
+
+Process the current or given configuration and display the result.
+
+Usage:
+...............
+show [<config>]
+...............
+
+[[cmdhelp_cibstatus,CIB status management and editing]]
+=== `cibstatus`
+
+The `status` section of the CIB keeps the current status of nodes
+and resources. It is modified _only_ on events, i.e. when some
+resource operation is run or node status changes. For obvious
+reasons, the CRM has no user interface with which it is possible
+to affect the status section. From the user's point of view, the
+status section is essentially a read-only part of the CIB. The
+current status is never even written to disk, though it is
+available in the PE (Policy Engine) input files which represent
+the history of cluster motions. The current status may be read
+using the +cibadmin -Q+ command.
+
+It may sometimes be of interest to see how status changes would
+affect the Policy Engine. The set of `cibstatus` level commands
+allow the user to load status sections from various sources and
+then insert or modify resource operations or change nodes' state.
+
+The effect of those changes may then be observed by running the
+<<cmdhelp_configure_ptest,`ptest`>> command at the `configure` level
+or `simulate` and `run` commands at this level. The `ptest`
+runs with the user edited CIB whereas the latter two commands
+run with the CIB which was loaded along with the status section.
+
+The `simulate` and `run` commands as well as all status
+modification commands are implemented using `crm_simulate(8)`.
+
+[[cmdhelp_cibstatus_load,load the CIB status section]]
+==== `load`
+
+Load a status section from a file, a shadow CIB, or the running
+cluster. By default, the current (+live+) status section is
+modified. Note that if the +live+ status section is modified it
+is not going to be updated if the cluster status changes, because
+that would overwrite the user changes. To make `crm` drop changes
+and resume use of the running cluster status, run +load live+.
+
+All CIB shadow configurations contain the status section which is
+a snapshot of the status section taken at the time the shadow was
+created. Obviously, this status section doesn't have much to do
+with the running cluster status, unless the shadow CIB has just
+been created. Therefore, the `ptest` command by default uses the
+running cluster status section.
+
+Usage:
+...............
+load {<file>|shadow:<cib>|live}
+...............
+Example:
+...............
+load bug-12299.xml
+load shadow:test1
+...............
+
+[[cmdhelp_cibstatus_save,save the CIB status section]]
+==== `save`
+
+The current internal status section with whatever modifications
+were performed can be saved to a file or shadow CIB.
+
+If the file exists and contains a complete CIB, only the status
+section is going to be replaced and the rest of the CIB will
+remain intact. Otherwise, the current user edited configuration
+is saved along with the status section.
+
+Note that all modifications are saved in the source file as soon
+as they are run.
+
+Usage:
+...............
+save [<file>|shadow:<cib>]
+...............
+Example:
+...............
+save bug-12299.xml
+...............
+
+[[cmdhelp_cibstatus_origin,display origin of the CIB status section]]
+==== `origin`
+
+Show the origin of the status section currently in use. This
+essentially shows the latest `load` argument.
+
+Usage:
+...............
+origin
+...............
+
+[[cmdhelp_cibstatus_show,show CIB status section]]
+==== `show`
+
+Show the current status section in the XML format. Brace yourself
+for some unreadable output. Add +changed+ option to get a human
+readable output of all changes.
+
+Usage:
+...............
+show [changed]
+...............
+
+[[cmdhelp_cibstatus_node,change node status]]
+==== `node`
+
+Change the node status. It is possible to throw a node out of
+the cluster, make it a member, or set its state to unclean.
+
++online+:: Set the +node_state+ `crmd` attribute to +online+
+and the +expected+ and +join+ attributes to +member+. The effect
+is that the node becomes a cluster member.
+
++offline+:: Set the +node_state+ `crmd` attribute to +offline+
+and the +expected+ attribute to empty. This makes the node
+cleanly removed from the cluster.
+
++unclean+:: Set the +node_state+ `crmd` attribute to +offline+
+and the +expected+ attribute to +member+. In this case the node
+has unexpectedly disappeared.
+
+Usage:
+...............
+node <node> {online|offline|unclean}
+...............
+Example:
+...............
+node xen-b unclean
+...............
+
+[[cmdhelp_cibstatus_op,edit outcome of a resource operation]]
+==== `op`
+
+Edit the outcome of a resource operation. This way you can
+tell CRM that it ran an operation and that the resource agent
+returned certain exit code. It is also possible to change the
+operation's status. In case the operation status is set to
+something other than +done+, the exit code is effectively
+ignored.
+
+Usage:
+...............
+op <operation> <resource> <exit_code> [<op_status>] [<node>]
+
+operation :: probe | monitor[:<n>] | start | stop |
+   promote | demote | notify | migrate_to | migrate_from
+exit_code :: <rc> | success | generic | args |
+   unimplemented | perm | installed | configured | not_running |
+   master | failed_master
+op_status :: pending | done | cancelled | timeout | notsupported | error
+
+n :: the monitor interval in seconds; if omitted, the first
+   recurring operation is referenced
+rc :: numeric exit code in range 0..9
+...............
+Example:
+...............
+op start d1 xen-b generic
+op start d1 xen-b 1
+op monitor d1 xen-b not_running
+op stop d1 xen-b 0 timeout
+...............
+
+[[cmdhelp_cibstatus_quorum,set the quorum]]
+==== `quorum`
+
+Set the quorum value.
+
+Usage:
+...............
+quorum <bool>
+...............
+Example:
+...............
+quorum false
+...............
+
+[[cmdhelp_cibstatus_ticket,manage tickets]]
+==== `ticket`
+
+Modify the ticket status. Tickets can be granted and revoked.
+Granted tickets could be activated or put in standby.
+
+Usage:
+...............
+ticket <ticket> {grant|revoke|activate|standby}
+...............
+Example:
+...............
+ticket ticketA grant
+...............
+
+[[cmdhelp_cibstatus_run,run policy engine]]
+==== `run`
+
+Run the policy engine with the edited status section.
+
+Add a string of +v+ characters to increase verbosity. Specify
++scores+ to see allocation scores also. +utilization+ turns on
+information about the remaining capacity of nodes.
+
+If you have graphviz installed and X11 session, `dotty(1)` is run
+to display the changes graphically.
+
+Usage:
+...............
+run [nograph] [v...] [scores] [utilization]
+...............
+Example:
+...............
+run
+...............
+
+[[cmdhelp_cibstatus_simulate,simulate cluster transition]]
+==== `simulate`
+
+Run the policy engine with the edited status section and simulate
+the transition.
+
+Add a string of +v+ characters to increase verbosity. Specify
++scores+ to see allocation scores also. +utilization+ turns on
+information about the remaining capacity of nodes.
+
+If you have graphviz installed and X11 session, `dotty(1)` is run
+to display the changes graphically.
+
+Usage:
+...............
+simulate [nograph] [v...] [scores] [utilization]
+...............
+Example:
+...............
+simulate
+...............
+
+[[cmdhelp_assist,Configuration assistant]]
+=== `assist`
+
+The `assist` sublevel is a collection of helper
+commands that create or modify resources and
+constraints, to simplify the creation of certain
+configurations.
+
+For more information on individual commands, see
+the help text for those commands.
+
+[[cmdhelp_assist_weak-bond,Create a weak bond between resources]]
+==== `weak-bond`
+
+A colocation between a group of resources says that the resources
+should be located together, but it also means that those resources are
+dependent on each other. If one of the resources fails, the others
+will be restarted.
+
+If this is not desired, it is possible to circumvent: By placing the
+resources in a non-sequential set and colocating the set with a dummy
+resource which is not monitored, the resources will be placed together
+but will have no further dependency on each other.
+
+This command creates both the constraint and the dummy resource needed
+for such a colocation.
+
+Usage:
+........
+weak-bond resource-1 resource-2
+........
+
+[[cmdhelp_assist_template,Create template for primitives]]
+==== `template`
+
+This command takes a list of primitives as argument, and creates a new
+`rsc_template` for these primitives. It can only do this if the
+primitives do not already share a template and are of the same type.
+
+Usage:
+........
+template primitive-1 primitive-2 primitive-3
+........
+
+[[cmdhelp_history,cluster history]]
+=== `history`
+
+Examining Pacemaker's history is a particularly involved task.
+The number of subsystems to be considered, the complexity of the
+configuration, and the set of various information sources, most
+of which are not exactly human readable, keep analyzing resource
+or node problems accessible to only the most knowledgeable. Or,
+depending on the point of view, to the most persistent. The
+following set of commands has been devised in hope to make
+cluster history more accessible.
+
+Of course, looking at _all_ history could be time consuming
+regardless of how good tools at hand are. Therefore, one should
+first say which period he or she wants to analyze. If not
+otherwise specified, the last hour is considered. Logs and other
+relevant information is collected using `hb_report`. Since this
+process takes some time and we always need fresh logs,
+information is refreshed in a much faster way using `pssh(1)`. If
++python-pssh+ is not found on the system, examining live cluster
+is still possible though not as comfortable.
+
+Apart from examining live cluster, events may be retrieved from a
+report generated by `hb_report` (see also the +-H+ option). In
+that case we assume that the period stretching the whole report
+needs to be investigated. Of course, it is still possible to
+further reduce the time range.
+
+If you think you may have found a bug or just need clarification
+from developers or your support, the `session pack` command can
+help create a report.
+
+Example:
+...............
+crm(live)history# timeframe "Jul 18 12:00" "Jul 18 12:30"
+crm(live)history# session save strange_restart
+crm(live)history# session pack
+Report saved in .../strange_restart.tar.bz2
+crm(live)history#
+...............
+
+In order to reduce report size and allow developers to
+concentrate on the issue, you should beforehand limit the time
+frame. Giving a meaningful session name helps too.
+
+[[cmdhelp_history_info,Cluster information summary]]
+==== `info`
+
+The `info` command provides a summary of the information source, which
+can be either a live cluster snapshot or a previously generated
+report.
+
+Usage:
+...............
+info
+...............
+Example:
+...............
+info
+...............
+
+[[cmdhelp_history_latest,show latest news from the cluster]]
+==== `latest`
+
+The `latest` command shows a bit of recent history, more
+precisely whatever happened since the last cluster change (the
+latest transition). If the transition is running, the shell will
+first wait until it finishes.
+
+Usage:
+...............
+latest
+...............
+Example:
+...............
+latest
+...............
+
+[[cmdhelp_history_limit,limit timeframe to be examined]]
+==== `limit` (`timeframe`)
+
+All history commands look at events within certain period. It
+defaults to the last hour for the live cluster source. There is
+no limit for the `hb_report` source. Use this command to set the
+timeframe.
+
+The time period is parsed by the dateutil python module. It
+covers wide range of date formats. For instance:
+
+- 3:00      (today at 3am)
+- 15:00     (today at 3pm)
+- 2010/9/1 2pm  (September 1st 2010 at 2pm)
+
+We won't bother to give definition of the time specification in
+usage below. Either use common sense or read the
+http://labix.org/python-dateutil[dateutil] documentation.
+
+If dateutil is not available, then the time is parsed using
+strptime and only the kind as printed by `date(1)` is allowed:
+
+- Tue Sep 15 20:46:27 CEST 2010
+
+Usage:
+...............
+limit [<from_time> [<to_time>]]
+...............
+Examples:
+...............
+limit 10:15
+limit 15h22m 16h
+limit "Sun 5 20:46" "Sun 5 22:00"
+...............
+
+[[cmdhelp_history_source,set source to be examined]]
+==== `source`
+
+Events to be examined can come from the current cluster or from a
+`hb_report` report. This command sets the source. `source live`
+sets source to the running cluster and system logs. If no source
+is specified, the current source information is printed.
+
+In case a report source is specified as a file reference, the file
+is going to be unpacked in place where it resides. This directory
+is not removed on exit.
+
+Usage:
+...............
+source [<dir>|<file>|live]
+...............
+Examples:
+...............
+source live
+source /tmp/customer_case_22.tar.bz2
+source /tmp/customer_case_22
+source
+...............
+
+[[cmdhelp_history_refresh,refresh live report]]
+==== `refresh`
+
+This command makes sense only for the +live+ source and makes
+`crm` collect the latest logs and other relevant information from
+the logs. If you want to make a completely new report, specify
++force+.
+
+Usage:
+...............
+refresh [force]
+...............
+
+[[cmdhelp_history_detail,set the level of detail shown]]
+==== `detail`
+
+How much detail to show from the logs.
+
+Usage:
+...............
+detail <detail_level>
+
+detail_level :: small integer (defaults to 0)
+...............
+Example:
+...............
+detail 1
+...............
+
+[[cmdhelp_history_setnodes,set the list of cluster nodes]]
+==== `setnodes`
+
+In case the host this program runs on is not part of the cluster,
+it is necessary to set the list of nodes.
+
+Usage:
+...............
+setnodes node <node> [<node> ...]
+...............
+Example:
+...............
+setnodes node_a node_b
+...............
+
+[[cmdhelp_history_resource,resource events]]
+==== `resource`
+
+Show actions and any failures that happened on all specified
+resources on all nodes. Normally, one gives resource names as
+arguments, but it is also possible to use extended regular
+expressions. Note that neither groups nor clones or master/slave
+names are ever logged. The resource command is going to expand
+all of these appropriately, so that clone instances or resources
+which are part of a group are shown.
+
+Usage:
+...............
+resource <rsc> [<rsc> ...]
+...............
+Example:
+...............
+resource bigdb public_ip
+resource my_.*_db2
+resource ping_clone
+...............
+
+[[cmdhelp_history_node,node events]]
+==== `node`
+
+Show important events that happened on a node. Important events
+are node lost and join, standby and online, and fence. Use either
+node names or extended regular expressions.
+
+Usage:
+...............
+node <node> [<node> ...]
+...............
+Example:
+...............
+node node1
+...............
+
+[[cmdhelp_history_log,log content]]
+==== `log`
+
+Show messages logged on one or more nodes. Leaving out a node
+name produces combined logs of all nodes. Messages are sorted by
+time and, if the terminal emulations supports it, displayed in
+different colours depending on the node to allow for easier
+reading.
+
+The sorting key is the timestamp as written by syslog which
+normally has the maximum resolution of one second. Obviously,
+messages generated by events which share the same timestamp may
+not be sorted in the same way as they happened. Such close events
+may actually happen fairly often.
+
+Usage:
+...............
+log [<node> [<node> ...] ]
+...............
+Example:
+...............
+log node-a
+...............
+
+[[cmdhelp_history_exclude,exclude log messages]]
+==== `exclude`
+
+If a log is infested with irrelevant messages, those messages may
+be excluded by specifying a regular expression. The regular
+expressions used are Python extended. This command is additive.
+To drop all regular expressions, use +exclude clear+. Run
+`exclude` only to see the current list of regular expressions.
+Excludes are saved along with the history sessions.
+
+Usage:
+...............
+exclude [<regex>|clear]
+...............
+Example:
+...............
+exclude kernel.*ocfs2
+...............
+
+[[cmdhelp_history_peinputs,list or get PE input files]]
+==== `peinputs`
+
+Every event in the cluster results in generating one or more
+Policy Engine (PE) files. These files describe future motions of
+resources. The files are listed as full paths in the current
+report directory. Add +v+ to also see the creation time stamps.
+
+Usage:
+...............
+peinputs [{<range>|<number>} ...] [v]
+
+range :: <n1>:<n2>
+...............
+Example:
+...............
+peinputs
+peinputs 440:444 446
+peinputs v
+...............
+
+[[cmdhelp_history_transition,show transition]]
+==== `transition`
+
+This command will print actions planned by the PE and run
+graphviz (`dotty`) to display a graphical representation of the
+transition. Of course, for the latter an X11 session is required.
+This command invokes `ptest(8)` in background.
+
+The +showdot+ subcommand runs graphviz (`dotty`) to display a
+graphical representation of the +.dot+ file which has been
+included in the report. Essentially, it shows the calculation
+produced by `pengine` which is installed on the node where the
+report was produced. In optimal case this output should not
+differ from the one produced by the locally installed `pengine`.
+
+The `log` subcommand shows the full log for the duration of the
+transition.
+
+A transition can also be saved to a CIB shadow for further
+analysis or use with `cib` or `configure` commands (use the
+`save` subcommand). The shadow file name defaults to the name of
+the PE input file.
+
+If the PE input file number is not provided, it defaults to the
+last one, i.e. the last transition. The last transition can also
+be referenced with number 0. If the number is negative, then the
+corresponding transition relative to the last one is chosen.
+
+If there are warning and error PE input files or different nodes
+were the DC in the observed timeframe, it may happen that PE
+input file numbers collide. In that case provide some unique part
+of the path to the file.
+
+After the `ptest` output, logs about events that happened during
+the transition are printed.
+
+Usage:
+...............
+transition [<number>|<index>|<file>] [nograph] [v...] [scores] [actions] [utilization]
+transition showdot [<number>|<index>|<file>]
+transition log [<number>|<index>|<file>]
+transition save [<number>|<index>|<file> [name]]
+...............
+Examples:
+...............
+transition
+transition 444
+transition -1
+transition pe-error-3.bz2
+transition node-a/pengine/pe-input-2.bz2
+transition showdot 444
+transition log
+transition save 0 enigma-22
+...............
+
+[[cmdhelp_history_show,show status or configuration of the PE input file]]
+==== `show`
+
+Every transition is saved as a PE file. Use this command to
+render that PE file either as configuration or status. The
+configuration output is the same as `crm configure show`.
+
+Usage:
+...............
+show <pe> [status]
+
+pe :: <number>|<index>|<file>|live
+...............
+Examples:
+...............
+show 2066
+show pe-input-2080.bz2 status
+...............
+
+[[cmdhelp_history_graph,generate a directed graph from the PE file]]
+==== `graph`
+
+Create a graphviz graphical layout from the PE file (the
+transition). Every transition contains the cluster configuration
+which was active at the time. See also <<cmdhelp_configure_graph,generate a directed graph
+from configuration>>.
+
+Usage:
+...............
+graph <pe> [<gtype> [<file> [<img_format>]]]
+
+gtype :: dot
+img_format :: `dot` output format (see the +-T+ option)
+...............
+Example:
+...............
+graph -1
+graph 322 dot clu1.conf.dot
+graph 322 dot clu1.conf.svg svg
+...............
+
+[[cmdhelp_history_diff,cluster states/transitions difference]]
+==== `diff`
+
+A transition represents a change in cluster configuration or
+state. Use `diff` to see what has changed between two
+transitions.
+
+If you want to specify the current cluster configuration and
+status, use the string +live+.
+
+Normally, the first transition specified should be the one which
+is older, but we are not going to enforce that.
+
+Note that a single configuration update may result in more than
+one transition.
+
+Usage:
+...............
+diff <pe> <pe> [status] [html]
+
+pe :: <number>|<index>|<file>|live
+...............
+Examples:
+...............
+diff 2066 2067
+diff pe-input-2080.bz2 live status
+...............
+
+[[cmdhelp_history_wdiff,cluster states/transitions difference]]
+==== `wdiff`
+
+A transition represents a change in cluster configuration or
+state. Use `wdiff` to see what has changed between two
+transitions as word differences on a line-by-line basis.
+
+If you want to specify the current cluster configuration and
+status, use the string +live+.
+
+Normally, the first transition specified should be the one which
+is older, but we are not going to enforce that.
+
+Note that a single configuration update may result in more than
+one transition.
+
+Usage:
+...............
+wdiff <pe> <pe> [status]
+
+pe :: <number>|<index>|<file>|live
+...............
+Examples:
+...............
+wdiff 2066 2067
+wdiff pe-input-2080.bz2 live status
+...............
+
+[[cmdhelp_history_session,manage history sessions]]
+==== `session`
+
+Sometimes you may want to get back to examining a particular
+history period or bug report. In order to make that easier, the
+current settings can be saved and later retrieved.
+
+If the current history being examined is coming from a live
+cluster the logs, PE inputs, and other files are saved too,
+because they may disappear from nodes. For the existing reports
+coming from `hb_report`, only the directory location is saved
+(not to waste space).
+
+A history session may also be packed into a tarball which can
+then be sent to support.
+
+Leave out subcommand to see the current session.
+
+Usage:
+...............
+session [{save|load|delete} <name> | pack [<name>] | update | list]
+...............
+Examples:
+...............
+session save bnc966622
+session load rsclost-2
+session list
+...............
+
+[[cmdhelp_root_report,Create cluster status report]]
+=== `report`
+
+Interface to a tool for creating a cluster report. A report is an
+archive containing log files, configuration files, system information
+and other relevant data for a given time period. This is a useful tool
+for collecting data to attach to bug reports, or for detecting the
+root cause of errors resulting in resource failover, for example.
+
+See `crmsh_hb_report(8)` for more details on arguments,
+or call `crm report -h`
+
+Usage:
+...............
+report -f {time|"cts:"testnum} [-t time] [-u user] [-l file]
+       [-n nodes] [-E files] [-p patt] [-L patt] [-e prog]
+       [-MSDZAVsvhd] [dest]
+...............
+
+Examples:
+...............
+report -f 2pm report_1
+report -f "2007/9/5 12:30" -t "2007/9/5 14:00" report_2
+report -f 1:00 -t 3:00 -l /var/log/cluster/ha-debug report_3
+report -f "09sep07 2:00" -u hbadmin report_4
+report -f 18:00 -p "usern.*" -p "admin.*" report_5
+report -f cts:133 ctstest_133
+...............
+
+=== `end` (`cd`, `up`)
+
+The `end` command ends the current level and the user moves to
+the parent level. This command is available everywhere.
+
+Usage:
+...............
+end
+...............
+
+=== `help`
+
+The `help` command prints help for the current level or for the
+specified topic (command). This command is available everywhere.
+
+Usage:
+...............
+help [<topic>]
+...............
+
+=== `quit` (`exit`, `bye`)
+
+Leave the program.
+
+BUGS
+----
+Even though all sensible configurations (and most of those that
+are not) are going to be supported by the crm shell, I suspect
+that it may still happen that certain XML constructs may confuse
+the tool. When that happens, please file a bug report.
+
+The crm shell will not try to update the objects it does not
+understand. Of course, it is always possible to edit such objects
+in the XML format.
+
+AUTHORS
+-------
+Dejan Muhamedagic, <dejan at suse.de>
+Kristoffer Gronlund <kgronlund at suse.com>
+and many OTHERS
+
+SEE ALSO
+--------
+crm_resource(8), crm_attribute(8), crm_mon(8), cib_shadow(8),
+ptest(8), dotty(1), crm_simulate(8), cibadmin(8)
+
+
+COPYING
+-------
+Copyright \(C) 2008-2013 Dejan Muhamedagic.
+Copyright \(C) 2013 Kristoffer Gronlund.
+
+Free use of this software is granted under the terms of the GNU General Public License (GPL).
+
+//////////////////////
+ vim:ts=4:sw=4:expandtab:
+//////////////////////
diff --git a/doc/crmsh_hb_report.8.txt b/doc/crmsh_hb_report.8.txt
new file mode 100644
index 0000000..df9bdbf
--- /dev/null
+++ b/doc/crmsh_hb_report.8.txt
@@ -0,0 +1,477 @@
+:man source:   crmsh_hb_report
+:man version:  1.2
+:man manual:   Pacemaker documentation
+
+crmsh_hb_report(8)
+==================
+
+NAME
+----
+crmsh_hb_report - create report for CRM based clusters (Pacemaker)
+
+
+SYNOPSIS
+--------
+*crm report* -f {time|"cts:"testnum} [-t time] [-u user] [-l file]
+       [-n nodes] [-E files] [-p patt] [-L patt] [-e prog]
+	   [-MSDCZAQVsvhd] [dest]
+
+
+DESCRIPTION
+-----------
+The crmsh_hb_report(8) is a utility to collect all information (logs,
+configuration files, system information, etc) relevant to
+Pacemaker (CRM) over the given period of time.
+
+
+OPTIONS
+-------
+dest::
+	The report name. It can also contain a path where to put the
+	report tarball. If left out, the tarball is created in the
+	current directory named "hb_report-current_date", for instance
+	hb_report-Wed-03-Mar-2010.
+
+*-d*::
+	Don't create the compressed tar, but leave the result in a
+	directory.
+
+*-f* { time | "cts:"testnum }::
+	The start time from which to collect logs. The time is in the
+	format as used by the Date::Parse perl module. For cts tests,
+	specify the "cts:" string followed by the test number. This
+	option is required.
+
+*-t* time::
+	The end time to which to collect logs. Defaults to now.
+
+*-n* nodes::
+	A list of space separated hostnames (cluster members).
+	crm report may try to find out the set of nodes by itself, but
+	if it runs on the loghost which, as it is usually the case,
+	does not belong to the cluster, that may be difficult. Also,
+	OpenAIS doesn't contain a list of nodes and if Pacemaker is
+	not running, there is no way to find it out automatically.
+	This option is cumulative (i.e. use -n "a b" or -n a -n b).
+
+*-l* file::
+	Log file location. If, for whatever reason, crm report cannot
+	find the log files, you can specify its absolute path.
+
+*-E* files::
+	Extra log files to collect. This option is cumulative. By
+	default, /var/log/messages are collected along with the
+	cluster logs.
+
+*-M*::
+	Don't collect extra log files, but only the file containing
+	messages from the cluster subsystems.
+
+*-L* patt::
+	A list of regular expressions to match in log files for
+	analysis. This option is additive (default: "CRIT: ERROR:").
+
+*-p* patt::
+	Additional patterns to match parameter name which contain
+	sensitive information. This option is additive (default: "passw.*").
+
+*-Q*::
+	Quick run. Gathering some system information can be expensive.
+	With this option, such operations are skipped and thus
+	information collecting sped up. The operations considered
+	I/O or CPU intensive: verifying installed packages content,
+	sanitizing files for sensitive information, and producing dot
+	files from PE inputs.
+
+*-A*::
+	This is an OpenAIS cluster. `crm report` has some heuristics to
+	find the cluster stack, but that is not always reliable.
+	By default, `crm report` assumes that it is run on a Heartbeat
+	cluster.
+
+*-u* user::
+	The ssh user. `crm report` will try to login to other nodes
+	without specifying a user, then as "root", and finally as
+	"hacluster". If you have another user for administration over
+	ssh, please use this option.
+
+*-X* ssh-options::
+	Extra ssh options. These will be added to every ssh
+	invocation. Alternatively, use `$HOME/.ssh/config` to setup
+	desired ssh connection options.
+
+*-S*::
+	Single node operation. Run `crm report` only on this node and
+	don't try to start slave collectors on other members of the
+	cluster. Under normal circumstances this option is not
+	needed. Use if ssh(1) does not work to other nodes.
+
+*-Z*::
+	If the destination directory exist, remove it instead of
+	exiting (this is default for CTS).
+
+*-V*::
+	Print the version including the last repository changeset.
+
+*-v*::
+	Increase verbosity. Normally used to debug unexpected
+	behaviour.
+
+*-h*::
+	Show usage and some examples.
+
+*-D* (obsolete)::
+	Don't invoke editor to fill the description text file.
+
+*-e* prog (obsolete)::
+	Your favourite text editor. Defaults to $EDITOR, vim, vi,
+	emacs, or nano, whichever is found first.
+
+*-C* (obsolete)::
+	Remove the destination directory once the report has been put
+	in a tarball.
+
+EXAMPLES
+--------
+Last night during the backup there were several warnings
+encountered (logserver is the log host):
+
+	logserver# crm report -f 3:00 -t 4:00 -n "node1 node2" report
+
+collects everything from all nodes from 3am to 4am last night.
+The files are compressed to a tarball report.tar.bz2.
+
+Just found a problem during testing:
+
+	# note the current time
+	node1# date
+	Fri Sep 11 18:51:40 CEST 2009
+	node1# /etc/init.d/heartbeat start
+	node1# nasty-command-that-breaks-things
+	node1# sleep 120 #wait for the cluster to settle
+	node1# crm report -f 18:51 hb1
+
+	# if crm report can't figure out that this is corosync
+	node1# crm report -f 18:51 -A hb1
+
+	# if crm report can't figure out the cluster members
+	node1# crm report -f 18:51 -n "node1 node2" hb1
+
+The files are compressed to a tarball hb1.tar.bz2.
+
+INTERPRETING RESULTS
+--------------------
+The compressed tar archive is the final product of `crm report`.
+This is one example of its content, for a CTS test case on a
+three node OpenAIS cluster:
+
+	$ ls -RF 001-Restart
+
+	001-Restart:
+	analysis.txt     events.txt  logd.cf       s390vm13/  s390vm16/
+	description.txt  ha-log.txt  openais.conf  s390vm14/
+
+	001-Restart/s390vm13:
+	STOPPED  crm_verify.txt  hb_uuid.txt  openais.conf@   sysinfo.txt
+	cib.txt  dlm_dump.txt    logd.cf@     pengine/        sysstats.txt
+	cib.xml  events.txt      messages     permissions.txt
+
+	001-Restart/s390vm13/pengine:
+	pe-input-738.bz2  pe-input-740.bz2  pe-warn-450.bz2
+	pe-input-739.bz2  pe-warn-449.bz2   pe-warn-451.bz2
+
+	001-Restart/s390vm14:
+	STOPPED  crm_verify.txt  hb_uuid.txt  openais.conf@   sysstats.txt
+	cib.txt  dlm_dump.txt    logd.cf@     permissions.txt
+	cib.xml  events.txt      messages     sysinfo.txt
+
+	001-Restart/s390vm16:
+	STOPPED  crm_verify.txt  hb_uuid.txt  messages        sysinfo.txt
+	cib.txt  dlm_dump.txt    hostcache    openais.conf@   sysstats.txt
+	cib.xml  events.txt      logd.cf@     permissions.txt
+
+The top directory contains information which pertains to the
+cluster or event as a whole. Files with exactly the same content
+on all nodes will also be at the top, with per-node links created
+(as it is in this example the case with openais.conf and logd.cf).
+
+The cluster log files are named ha-log.txt regardless of the
+actual log file name on the system. If it is found on the
+loghost, then it is placed in the top directory. If not, the top
+directory ha-log.txt contains all nodes logs merged and sorted by
+time. Files named messages are excerpts of /var/log/messages from
+nodes.
+
+Most files are copied verbatim or they contain output of a
+command. For instance, cib.xml is a copy of the CIB found in
+/var/lib/heartbeat/crm/cib.xml. crm_verify.txt is output of the
+crm_verify(8) program.
+
+Some files are result of a more involved processing:
+
+	*analysis.txt*::
+	A set of log messages matching user defined patterns (may be
+	provided with the -L option).
+
+	*events.txt*::
+	A set of log messages matching event patterns. It should
+	provide information about major cluster motions without
+	unnecessary details.  These patterns are devised by the
+	cluster experts.  Currently, the patterns cover membership
+	and quorum changes, resource starts and stops, fencing
+	(stonith) actions, and cluster starts and stops. events.txt
+	is always generated for each node. In case the central
+	cluster log was found, also combined for all nodes.
+
+	*permissions.txt*::
+	One of the more common problem causes are file and directory
+	permissions. `crm report` looks for a set of predefined
+	directories and checks their permissions. Any issues are
+	reported here.
+
+	*backtraces.txt*::
+	gdb generated backtrace information for cores dumped
+	within the specified period.
+
+	*sysinfo.txt*::
+	Various release information about the platform, kernel,
+	operating system, packages, and anything else deemed to be
+	relevant. The static part of the system.
+
+	*sysstats.txt*::
+	Output of various system commands such as ps(1), uptime(1),
+	netstat(8), and ip(8). The dynamic part of the system.
+
+description.txt should contain a user supplied description of the
+problem, but since it is very seldom used, it will be dropped
+from the future releases.
+
+PREREQUISITES
+-------------
+
+ssh::
+	It is not strictly required, but you won't regret having a
+	password-less ssh. It is not too difficult to setup and will save
+	you a lot of time. If you can't have it, for example because your
+	security policy does not allow such a thing, or you just prefer
+	menial work, then you will have to resort to the semi-manual
+	semi-automated report generation. See below for instructions.
+	+
+	If you need to supply a password for your passphrase/login, then
+	always use the `-u` option.
+	+
+	For extra ssh(1) options, if you're too lazy to setup
+	$HOME/.ssh/config, use the `-X` option. Do not forget to put
+	the options in quotes.
+
+sudo::
+	If the ssh user (as specified with the `-u` option) is other
+	than `root`, then `crm report` uses `sudo` to collect the
+	information which is readable only by the `root` user. In that
+	case it is required to setup the `sudoers` file properly. The
+	user (or group to which the user belongs) should have the
+	following line:
+	+
+	<user> ALL = NOPASSWD: /usr/sbin/crm
+	+
+	See the `sudoers(5)` man page for more details.
+
+Times::
+	In order to find files and messages in the given period and to
+	parse the `-f` and `-t` options, `crm report` uses perl and one of the
+	`Date::Parse` or `Date::Manip` perl modules. Note that you need
+	only one of these. Furthermore, on nodes which have no logs and
+	where you don't run `crm report` directly, no date parsing is
+	necessary. In other words, if you run this on a loghost then you
+	don't need these perl modules on the cluster nodes.
+	+
+	On rpm based distributions, you can find `Date::Parse` in
+	`perl-TimeDate` and on Debian and its derivatives in
+	`libtimedate-perl`.
+
+Core dumps::
+	To backtrace core dumps gdb is needed and the packages with
+	the debugging info. The debug info packages may be installed
+	at the time the report is created. Let's hope that you will
+	need this really seldom.
+
+TIMES
+-----
+
+Specifying times can at times be a nuisance. That is why we have
+chosen to use one of the perl modules--they do allow certain
+freedom when talking dates. You can either read the instructions
+at the
+http://search.cpan.org/dist/TimeDate/lib/Date/Parse.pm#EXAMPLE_DATES[Date::Parse
+examples page].
+or just rely on common sense and try stuff like:
+
+	3:00          (today at 3am)
+	15:00         (today at 3pm)
+	2007/9/1 2pm  (September 1st at 2pm)
+	Tue Sep 15 20:46:27 CEST 2009 (September 15th etc)
+
+`crm report` will (probably) complain if it can't figure out what do
+you mean.
+
+Try to delimit the event as close as possible in order to reduce
+the size of the report, but still leaving a minute or two around
+for good measure.
+
+`-f` is not optional. And don't forget to quote dates when they
+contain spaces.
+
+
+Should I send all this to the rest of Internet?
+-----------------------------------------------
+
+By default, the sensitive data in CIB and PE files is not mangled
+by `crm report` because that makes PE input files mostly useless.
+If you still have no other option but to send the report to a
+public mailing list and do not want the sensitive data to be
+included, use the `-s` option. Without this option, `crm report`
+will issue a warning if it finds information which should not be
+exposed. By default, parameters matching 'passw.*' are considered
+sensitive.  Use the `-p` option to specify additional regular
+expressions to match variable names which may contain information
+you don't want to leak. For example:
+
+	# crm report -f 18:00 -p "user.*" -p "secret.*" /var/tmp/report
+
+Heartbeat's ha.cf is always sanitized. Logs and other files are
+not filtered.
+
+LOGS
+----
+
+It may be tricky to find syslog logs. The scheme used is to log a
+unique message on all nodes and then look it up in the usual
+syslog locations. This procedure is not foolproof, in particular
+if the syslog files are in a non-standard directory. We look in
+/var/log /var/logs /var/syslog /var/adm /var/log/ha
+/var/log/cluster. In case we can't find the logs, please supply
+their location:
+
+	# crm report -f 5pm -l /var/log/cluster1/ha-log -S /tmp/report_node1
+
+If you have different log locations on different nodes, well,
+perhaps you'd like to make them the same and make life easier for
+everybody.
+
+Files starting with "ha-" are preferred. In case syslog sends
+messages to more than one file, if one of them is named ha-log or
+ha-debug those will be favoured over syslog or messages.
+
+`crm report` supports also archived logs in case the period
+specified extends that far in the past. The archives must reside
+in the same directory as the current log and their names must
+be prefixed with the name of the current log (syslog-1.gz or
+messages-20090105.bz2).
+
+If there is no separate log for the cluster, possibly unrelated
+messages from other programs are included. We don't filter logs,
+but just pick a segment for the period you specified.
+
+MANUAL REPORT COLLECTION
+------------------------
+
+So, your ssh doesn't work. In that case, you will have to run
+this procedure on all nodes. Use `-S` so that `crm report` doesn't
+bother with ssh:
+
+	# crm report -f 5:20pm -t 5:30pm -S /tmp/report_node1
+
+If you also have a log host which is not in the cluster, then
+you'll have to copy the log to one of the nodes and tell us where
+it is:
+
+	# crm report -f 5:20pm -t 5:30pm -l /var/tmp/ha-log -S /tmp/report_node1
+
+OPERATION
+---------
+`crm report` collects files and other information in a fairly
+straightforward way. The most complex tasks are discovering the
+log file locations (if syslog is used which is the most common
+case) and coordinating the operation on multiple nodes.
+
+The instance of `crm report` running on the host where it was
+invoked is the master instance. Instances running on other nodes
+are slave instances. The master instance communicates with slave
+instances by ssh. There are multiple ssh invocations per run, so
+it is essential that the ssh works without password, i.e. with
+the public key authentication and authorized_keys.
+
+The operation consists of three phases. Each phase must finish
+on all nodes before the next one can commence. The first phase
+consists of logging unique messages through syslog on all nodes.
+This is the shortest of all phases.
+
+The second phase is the most involved. During this phase all
+local information is collected, which includes:
+
+- logs (both current and archived if the start time is far in the past)
+- various configuration files (corosync, heartbeat, logd)
+- the CIB (both as xml and as represented by the crm shell)
+- pengine inputs (if this node was the DC at any point in
+  time over the given period)
+- system information and status
+- package information and status
+- dlm lock information
+- backtraces (if there were core dumps)
+
+The third phase is collecting information from all nodes and
+analyzing it. The analyzis consists of the following tasks:
+
+- identify files equal on all nodes which may then be moved to
+  the top directory
+- save log messages matching user defined patterns
+  (defaults to ERRORs and CRITical conditions)
+- report if there were coredumps and by whom
+- report crm_verify(8) results
+- save log messages matching major events to events.txt
+- in case logging is configured without loghost, node logs and
+  events files are combined using a perl utility
+
+
+BUGS
+----
+Finding logs may at times be extremely difficult, depending on
+how weird the syslog configuration. It would be nice to ask
+syslog-ng developers to provide a way to find out the log
+destination based on facility and priority.
+
+If you think you found a bug, please rerun with the -v option and
+attach the output to bugzilla.
+
+`crm report` can function in a satisfactory way only if ssh works to
+all nodes using authorized_keys (without password).
+
+There are way too many options.
+
+
+AUTHOR
+------
+Written by Dejan Muhamedagic, <dejan at suse.de>
+
+
+RESOURCES
+---------
+ClusterLabs: <http://clusterlabs.org/>
+
+Heartbeat and other Linux HA resources: <http://linux-ha.org/wiki>
+
+OpenAIS: <http://www.openais.org/>
+
+Corosync: <http://www.corosync.org/>
+
+
+SEE ALSO
+--------
+crm(8), Date::Parse(3)
+
+
+COPYING
+-------
+Copyright \(C) 2007-2009 Dejan Muhamedagic. Free use of this
+software is granted under the terms of the GNU General Public License (GPL).
+
diff --git a/doc/website-v1/404.txt b/doc/website-v1/404.txt
new file mode 100644
index 0000000..926d803
--- /dev/null
+++ b/doc/website-v1/404.txt
@@ -0,0 +1,9 @@
+404: Page not found
+===================
+
+Apologies, but there is nothing here!
+
+The page you are looking for may have moved.
+
+* link:/documentation[Documentation]
+* link:/faq[Frequently Asked Questions]
diff --git a/doc/website-v1/Makefile b/doc/website-v1/Makefile
new file mode 100644
index 0000000..2036d9e
--- /dev/null
+++ b/doc/website-v1/Makefile
@@ -0,0 +1,88 @@
+ASCIIDOC := asciidoc
+SRC := faq.txt documentation.txt development.txt installation.txt \
+	configuration.txt about.txt rsctest-guide.txt \
+	history-guide.txt start-guide.txt man-1.2.txt scripts.txt man-2.0.txt
+TGT := $(patsubst %.txt,gen/%/index.html,$(SRC))
+CSS := css/crm.css css/font-awesome.min.css
+CSS := $(patsubst %,gen/%,$(CSS))
+IMG := img/loader.gif img/laptop.png img/servers.gif
+IMG  := $(patsubst %,gen/%,$(IMG))
+FONTS := fonts/FontAwesome.otf fonts/fontawesome-webfont.eot \
+	fonts/fontawesome-webfont.svg fonts/fontawesome-webfont.ttf \
+	fonts/fontawesome-webfont.woff
+FONTS := $(patsubst %,gen/%,$(FONTS))
+WATCHDIR := watchdir
+XDGOPEN := xdg-open
+NEWS := $(wildcard news/*.txt)
+NEWSDOC := $(patsubst %.txt,gen/%/index.html,$(NEWS))
+
+.PHONY: all clean deploy open
+
+all: site
+
+gen/index.html: index.txt crm.conf
+	@mkdir -p $(dir $@)
+	@$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	@python ./postprocess.py -o $@ $<
+
+gen/%/index.html: %.txt crm.conf
+	@mkdir -p $(dir $@)
+	@$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	@python ./postprocess.py -o $@ $<
+
+gen/man/index.html: ../crm.8.txt crm.conf
+	@mkdir -p $(dir $@)
+	@$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	@python ./postprocess.py -o $@ $<
+
+gen/404.html: 404.txt crm.conf
+	@mkdir -p $(dir $@)
+	@$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	@python ./postprocess.py -o $@ $<
+
+news.txt: $(NEWS) crm.conf
+	@echo "news:" $(NEWS)
+	python ./make-news.py $@ $(NEWS)
+
+gen/news/index.html: news.txt
+	@mkdir -p $(dir $@)
+	$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	@python ./postprocess.py -o $@ $<
+
+gen/css/%.css: css/%.css
+	@mkdir -p gen/css
+	@cp -r $< $@
+	@echo "+ $@"
+
+gen/js/%.js: js/%.js
+	@mkdir -p gen/js
+	@cp -r $< $@
+	@echo "+ $@"
+
+gen/img/%: img/%
+	@mkdir -p gen/img
+	@cp -r $< $@
+	@echo "+ $@"
+
+gen/fonts/%: fonts/%
+	@mkdir -p gen/fonts
+	@cp -r $< $@
+	@echo "+ $@"
+
+gen/atom.xml: $(NEWSDOC)
+	@echo "atom:" $(NEWSDOC)
+	python ./make-news.py gen/atom.xml $(NEWS)
+
+site: gen/atom.xml gen/index.html gen/404.html gen/news/index.html gen/man/index.html $(TGT) $(CSS) $(IMG) $(FONTS) $(NEWSDOC)
+
+deploy: site
+	@echo "TODO: CVS upload"
+
+open: site
+	@$(XDGOPEN) gen/index.html
+
+watch:
+	@$(WATCHDIR) --verbose --cmd "make" . css img fonts
+
+clean:
+	-@$(RM) -rf gen/* news.txt
diff --git a/doc/website-v1/about.txt b/doc/website-v1/about.txt
new file mode 100644
index 0000000..2656625
--- /dev/null
+++ b/doc/website-v1/about.txt
@@ -0,0 +1,19 @@
+= About =
+
+== Authors ==
+
+include::../../AUTHORS[]
+
+== Site ==
+
+This site was generated from http://asciidoc.org[AsciiDoc] sources.
+
+The CSS for this site started as a clone of the +bare+ theme by https://github.com/rtomayko/adoc-themes[Ryan Tomayko].
+
+Fonts used are https://www.google.com/fonts/specimen/Open+Sans[Open Sans] and http://fontawesome.io[Font Awesome].
+
+== License ==
+
+`crmsh` is licensed under the GNU General Public License (GPL).
+
+For more information, see https://gnu.org/licenses/gpl.html
diff --git a/doc/website-v1/configuration.txt b/doc/website-v1/configuration.txt
new file mode 100644
index 0000000..fb48c93
--- /dev/null
+++ b/doc/website-v1/configuration.txt
@@ -0,0 +1,132 @@
+= Configuration =
+
+.Version information
+NOTE: This section applies to `crmsh 2.0+` only.
+
+
+`crm` can be configured using both a system-wide configuration file,
+and a per-user configuration file. The values set in the user-local
+file take precedence over the system-wide settings.
+
+The global configuration file is usually installed at
+`/etc/crm/crm.conf`, and the user-local configuration file at
+`~/.config/crm/crm.conf`.
+
+
+== Upgrading from crm 1.x to 2.x ==
+
+The configuration file format and location changed significantly going
+from crm 1.x to 2.x. If `crm` cannot find a user-local configuration
+file when starting up, it will look for an old-style configuration
+file at `~/.crm.rc`. If this file exists, `crm` will prompt the user
+asking if the old-style configuration should be  automatically
+converted to a new-style configuration file.
+
+
+== Format description ==
+
+The `settings` file consists of sections introduced by a `[section]`
+header, and followed by `name=value` pairs.
+
+Leading whitespace is stripped from values.
+
+Values can contain format strings referring to other values in the
+same section.
+
+Lines starting with `#` or `;` are interpreted as comments.
+
+Values starting with `$` are interpreted as environment variable
+references, and the value will be retrieved from the named environment
+variable if set.
+
+== Example configuration ==
+
+The example configuration below lists all available options and their
+default values.
+
+----------------------
+[core]
+editor = $EDITOR
+pager = $PAGER
+user =
+skill_level = expert
+sort_elements = yes
+check_frequency = always
+check_mode = strict
+wait = no
+add_quotes = yes
+manage_children = ask
+force = no
+debug = no
+ptest = ptest, crm_simulate
+dotty = dotty
+dot = dot
+
+[path]
+sharedir = /usr/share/crmsh
+cache = /var/cache/crm
+crm_config = /var/lib/pacemaker/cib
+crm_daemon_dir = /usr/lib64/pacemaker
+crm_daemon_user = hacluster
+ocf_root = /usr/lib/ocf
+crm_dtd_dir = /usr/share/pacemaker
+pe_state_dir = /var/lib/pacemaker/pengine
+heartbeat_dir = /var/lib/heartbeat
+hb_delnode = /usr/share/heartbeat/hb_delnode
+nagios_plugins = /usr/lib64/nagios/plugins
+
+[color]
+style = color
+error = red bold
+ok = green bold
+warn = yellow bold
+info = cyan
+help_keyword = blue bold underline
+help_header = normal bold
+help_topic = yellow bold
+help_block = cyan
+keyword = yellow
+identifier = normal
+attr_name = cyan
+attr_value = red
+resource_reference = green
+id_reference = green
+score = magenta
+ticket = magenta
+----------------------
+
+
+== Loading and saving options ==
+
+Options are loaded from the global configuration file first, and the
+user-local file second. This means that the user-local options take
+precedence over the global configuration.
+
+When changing an option using the `options` sublevel, the
+configuration file is written to disk with the new value.
+
+== Syntax highlighting ==
+
+By default, `crm` will try to syntax highlight its output when
+connected to a TTY. To disable this behavior, set the configuration
+value `style = none` in the `[color]` section.
+
+The available color choices may depend on the terminal used, but
+normally include the following:
+
+----
+black blue green cyan red magenta yellow white
+----
+
+Colors can be combined with styles:
+
+----
+bold blink dim reverse underline normal
+----
+
+== Setting options from the interactive shell ==
+
+Options can be set directly from the interactive shell using the
+`options` sublevel. These options will be written to the per-user
+configuration file. Note that changing an option in this way may erase
+comments added to the configuration file.
diff --git a/doc/website-v1/crm.conf b/doc/website-v1/crm.conf
new file mode 100644
index 0000000..44dcc41
--- /dev/null
+++ b/doc/website-v1/crm.conf
@@ -0,0 +1,587 @@
+#
+# html5.conf
+#
+# Asciidoc configuration file.
+# html5 backend.
+#
+
+[miscellaneous]
+outfilesuffix=.html
+
+[attributes]
+basebackend=html
+basebackend-html=
+basebackend-html5=
+b
+[replacements2]
+# Line break.
+(?m)^(.*)\s\+$=\1<br>
+
+[replacements]
+ifdef::asciidoc7compatible[]
+# Superscripts.
+\^(.+?)\^=<sup>\1</sup>
+# Subscripts.
+~(.+?)~=<sub>\1</sub>
+endif::asciidoc7compatible[]
+
+[ruler-blockmacro]
+<hr>
+
+[pagebreak-blockmacro]
+<div style="page-break-after:always"></div>
+
+[blockdef-pass]
+asciimath-style=template="asciimathblock",subs=()
+latexmath-style=template="latexmathblock",subs=()
+
+[macros]
+(?u)^(?P<name>audio|video)::(?P<target>\S*?)(\[(?P<attrlist>.*?)\])$=#
+# math macros.
+# Special characters are escaped in HTML math markup.
+(?su)[\\]?(?P<name>asciimath|latexmath):(?P<subslist>\S*?)\[(?P<passtext>.*?)(?<!\\)\]=[specialcharacters]
+(?u)^(?P<name>asciimath|latexmath)::(?P<subslist>\S*?)(\[(?P<passtext>.*?)\])$=#[specialcharacters]
+
+[asciimath-inlinemacro]
+`{passtext}`
+
+[asciimath-blockmacro]
+<div class="mathblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="content">
+<div class="title">{title}</div>
+`{passtext}`
+</div></div>
+
+[asciimathblock]
+<div class="mathblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="content">
+<div class="title">{title}</div>
+`|`
+</div></div>
+
+[latexmath-inlinemacro]
+{passtext}
+
+[latexmath-blockmacro]
+<div class="mathblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="content">
+<div class="title">{title}</div>
+{passtext}
+</div></div>
+
+[latexmathblock]
+<div class="mathblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="content">
+<div class="title">{title}</div>
+|
+</div></div>
+
+[image-inlinemacro]
+<span class="image{role? {role}}">
+<a class="image" href="{link}">
+{data-uri%}<img src="{imagesdir=}{imagesdir?/}{target}" alt="{alt={target}}"{width? width="{width}"}{height? height="{height}"}{title? title="{title}"}>
+{data-uri#}<img alt="{alt={target}}"{width? width="{width}"}{height? height="{height}"}{title? title="{title}"}
+{data-uri#}{sys:"{python}" -u -c "import mimetypes,base64,sys; print 'src=\"data:'+mimetypes.guess_type(r'{target}')[0]+';base64,'; base64.encode(sys.stdin,sys.stdout)" < "{eval:os.path.join(r"{indir={outdir}}",r"{imagesdir=}",r"{target}")}"}">
+{link#}</a>
+</span>
+
+[image-blockmacro]
+<div class="imageblock{style? {style}}{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}{align? style="text-align:{align};"}{float? style="float:{float};"}>
+<div class="content">
+<a class="image" href="{link}">
+{data-uri%}<img src="{imagesdir=}{imagesdir?/}{target}" alt="{alt={target}}"{width? width="{width}"}{height? height="{height}"}>
+{data-uri#}<img alt="{alt={target}}"{width? width="{width}"}{height? height="{height}"}
+{data-uri#}{sys:"{python}" -u -c "import mimetypes,base64,sys; print 'src=\"data:'+mimetypes.guess_type(r'{target}')[0]+';base64,'; base64.encode(sys.stdin,sys.stdout)" < "{eval:os.path.join(r"{indir={outdir}}",r"{imagesdir=}",r"{target}")}"}">
+{link#}</a>
+</div>
+<div class="title">{caption={figure-caption} {counter:figure-number}. }{title}</div>
+</div>
+
+[audio-blockmacro]
+<div class="audioblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{caption=}{title}</div>
+<div class="content">
+<audio src="{imagesdir=}{imagesdir?/}{target}"{autoplay-option? autoplay}{nocontrols-option! controls}{loop-option? loop}>
+Your browser does not support the audio tag.
+</audio>
+</div></div>
+
+[video-blockmacro]
+<div class="videoblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{caption=}{title}</div>
+<div class="content">
+<video src="{imagesdir=}{imagesdir?/}{target}"{width? width="{width}"}{height? height="{height}"}{poster? poster="{poster}"}{autoplay-option? autoplay}{nocontrols-option! controls}{loop-option? loop}>
+Your browser does not support the video tag.
+</video>
+</div></div>
+
+[unfloat-blockmacro]
+<div style="clear:both;"></div>
+
+[toc-blockmacro]
+template::[toc]
+
+[indexterm-inlinemacro]
+# Index term.
+{empty}
+
+[indexterm2-inlinemacro]
+# Index term.
+# Single entry index term that is visible in the primary text flow.
+{1}
+
+[footnote-inlinemacro]
+# footnote:[<text>].
+<span class="footnote"><br>[{0}]<br></span>
+
+[footnoteref-inlinemacro]
+# footnoteref:[<id>], create reference to footnote.
+{2%}<span class="footnoteref"><br><a href="#_footnote_{1}">[{1}]</a><br></span>
+# footnoteref:[<id>,<text>], create footnote with ID.
+{2#}<span class="footnote" id="_footnote_{1}"><br>[{2}]<br></span>
+
+[callout-inlinemacro]
+ifndef::icons[]
+<b><{index}></b>
+endif::icons[]
+ifdef::icons[]
+ifndef::data-uri[]
+<img src="{icon={iconsdir}/callouts/{index}.png}" alt="{index}">
+endif::data-uri[]
+ifdef::data-uri[]
+<img alt="{index}" src="data:image/png;base64,
+{sys:"{python}" -u -c "import base64,sys; base64.encode(sys.stdin,sys.stdout)" < "{eval:os.path.join(r"{indir={outdir}}",r"{icon={iconsdir}/callouts/{index}.png}")}"}">
+endif::data-uri[]
+endif::icons[]
+
+# Comment line macros.
+[comment-inlinemacro]
+{showcomments#}<br><span class="comment">{passtext}</span><br>
+
+[comment-blockmacro]
+{showcomments#}<p><span class="comment">{passtext}</span></p>
+
+[literal-inlinemacro]
+# Inline literal.
+<span class="monospaced">{passtext}</span>
+
+# List tags.
+[listtags-bulleted]
+list=<div class="ulist{style? {style}}{compact-option? compact}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<ul>|</ul></div>
+item=<li>|</li>
+text=<p>|</p>
+
+[listtags-numbered]
+# The start attribute is not valid XHTML 1.1 but all browsers support it.
+list=<div class="olist{style? {style}}{compact-option? compact}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<ol class="{style}"{start? start="{start}"}>|</ol></div>
+item=<li>|</li>
+text=<p>|</p>
+
+[listtags-labeled]
+list=<div class="dlist{compact-option? compact}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<dl>|</dl></div>
+entry=
+label=
+term=<dt class="hdlist1{strong-option? strong}">|</dt>
+item=<dd>|</dd>
+text=<p>|</p>
+
+[listtags-horizontal]
+list=<div class="hdlist{compact-option? compact}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<table>{labelwidth?<col width="{labelwidth}%">}{itemwidth?<col width="{itemwidth}%">}|</table></div>
+label=<td class="hdlist1{strong-option? strong}">|</td>
+term=|<br>
+entry=<tr>|</tr>
+item=<td class="hdlist2">|</td>
+text=<p style="margin-top: 0;">|</p>
+
+[listtags-qanda]
+list=<div class="qlist{style? {style}}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<ol>|</ol></div>
+entry=<li>|</li>
+label=
+term=<p><em>|</em></p>
+item=
+text=<p>|</p>
+
+[listtags-callout]
+ifndef::icons[]
+list=<div class="colist{style? {style}}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<ol>|</ol></div>
+item=<li>|</li>
+text=<p>|</p>
+endif::icons[]
+ifdef::icons[]
+list=<div class="colist{style? {style}}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<table>|</table></div>
+ifndef::data-uri[]
+item=<tr><td><img src="{iconsdir}/callouts/{listindex}.png" alt="{listindex}"></td><td>|</td></tr>
+endif::data-uri[]
+ifdef::data-uri[]
+item=<tr><td><img alt="{listindex}" src="data:image/png;base64, {sys:"{python}" -u -c "import base64,sys; base64.encode(sys.stdin,sys.stdout)" < "{eval:os.path.join(r"{indir={outdir}}",r"{icon={iconsdir}/callouts/{listindex}.png}")}"}"></td><td>|</td></tr>
+endif::data-uri[]
+text=|
+endif::icons[]
+
+[listtags-glossary]
+list=<div class="dlist{style? {style}}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<dl>|</dl></div>
+label=
+entry=
+term=<dt>|</dt>
+item=<dd>|</dd>
+text=<p>|</p>
+
+[listtags-bibliography]
+list=<div class="ulist{style? {style}}{role? {role}}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<ul>|</ul></div>
+item=<li>|</li>
+text=<p>|</p>
+
+[tags]
+# Quoted text.
+emphasis=<em>{1?<span class="{1}">}|{1?</span>}</em>
+strong=<strong>{1?<span class="{1}">}|{1?</span>}</strong>
+monospaced=<span class="monospaced{1? {1}}">|</span>
+singlequoted={lsquo}{1?<span class="{1}">}|{1?</span>}{rsquo}
+doublequoted={ldquo}{1?<span class="{1}">}|{1?</span>}{rdquo}
+unquoted={1?<span class="{1}">}|{1?</span>}
+superscript=<sup>{1?<span class="{1}">}|{1?</span>}</sup>
+subscript=<sub>{1?<span class="{1}">}|{1?</span>}</sub>
+
+ifdef::deprecated-quotes[]
+# Override with deprecated quote attributes.
+emphasis={role?<span class="{role}">}<em{1,2,3? style="}{1?color:{1};}{2?background-color:{2};}{3?font-size:{3}em;}{1,2,3?"}>|</em>{role?</span>}
+strong={role?<span class="{role}">}<strong{1,2,3? style="}{1?color:{1};}{2?background-color:{2};}{3?font-size:{3}em;}{1,2,3?"}>|</strong>{role?</span>}
+monospaced=<span class="monospaced{role? {role}}"{1,2,3? style="}{1?color:{1};}{2?background-color:{2};}{3?font-size:{3}em;}{1,2,3?"}>|</span>
+singlequoted={role?<span class="{role}">}{1,2,3?<span style="}{1?color:{1};}{2?background-color:{2};}{3?font-size:{3}em;}{1,2,3?">}{amp}#8216;|{amp}#8217;{1,2,3?</span>}{role?</span>}
+doublequoted={role?<span class="{role}">}{1,2,3?<span style="}{1?color:{1};}{2?background-color:{2};}{3?font-size:{3}em;}{1,2,3?">}{amp}#8220;|{amp}#8221;{1,2,3?</span>}{role?</span>}
+unquoted={role?<span class="{role}">}{1,2,3?<span style="{1?color:{1};}{2?background-color:{2};}{3?font-size:{3}em;}">}|{1,2,3?</span>}{role?</span>}
+superscript={role?<span class="{role}">}<sup{1,2,3? style="}{1?color:{1};}{2?background-color:{2};}{3?font-size:{3}em;}{1,2,3?"}>|</sup>{role?</span>}
+subscript={role?<span class="{role}">}<sub{1,2,3? style="}{1?color:{1};}{2?background-color:{2};}{3?font-size:{3}em;}{1,2,3?"}>|</sub>{role?</span>}
+endif::deprecated-quotes[]
+
+# Inline macros
+[http-inlinemacro]
+<a href="{name}:{target}">{0={name}:{target}}</a>
+[https-inlinemacro]
+<a href="{name}:{target}">{0={name}:{target}}</a>
+[ftp-inlinemacro]
+<a href="{name}:{target}">{0={name}:{target}}</a>
+[file-inlinemacro]
+<a href="{name}:{target}">{0={name}:{target}}</a>
+[irc-inlinemacro]
+<a href="{name}:{target}">{0={name}:{target}}</a>
+[mailto-inlinemacro]
+<a href="mailto:{target}">{0={target}}</a>
+[link-inlinemacro]
+<a href="{target}">{0={target}}</a>
+[callto-inlinemacro]
+<a href="{name}:{target}">{0={target}}</a>
+# anchor:id[text]
+[anchor-inlinemacro]
+<a id="{target}"></a>
+# [[id,text]]
+[anchor2-inlinemacro]
+<a id="{1}"></a>
+# [[[id]]]
+[anchor3-inlinemacro]
+<a id="{1}"></a>[{1}]
+# xref:id[text]
+[xref-inlinemacro]
+<a href="#{target}">{0=[{target}]}</a>
+# <<id,text>>
+[xref2-inlinemacro]
+<a href="#{1}">{2=[{1}]}</a>
+
+# Special word substitution.
+[emphasizedwords]
+<em>{words}</em>
+[monospacedwords]
+<span class="monospaced">{words}</span>
+[strongwords]
+<strong>{words}</strong>
+
+# Paragraph substitution.
+[paragraph]
+<div class="paragraph{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>{title?<div class="title">{title}</div>}<p>
+|
+</p></div>
+
+[admonitionparagraph]
+template::[admonitionblock]
+
+# Delimited blocks.
+[listingblock]
+<div class="listingblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{caption=}{title}</div>
+<div class="content monospaced">
+<pre>
+|
+</pre>
+</div></div>
+
+[literalblock]
+<div class="literalblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{title}</div>
+<div class="content monospaced">
+<pre>
+|
+</pre>
+</div></div>
+
+[sidebarblock]
+<div class="sidebarblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="content">
+<div class="title">{title}</div>
+|
+</div></div>
+
+[openblock]
+<div class="openblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{title}</div>
+<div class="content">
+|
+</div></div>
+
+[partintroblock]
+template::[openblock]
+
+[abstractblock]
+template::[quoteblock]
+
+[quoteblock]
+<div class="quoteblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{title}</div>
+<div class="content">
+|
+</div>
+<div class="attribution">
+<em>{citetitle}</em>{attribution?<br>}
+— {attribution}
+</div></div>
+
+[verseblock]
+<div class="verseblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{title}</div>
+<pre class="content">
+|
+</pre>
+<div class="attribution">
+<em>{citetitle}</em>{attribution?<br>}
+— {attribution}
+</div></div>
+
+[exampleblock]
+<div class="exampleblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<div class="title">{caption={example-caption} {counter:example-number}. }{title}</div>
+<div class="content">
+|
+</div></div>
+
+[admonitionblock]
+<div class="admonitionblock{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}>
+<table><tr>
+<td class="icon">
+{data-uri%}{icons#}<img src="{icon={iconsdir}/{name}.png}" alt="{caption}">
+{data-uri#}{icons#}<img alt="{caption}" src="data:image/png;base64,
+{data-uri#}{icons#}{sys:"{python}" -u -c "import base64,sys; base64.encode(sys.stdin,sys.stdout)" < "{eval:os.path.join(r"{indir={outdir}}",r"{icon={iconsdir}/{name}.png}")}"}">
+{icons%}<div class="title">{caption}</div>
+</td>
+<td class="content">
+<div class="title">{title}</div>
+|
+</td>
+</tr></table>
+</div>
+
+# Tables.
+[tabletags-default]
+colspec=<col{autowidth-option! style="width:{colpcwidth}%;"}>
+bodyrow=<tr>|</tr>
+headdata=<th class="tableblock halign-{halign=left} valign-{valign=top}" {colspan at 1::colspan="{colspan}" }{rowspan at 1::rowspan="{rowspan}" }>|</th>
+bodydata=<td class="tableblock halign-{halign=left} valign-{valign=top}" {colspan at 1::colspan="{colspan}" }{rowspan at 1::rowspan="{rowspan}" }>|</td>
+paragraph=<p class="tableblock">|</p>
+
+[tabletags-header]
+paragraph=<p class="tableblock header">|</p>
+
+[tabletags-emphasis]
+paragraph=<p class="tableblock"><em>|</em></p>
+
+[tabletags-strong]
+paragraph=<p class="tableblock"><strong>|</strong></p>
+
+[tabletags-monospaced]
+paragraph=<p class="tableblock monospaced">|</p>
+
+[tabletags-verse]
+bodydata=<td class="tableblock halign-{halign=left} valign-{valign=top}" {colspan at 1::colspan="{colspan}" }{rowspan at 1::rowspan="{rowspan}" }><div class="verse">|</div></td>
+paragraph=
+
+[tabletags-literal]
+bodydata=<td class="tableblock halign-{halign=left} valign-{valign=top}" {colspan at 1::colspan="{colspan}" }{rowspan at 1::rowspan="{rowspan}" }><div class="literal monospaced"><pre>|</pre></div></td>
+paragraph=
+
+[tabletags-asciidoc]
+bodydata=<td class="tableblock halign-{halign=left} valign-{valign=top}" {colspan at 1::colspan="{colspan}" }{rowspan at 1::rowspan="{rowspan}" }><div>|</div></td>
+paragraph=
+
+[table]
+<table class="tableblock frame-{frame=all} grid-{grid=all}{role? {role}}{unbreakable-option? unbreakable}"{id? id="{id}"}
+style="
+margin-left:{align at left:0}{align at center|right:auto}; margin-right:{align at left|center:auto}{align at right:0};
+float:{float};
+{autowidth-option%}width:{tablepcwidth}%;
+{autowidth-option#}{width#style=width:{tablepcwidth}%;}
+">
+<caption class="title">{caption={table-caption} {counter:table-number}. }{title}</caption>
+{colspecs}
+{headrows#}<thead>
+{headrows}
+{headrows#}</thead>
+{footrows#}<tfoot>
+{footrows}
+{footrows#}</tfoot>
+<tbody>
+{bodyrows}
+</tbody>
+</table>
+
+#--------------------------------------------------------------------
+# Deprecated old table definitions.
+#
+
+[miscellaneous]
+# Screen width in pixels.
+pagewidth=800
+pageunits=px
+
+[old_tabledef-default]
+template=old_table
+colspec=<col style="width:{colwidth}{pageunits};" />
+bodyrow=<tr>|</tr>
+headdata=<th class="tableblock halign-{colalign=left}">|</th>
+footdata=<td class="tableblock halign-{colalign=left}">|</td>
+bodydata=<td class="tableblock halign-{colalign=left}">|</td>
+
+[old_table]
+<table class="tableblock frame-{frame=all} grid-{grid=all}"{id? id="{id}"}>
+<caption class="title">{caption={table-caption}}{title}</caption>
+{colspecs}
+{headrows#}<thead>
+{headrows}
+{headrows#}</thead>
+{footrows#}<tfoot>
+{footrows}
+{footrows#}</tfoot>
+<tbody style="vertical-align:top;">
+{bodyrows}
+</tbody>
+</table>
+
+# End of deprecated old table definitions.
+#--------------------------------------------------------------------
+
+[floatingtitle]
+<h{level at 0:1}{level at 1:2}{level at 2:3}{level at 3:4}{level at 4:5}{id? id="{id}"} class="float">{title}</h{level at 0:1}{level at 1:2}{level at 2:3}{level at 3:4}{level at 4:5}>
+
+[preamble]
+# Untitled elements between header and first section title.
+<div id="preamble">
+<div class="sectionbody">
+|
+</div>
+</div>
+
+# Document sections.
+[sect0]
+<h1{id? id="{id}"}>{title}</h1>
+|
+
+[sect1]
+<div class="sect1{style? {style}}{role? {role}}">
+<h2{id? id="{id}"}>{numbered?{sectnum} }{title}</h2>
+<div class="sectionbody">
+|
+</div>
+</div>
+
+[sect2]
+<div class="sect2{style? {style}}{role? {role}}">
+<h3{id? id="{id}"}>{numbered?{sectnum} }{title}</h3>
+|
+</div>
+
+[sect3]
+<div class="sect3{style? {style}}{role? {role}}">
+<h4{id? id="{id}"}>{numbered?{sectnum} }{title}</h4>
+|
+</div>
+
+[sect4]
+<div class="sect4{style? {style}}{role? {role}}">
+<h5{id? id="{id}"}>{title}</h5>
+|
+</div>
+
+[appendix]
+<div class="sect1{style? {style}}{role? {role}}">
+<h2{id? id="{id}"}>{numbered?{sectnum} }{appendix-caption} {counter:appendix-number:A}: {title}</h2>
+<div class="sectionbody">
+|
+</div>
+</div>
+
+[toc]
+<div id="toc">
+  <div id="toctitle">{toc-title}</div>
+  <noscript><p><b>JavaScript must be enabled in your browser to display the table of contents.</b></p></noscript>
+</div>
+
+[header]
+<!DOCTYPE html>
+<html lang="{lang=en}">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset={encoding}">
+<meta name="generator" content="AsciiDoc {asciidoc-version}">
+<meta name="description" content="{description}">
+<meta name="keywords" content="{keywords}">
+<title>crmsh - {title}</title>
+{title%}<title>crmsh - {doctitle=}</title>
+<link rel="stylesheet" href="/css/font-awesome.min.css">
+<link rel="stylesheet" href="/css/crm.css" type="text/css">
+<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,700|Ubuntu+Mono' rel='stylesheet' type='text/css'>
+<link href="/atom.xml" type="application/atom+xml" rel="alternate" title="crmsh atom feed">
+</head>
+<body>
+<div id="header">
+<h1><a href="/index.html"><span class="fa-stack">
+  <i class="fa fa-square fa-stack-2x"></i>
+  <i class="fa fa-terminal fa-stack-1x fa-inverse"></i>
+</span>crmsh</a></h1>
+<div id="topbar">
+<ul>
+<li><a href="/news">News</a></li>
+<li><a href="/documentation">Documentation</a></li>
+<li><a href="/development">Development</a></li>
+<li><a href="/about">About</a></li>
+</ul>
+</div>
+</div>
+<!--TOC-->
+<div id="container">
+<div id="content">
+<h1>{doctitle}</h1>
+
+[footer]
+</div>
+</div>
+<div id="footer">
+<div id="footer-text">
+</div>
+</div>
+
+<a href="https://github.com/ClusterLabs/crmsh"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://camo.githubusercontent.com/652c5b9acfaddf3a9c326fa6bde407b87f7be0f4/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6f72616e67655f6666373630302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_orange_ff7600.png"></a>
+
+</body>
+</html>
+
+ifdef::doctype-manpage[]
+[synopsis]
+template::[sect1]
+endif::doctype-manpage[]
+
diff --git a/doc/website-v1/css/crm.css b/doc/website-v1/css/crm.css
new file mode 100644
index 0000000..4a26960
--- /dev/null
+++ b/doc/website-v1/css/crm.css
@@ -0,0 +1,478 @@
+/* ---------------------------------------------------------------------------
+   Based on
+   Bare AsciiDoc styles
+   Ryan Tomayko <r at tomayko.com>
+
+   Heavily modified by
+   Kristoffer Gronlund <kgronlund at suse.com>
+   --------------------------------------------------------------------------- */
+
+body {
+    font-family:'Open Sans', 'lucida grande',verdana,helvetica,arial,sans-serif;
+    font-size: 100%;
+    line-height: 1.3636rem;
+    margin: 0px 0px;
+    padding: 0px;
+    width: 100%;
+    color:#333;
+    background: #fff;
+}
+
+em {
+    font-style:italic;
+}
+
+strong {
+    font-weight:bold;
+}
+
+.monospaced {
+    font-family:'Ubuntu Mono', consolas, 'lucida console', 'bitstream vera sans mono', 'courier new', monospace;
+    color: #211;
+}
+
+p {
+    margin-bottom: 1.3636rem;
+}
+
+ul, ol, dl {
+    margin-top: 1rem;
+    margin-bottom: 2rem;
+}
+
+ul p {
+    margin: 10px 0;
+}
+
+dl {
+    margin-left:40px
+}
+
+dt {
+    font-weight:normal;
+    color:#000;
+}
+
+h1, h2, h3, h4, h5 {
+    font-family:'Open Sans', 'lucida grande',verdana,helvetica,arial,sans-serif;
+    font-weight:normal;
+    color:#000;
+}
+
+h2, h3, h4, h5 {
+    padding-bottom: 0.333rem;
+    border-bottom: 1px solid #eee;
+}
+
+h1 {
+    font-size:2.6rem;
+    line-height:1.428;
+    margin:0px;
+    margin-top: 48px;
+}
+
+h2 {
+    font-size:2rem;
+    line-height:1.36363636; /* repeating, of course */
+    margin-top: 36px;
+    margin-bottom: 1.5rem;
+}
+
+h2 + .sectionbody {}
+
+h3 {
+    font-size:1.6rem;
+    line-height:1.1;
+    margin: 0px;
+    margin-top: 30px;
+}
+
+h4 {
+    font-weight: bold;
+    font-size:1.3rem;
+    line-height:1.538;
+}
+
+h5 {
+    font-size:1.2rem;
+    font-style:italic;
+    line-height:1.538;
+}
+
+pre {
+    font-family:'Ubuntu Mono', consolas, 'lucida console', 'bitstream vera sans mono', 'courier new', monospace;
+    color: #211;
+}
+
+#header {
+    background: #faf8f6;
+    padding-left: 24px;
+    padding-top: 4px;
+    padding-bottom: 0px;
+    border-bottom: 2px solid #efefea;
+}
+
+#header h1 {
+    margin: 0px;
+    font-size: 42px;
+    display: inline;
+}
+
+#header a {
+    text-decoration: none;
+    color: #34495e;
+}
+
+#header a:hover {
+    color:#ee3300;
+}
+
+#topbar {
+    display: inline;
+    font-size: 18px;
+}
+
+#topbar ul {
+    list-style: none;
+    display: inline;
+}
+
+#topbar li {
+    list-style: none;
+    display: inline;
+    padding-right: 1rem;
+}
+
+#container {
+    max-width: 720px;
+    margin-left: 240px;
+    padding-left: 8px;
+    text-align:left;
+}
+
+#author {
+    color:#999;
+}
+
+a {
+    text-decoration: none;
+    color:#419eda;
+}
+
+a:active {
+ color:#6ec654;
+}
+
+a:hover {
+ color:#ee3300;
+ text-decoration: underline;
+}
+
+
+#content {
+    text-align: justify;
+}
+
+h1 {
+    margin-left: auto;
+    margin-right: auto;
+    width: 551px;
+    text-align: center;
+    margin-bottom: 1.5rem;
+}
+
+.frontpage-image {
+    margin-left: auto;
+    margin-right: auto;
+    width: 551px;
+}
+
+.title, .sidebar-title {
+    font-weight:normal;
+    color:#000;
+    margin-bottom:0;
+}
+
+div.content {
+    margin: 8px;
+    padding: 0;
+}
+
+div.admonitionblock .title {
+    font-weight:bold;
+}
+
+div.admonitionblock {
+    margin:30px 0px;
+    color:#555;
+}
+
+div.admonitionblock td.icon {
+    width:30px;
+    padding-right:20px;
+    padding-left:20px;
+    text-transform:uppercase;
+    font-weight:bold;
+    color:#888;
+}
+
+div.listingblock .content {
+    border-left:4px solid #419eda;
+    padding:8px;
+
+    background: #faf7f8;
+
+    background-image: -moz-linear-gradient(left, right,
+                                           from(#faf7f8),
+                                           to(#ffffff));
+
+    background-image: -webkit-gradient(linear, left top, right bottom,
+                                       color-stop(0.00, #faf7f8),
+                                       color-stop(1.00, #ffffff));
+
+}
+
+div.listingblock .content pre {
+    margin:0;
+}
+
+div.literalblock .content {
+    margin-left:40px;
+}
+
+div.verseblock .content {
+    white-space:pre
+}
+
+div.sidebarblock {
+    margin-top: 1.5rem;
+    margin-bottom: 2rem;
+}
+
+div.sidebarblock > div.content {
+    border-left:4px solid #419eda;
+    background: #faf7f8;
+    padding:0 10px;
+    color:#222;
+    font-size:smaller;
+    line-height:1.5;
+    max-width: 720px;
+
+    background-image: -moz-linear-gradient(left, right,
+                                           from(#faf7f8),
+                                           to(#ffffff));
+
+    background-image: -webkit-gradient(linear, left top, right bottom,
+                                       color-stop(0.00, #faf7f8),
+                                       color-stop(1.00, #ffffff));
+
+}
+
+div.sidebarblock .title {
+    margin:10px 0;
+    font-weight:bold;
+    font-size: 1.1rem;
+    color:#442;
+}
+
+.quoteblock-content {
+    font-style:italic;
+    color:#444;
+    margin-left:40px;
+}
+
+.quoteblock-content .attribution {
+    font-style:normal;
+    text-align:right;
+    color:#000;
+}
+
+.exampleblock-content *:first-child { margin-top:0 }
+.exampleblock-content {
+    border-left:2px solid silver;
+    padding-left:8px;
+}
+
+#footnotes {
+    text-align:left;
+}
+
+#footnotes hr {
+    height: 1px;
+    color: #ccc;
+    width: 80%;
+}
+
+#footer {
+    font-size:11px;
+    color:#888;
+    margin-top:40px;
+    text-align: right;
+}
+
+.nav {
+    margin-bottom: 0;
+    padding-left: 0;
+    list-style: none;
+}
+
+.nav li {
+    line-height: 4rem;
+}
+
+.nav a {
+    font-size: 120%;
+    text-decoration: none;
+}
+
+.feedEkList .newsItem {
+    list-style-type: none;
+}
+
+.feedEkList .itemTitle {
+    font-size: large;
+}
+
+.feedEkList .itemDate {
+    font-size: smaller;
+}
+
+.feedEkList .itemContent {
+}
+
+ at media screen {
+    #toc {
+        position: fixed;
+        top: 120px;
+        left: 0;
+        margin: 0px;
+        font-size: small;
+        line-height: 1rem;
+    }
+
+    #toc a .monospaced {
+        font-size: small;
+        color:#419eda;
+    }
+
+    #toc a {
+        text-decoration: none;
+    }
+
+    #toc .toclevel1 {
+        font-weight: bold;
+        padding: 1px;
+    }
+
+    #toc .toclevel2 {
+        font-weight: bold;
+        font-style: italic;
+        margin-left: 8px;
+        color: #aaaaaa;
+        padding-left: 4px;
+        line-height: 1.2rem;
+    }
+
+    #toc .toclevel3 {
+        font-size: 0.8rem;
+        line-height: 1rem;
+        margin-left: 24px;
+        color: #aaaaaa;
+        padding-left: 4px;
+    }
+
+    #toctitle {
+        font-size:1.2rem;
+        line-height:1.1rem;
+        margin:20px 0;
+
+    }
+}
+
+ at media screen and (max-width: 900px) {
+    #toc {
+        display: none;
+    }
+
+    #container {
+        max-width: 720px;
+        margin: 0px auto;
+        text-align:left;
+    }
+
+}
+
+ at media screen and (min-width: 900px) {
+    #toc {
+        position: absolute;
+        overflow: hidden;
+        top: 120px;
+        left: 0px;
+        max-width: 240px;
+    }
+}
+
+
+/* pygments highlighting */
+
+.hll { background-color: #ffffcc }
+.c { color: #999988; font-style: italic } /* Comment */
+.err { color: #a61717; background-color: #e3d2d2 } /* Error */
+.k { color: #000000; font-weight: bold } /* Keyword */
+.o { color: #000000; font-weight: bold } /* Operator */
+.cm { color: #999988; font-style: italic } /* Comment.Multiline */
+.cp { color: #999999; font-weight: bold; font-style: italic } /* Comment.Preproc */
+.c1 { color: #999988; font-style: italic } /* Comment.Single */
+.cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
+.gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
+.ge { color: #000000; font-style: italic } /* Generic.Emph */
+.gr { color: #aa0000 } /* Generic.Error */
+.gh { color: #999999 } /* Generic.Heading */
+.gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
+.go { color: #888888 } /* Generic.Output */
+.gp { color: #555555 } /* Generic.Prompt */
+.gs { font-weight: bold } /* Generic.Strong */
+.gu { color: #aaaaaa } /* Generic.Subheading */
+.gt { color: #aa0000 } /* Generic.Traceback */
+.kc { color: #000000; font-weight: bold } /* Keyword.Constant */
+.kd { color: #000000; font-weight: bold } /* Keyword.Declaration */
+.kn { color: #000000; font-weight: bold } /* Keyword.Namespace */
+.kp { color: #000000; font-weight: bold } /* Keyword.Pseudo */
+.kr { color: #000000; font-weight: bold } /* Keyword.Reserved */
+.kt { color: #445588; font-weight: bold } /* Keyword.Type */
+.m { color: #009999 } /* Literal.Number */
+.s { color: #d01040 } /* Literal.String */
+.na { color: #008080 } /* Name.Attribute */
+.nb { color: #0086B3 } /* Name.Builtin */
+.nc { color: #445588; font-weight: bold } /* Name.Class */
+.no { color: #008080 } /* Name.Constant */
+.nd { color: #3c5d5d; font-weight: bold } /* Name.Decorator */
+.ni { color: #800080 } /* Name.Entity */
+.ne { color: #990000; font-weight: bold } /* Name.Exception */
+.nf { color: #990000; font-weight: bold } /* Name.Function */
+.nl { color: #990000; font-weight: bold } /* Name.Label */
+.nn { color: #555555 } /* Name.Namespace */
+.nt { color: #000080 } /* Name.Tag */
+.nv { color: #008080 } /* Name.Variable */
+.ow { color: #000000; font-weight: bold } /* Operator.Word */
+.w { color: #bbbbbb } /* Text.Whitespace */
+.mf { color: #009999 } /* Literal.Number.Float */
+.mh { color: #009999 } /* Literal.Number.Hex */
+.mi { color: #009999 } /* Literal.Number.Integer */
+.mo { color: #009999 } /* Literal.Number.Oct */
+.sb { color: #d01040 } /* Literal.String.Backtick */
+.sc { color: #d01040 } /* Literal.String.Char */
+.sd { color: #d01040 } /* Literal.String.Doc */
+.s2 { color: #d01040 } /* Literal.String.Double */
+.se { color: #d01040 } /* Literal.String.Escape */
+.sh { color: #d01040 } /* Literal.String.Heredoc */
+.si { color: #d01040 } /* Literal.String.Interpol */
+.sx { color: #d01040 } /* Literal.String.Other */
+.sr { color: #009926 } /* Literal.String.Regex */
+.s1 { color: #d01040 } /* Literal.String.Single */
+.ss { color: #990073 } /* Literal.String.Symbol */
+.bp { color: #999999 } /* Name.Builtin.Pseudo */
+.vc { color: #008080 } /* Name.Variable.Class */
+.vg { color: #008080 } /* Name.Variable.Global */
+.vi { color: #008080 } /* Name.Variable.Instance */
+.il { color: #009999 } /* Literal.Number.Integer.Long */
diff --git a/doc/website-v1/css/font-awesome.css b/doc/website-v1/css/font-awesome.css
new file mode 100644
index 0000000..048cff9
--- /dev/null
+++ b/doc/website-v1/css/font-awesome.css
@@ -0,0 +1,1338 @@
+/*!
+ *  Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome
+ *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+/* FONT PATH
+ * -------------------------- */
+ at font-face {
+  font-family: 'FontAwesome';
+  src: url('../fonts/fontawesome-webfont.eot?v=4.0.3');
+  src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');
+  font-weight: normal;
+  font-style: normal;
+}
+.fa {
+  display: inline-block;
+  font-family: FontAwesome;
+  font-style: normal;
+  font-weight: normal;
+  line-height: 1;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+/* makes the font 33% larger relative to the icon container */
+.fa-lg {
+  font-size: 1.3333333333333333em;
+  line-height: 0.75em;
+  vertical-align: -15%;
+}
+.fa-2x {
+  font-size: 2em;
+}
+.fa-3x {
+  font-size: 3em;
+}
+.fa-4x {
+  font-size: 4em;
+}
+.fa-5x {
+  font-size: 5em;
+}
+.fa-fw {
+  width: 1.2857142857142858em;
+  text-align: center;
+}
+.fa-ul {
+  padding-left: 0;
+  margin-left: 2.142857142857143em;
+  list-style-type: none;
+}
+.fa-ul > li {
+  position: relative;
+}
+.fa-li {
+  position: absolute;
+  left: -2.142857142857143em;
+  width: 2.142857142857143em;
+  top: 0.14285714285714285em;
+  text-align: center;
+}
+.fa-li.fa-lg {
+  left: -1.8571428571428572em;
+}
+.fa-border {
+  padding: .2em .25em .15em;
+  border: solid 0.08em #eeeeee;
+  border-radius: .1em;
+}
+.pull-right {
+  float: right;
+}
+.pull-left {
+  float: left;
+}
+.fa.pull-left {
+  margin-right: .3em;
+}
+.fa.pull-right {
+  margin-left: .3em;
+}
+.fa-spin {
+  -webkit-animation: spin 2s infinite linear;
+  -moz-animation: spin 2s infinite linear;
+  -o-animation: spin 2s infinite linear;
+  animation: spin 2s infinite linear;
+}
+ at -moz-keyframes spin {
+  0% {
+    -moz-transform: rotate(0deg);
+  }
+  100% {
+    -moz-transform: rotate(359deg);
+  }
+}
+ at -webkit-keyframes spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(359deg);
+  }
+}
+ at -o-keyframes spin {
+  0% {
+    -o-transform: rotate(0deg);
+  }
+  100% {
+    -o-transform: rotate(359deg);
+  }
+}
+ at -ms-keyframes spin {
+  0% {
+    -ms-transform: rotate(0deg);
+  }
+  100% {
+    -ms-transform: rotate(359deg);
+  }
+}
+ at keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(359deg);
+  }
+}
+.fa-rotate-90 {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
+  -webkit-transform: rotate(90deg);
+  -moz-transform: rotate(90deg);
+  -ms-transform: rotate(90deg);
+  -o-transform: rotate(90deg);
+  transform: rotate(90deg);
+}
+.fa-rotate-180 {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
+  -webkit-transform: rotate(180deg);
+  -moz-transform: rotate(180deg);
+  -ms-transform: rotate(180deg);
+  -o-transform: rotate(180deg);
+  transform: rotate(180deg);
+}
+.fa-rotate-270 {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
+  -webkit-transform: rotate(270deg);
+  -moz-transform: rotate(270deg);
+  -ms-transform: rotate(270deg);
+  -o-transform: rotate(270deg);
+  transform: rotate(270deg);
+}
+.fa-flip-horizontal {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);
+  -webkit-transform: scale(-1, 1);
+  -moz-transform: scale(-1, 1);
+  -ms-transform: scale(-1, 1);
+  -o-transform: scale(-1, 1);
+  transform: scale(-1, 1);
+}
+.fa-flip-vertical {
+  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);
+  -webkit-transform: scale(1, -1);
+  -moz-transform: scale(1, -1);
+  -ms-transform: scale(1, -1);
+  -o-transform: scale(1, -1);
+  transform: scale(1, -1);
+}
+.fa-stack {
+  position: relative;
+  display: inline-block;
+  width: 2em;
+  height: 2em;
+  line-height: 2em;
+  vertical-align: middle;
+}
+.fa-stack-1x,
+.fa-stack-2x {
+  position: absolute;
+  left: 0;
+  width: 100%;
+  text-align: center;
+}
+.fa-stack-1x {
+  line-height: inherit;
+}
+.fa-stack-2x {
+  font-size: 2em;
+}
+.fa-inverse {
+  color: #ffffff;
+}
+/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+   readers do not read off random characters that represent icons */
+.fa-glass:before {
+  content: "\f000";
+}
+.fa-music:before {
+  content: "\f001";
+}
+.fa-search:before {
+  content: "\f002";
+}
+.fa-envelope-o:before {
+  content: "\f003";
+}
+.fa-heart:before {
+  content: "\f004";
+}
+.fa-star:before {
+  content: "\f005";
+}
+.fa-star-o:before {
+  content: "\f006";
+}
+.fa-user:before {
+  content: "\f007";
+}
+.fa-film:before {
+  content: "\f008";
+}
+.fa-th-large:before {
+  content: "\f009";
+}
+.fa-th:before {
+  content: "\f00a";
+}
+.fa-th-list:before {
+  content: "\f00b";
+}
+.fa-check:before {
+  content: "\f00c";
+}
+.fa-times:before {
+  content: "\f00d";
+}
+.fa-search-plus:before {
+  content: "\f00e";
+}
+.fa-search-minus:before {
+  content: "\f010";
+}
+.fa-power-off:before {
+  content: "\f011";
+}
+.fa-signal:before {
+  content: "\f012";
+}
+.fa-gear:before,
+.fa-cog:before {
+  content: "\f013";
+}
+.fa-trash-o:before {
+  content: "\f014";
+}
+.fa-home:before {
+  content: "\f015";
+}
+.fa-file-o:before {
+  content: "\f016";
+}
+.fa-clock-o:before {
+  content: "\f017";
+}
+.fa-road:before {
+  content: "\f018";
+}
+.fa-download:before {
+  content: "\f019";
+}
+.fa-arrow-circle-o-down:before {
+  content: "\f01a";
+}
+.fa-arrow-circle-o-up:before {
+  content: "\f01b";
+}
+.fa-inbox:before {
+  content: "\f01c";
+}
+.fa-play-circle-o:before {
+  content: "\f01d";
+}
+.fa-rotate-right:before,
+.fa-repeat:before {
+  content: "\f01e";
+}
+.fa-refresh:before {
+  content: "\f021";
+}
+.fa-list-alt:before {
+  content: "\f022";
+}
+.fa-lock:before {
+  content: "\f023";
+}
+.fa-flag:before {
+  content: "\f024";
+}
+.fa-headphones:before {
+  content: "\f025";
+}
+.fa-volume-off:before {
+  content: "\f026";
+}
+.fa-volume-down:before {
+  content: "\f027";
+}
+.fa-volume-up:before {
+  content: "\f028";
+}
+.fa-qrcode:before {
+  content: "\f029";
+}
+.fa-barcode:before {
+  content: "\f02a";
+}
+.fa-tag:before {
+  content: "\f02b";
+}
+.fa-tags:before {
+  content: "\f02c";
+}
+.fa-book:before {
+  content: "\f02d";
+}
+.fa-bookmark:before {
+  content: "\f02e";
+}
+.fa-print:before {
+  content: "\f02f";
+}
+.fa-camera:before {
+  content: "\f030";
+}
+.fa-font:before {
+  content: "\f031";
+}
+.fa-bold:before {
+  content: "\f032";
+}
+.fa-italic:before {
+  content: "\f033";
+}
+.fa-text-height:before {
+  content: "\f034";
+}
+.fa-text-width:before {
+  content: "\f035";
+}
+.fa-align-left:before {
+  content: "\f036";
+}
+.fa-align-center:before {
+  content: "\f037";
+}
+.fa-align-right:before {
+  content: "\f038";
+}
+.fa-align-justify:before {
+  content: "\f039";
+}
+.fa-list:before {
+  content: "\f03a";
+}
+.fa-dedent:before,
+.fa-outdent:before {
+  content: "\f03b";
+}
+.fa-indent:before {
+  content: "\f03c";
+}
+.fa-video-camera:before {
+  content: "\f03d";
+}
+.fa-picture-o:before {
+  content: "\f03e";
+}
+.fa-pencil:before {
+  content: "\f040";
+}
+.fa-map-marker:before {
+  content: "\f041";
+}
+.fa-adjust:before {
+  content: "\f042";
+}
+.fa-tint:before {
+  content: "\f043";
+}
+.fa-edit:before,
+.fa-pencil-square-o:before {
+  content: "\f044";
+}
+.fa-share-square-o:before {
+  content: "\f045";
+}
+.fa-check-square-o:before {
+  content: "\f046";
+}
+.fa-arrows:before {
+  content: "\f047";
+}
+.fa-step-backward:before {
+  content: "\f048";
+}
+.fa-fast-backward:before {
+  content: "\f049";
+}
+.fa-backward:before {
+  content: "\f04a";
+}
+.fa-play:before {
+  content: "\f04b";
+}
+.fa-pause:before {
+  content: "\f04c";
+}
+.fa-stop:before {
+  content: "\f04d";
+}
+.fa-forward:before {
+  content: "\f04e";
+}
+.fa-fast-forward:before {
+  content: "\f050";
+}
+.fa-step-forward:before {
+  content: "\f051";
+}
+.fa-eject:before {
+  content: "\f052";
+}
+.fa-chevron-left:before {
+  content: "\f053";
+}
+.fa-chevron-right:before {
+  content: "\f054";
+}
+.fa-plus-circle:before {
+  content: "\f055";
+}
+.fa-minus-circle:before {
+  content: "\f056";
+}
+.fa-times-circle:before {
+  content: "\f057";
+}
+.fa-check-circle:before {
+  content: "\f058";
+}
+.fa-question-circle:before {
+  content: "\f059";
+}
+.fa-info-circle:before {
+  content: "\f05a";
+}
+.fa-crosshairs:before {
+  content: "\f05b";
+}
+.fa-times-circle-o:before {
+  content: "\f05c";
+}
+.fa-check-circle-o:before {
+  content: "\f05d";
+}
+.fa-ban:before {
+  content: "\f05e";
+}
+.fa-arrow-left:before {
+  content: "\f060";
+}
+.fa-arrow-right:before {
+  content: "\f061";
+}
+.fa-arrow-up:before {
+  content: "\f062";
+}
+.fa-arrow-down:before {
+  content: "\f063";
+}
+.fa-mail-forward:before,
+.fa-share:before {
+  content: "\f064";
+}
+.fa-expand:before {
+  content: "\f065";
+}
+.fa-compress:before {
+  content: "\f066";
+}
+.fa-plus:before {
+  content: "\f067";
+}
+.fa-minus:before {
+  content: "\f068";
+}
+.fa-asterisk:before {
+  content: "\f069";
+}
+.fa-exclamation-circle:before {
+  content: "\f06a";
+}
+.fa-gift:before {
+  content: "\f06b";
+}
+.fa-leaf:before {
+  content: "\f06c";
+}
+.fa-fire:before {
+  content: "\f06d";
+}
+.fa-eye:before {
+  content: "\f06e";
+}
+.fa-eye-slash:before {
+  content: "\f070";
+}
+.fa-warning:before,
+.fa-exclamation-triangle:before {
+  content: "\f071";
+}
+.fa-plane:before {
+  content: "\f072";
+}
+.fa-calendar:before {
+  content: "\f073";
+}
+.fa-random:before {
+  content: "\f074";
+}
+.fa-comment:before {
+  content: "\f075";
+}
+.fa-magnet:before {
+  content: "\f076";
+}
+.fa-chevron-up:before {
+  content: "\f077";
+}
+.fa-chevron-down:before {
+  content: "\f078";
+}
+.fa-retweet:before {
+  content: "\f079";
+}
+.fa-shopping-cart:before {
+  content: "\f07a";
+}
+.fa-folder:before {
+  content: "\f07b";
+}
+.fa-folder-open:before {
+  content: "\f07c";
+}
+.fa-arrows-v:before {
+  content: "\f07d";
+}
+.fa-arrows-h:before {
+  content: "\f07e";
+}
+.fa-bar-chart-o:before {
+  content: "\f080";
+}
+.fa-twitter-square:before {
+  content: "\f081";
+}
+.fa-facebook-square:before {
+  content: "\f082";
+}
+.fa-camera-retro:before {
+  content: "\f083";
+}
+.fa-key:before {
+  content: "\f084";
+}
+.fa-gears:before,
+.fa-cogs:before {
+  content: "\f085";
+}
+.fa-comments:before {
+  content: "\f086";
+}
+.fa-thumbs-o-up:before {
+  content: "\f087";
+}
+.fa-thumbs-o-down:before {
+  content: "\f088";
+}
+.fa-star-half:before {
+  content: "\f089";
+}
+.fa-heart-o:before {
+  content: "\f08a";
+}
+.fa-sign-out:before {
+  content: "\f08b";
+}
+.fa-linkedin-square:before {
+  content: "\f08c";
+}
+.fa-thumb-tack:before {
+  content: "\f08d";
+}
+.fa-external-link:before {
+  content: "\f08e";
+}
+.fa-sign-in:before {
+  content: "\f090";
+}
+.fa-trophy:before {
+  content: "\f091";
+}
+.fa-github-square:before {
+  content: "\f092";
+}
+.fa-upload:before {
+  content: "\f093";
+}
+.fa-lemon-o:before {
+  content: "\f094";
+}
+.fa-phone:before {
+  content: "\f095";
+}
+.fa-square-o:before {
+  content: "\f096";
+}
+.fa-bookmark-o:before {
+  content: "\f097";
+}
+.fa-phone-square:before {
+  content: "\f098";
+}
+.fa-twitter:before {
+  content: "\f099";
+}
+.fa-facebook:before {
+  content: "\f09a";
+}
+.fa-github:before {
+  content: "\f09b";
+}
+.fa-unlock:before {
+  content: "\f09c";
+}
+.fa-credit-card:before {
+  content: "\f09d";
+}
+.fa-rss:before {
+  content: "\f09e";
+}
+.fa-hdd-o:before {
+  content: "\f0a0";
+}
+.fa-bullhorn:before {
+  content: "\f0a1";
+}
+.fa-bell:before {
+  content: "\f0f3";
+}
+.fa-certificate:before {
+  content: "\f0a3";
+}
+.fa-hand-o-right:before {
+  content: "\f0a4";
+}
+.fa-hand-o-left:before {
+  content: "\f0a5";
+}
+.fa-hand-o-up:before {
+  content: "\f0a6";
+}
+.fa-hand-o-down:before {
+  content: "\f0a7";
+}
+.fa-arrow-circle-left:before {
+  content: "\f0a8";
+}
+.fa-arrow-circle-right:before {
+  content: "\f0a9";
+}
+.fa-arrow-circle-up:before {
+  content: "\f0aa";
+}
+.fa-arrow-circle-down:before {
+  content: "\f0ab";
+}
+.fa-globe:before {
+  content: "\f0ac";
+}
+.fa-wrench:before {
+  content: "\f0ad";
+}
+.fa-tasks:before {
+  content: "\f0ae";
+}
+.fa-filter:before {
+  content: "\f0b0";
+}
+.fa-briefcase:before {
+  content: "\f0b1";
+}
+.fa-arrows-alt:before {
+  content: "\f0b2";
+}
+.fa-group:before,
+.fa-users:before {
+  content: "\f0c0";
+}
+.fa-chain:before,
+.fa-link:before {
+  content: "\f0c1";
+}
+.fa-cloud:before {
+  content: "\f0c2";
+}
+.fa-flask:before {
+  content: "\f0c3";
+}
+.fa-cut:before,
+.fa-scissors:before {
+  content: "\f0c4";
+}
+.fa-copy:before,
+.fa-files-o:before {
+  content: "\f0c5";
+}
+.fa-paperclip:before {
+  content: "\f0c6";
+}
+.fa-save:before,
+.fa-floppy-o:before {
+  content: "\f0c7";
+}
+.fa-square:before {
+  content: "\f0c8";
+}
+.fa-bars:before {
+  content: "\f0c9";
+}
+.fa-list-ul:before {
+  content: "\f0ca";
+}
+.fa-list-ol:before {
+  content: "\f0cb";
+}
+.fa-strikethrough:before {
+  content: "\f0cc";
+}
+.fa-underline:before {
+  content: "\f0cd";
+}
+.fa-table:before {
+  content: "\f0ce";
+}
+.fa-magic:before {
+  content: "\f0d0";
+}
+.fa-truck:before {
+  content: "\f0d1";
+}
+.fa-pinterest:before {
+  content: "\f0d2";
+}
+.fa-pinterest-square:before {
+  content: "\f0d3";
+}
+.fa-google-plus-square:before {
+  content: "\f0d4";
+}
+.fa-google-plus:before {
+  content: "\f0d5";
+}
+.fa-money:before {
+  content: "\f0d6";
+}
+.fa-caret-down:before {
+  content: "\f0d7";
+}
+.fa-caret-up:before {
+  content: "\f0d8";
+}
+.fa-caret-left:before {
+  content: "\f0d9";
+}
+.fa-caret-right:before {
+  content: "\f0da";
+}
+.fa-columns:before {
+  content: "\f0db";
+}
+.fa-unsorted:before,
+.fa-sort:before {
+  content: "\f0dc";
+}
+.fa-sort-down:before,
+.fa-sort-asc:before {
+  content: "\f0dd";
+}
+.fa-sort-up:before,
+.fa-sort-desc:before {
+  content: "\f0de";
+}
+.fa-envelope:before {
+  content: "\f0e0";
+}
+.fa-linkedin:before {
+  content: "\f0e1";
+}
+.fa-rotate-left:before,
+.fa-undo:before {
+  content: "\f0e2";
+}
+.fa-legal:before,
+.fa-gavel:before {
+  content: "\f0e3";
+}
+.fa-dashboard:before,
+.fa-tachometer:before {
+  content: "\f0e4";
+}
+.fa-comment-o:before {
+  content: "\f0e5";
+}
+.fa-comments-o:before {
+  content: "\f0e6";
+}
+.fa-flash:before,
+.fa-bolt:before {
+  content: "\f0e7";
+}
+.fa-sitemap:before {
+  content: "\f0e8";
+}
+.fa-umbrella:before {
+  content: "\f0e9";
+}
+.fa-paste:before,
+.fa-clipboard:before {
+  content: "\f0ea";
+}
+.fa-lightbulb-o:before {
+  content: "\f0eb";
+}
+.fa-exchange:before {
+  content: "\f0ec";
+}
+.fa-cloud-download:before {
+  content: "\f0ed";
+}
+.fa-cloud-upload:before {
+  content: "\f0ee";
+}
+.fa-user-md:before {
+  content: "\f0f0";
+}
+.fa-stethoscope:before {
+  content: "\f0f1";
+}
+.fa-suitcase:before {
+  content: "\f0f2";
+}
+.fa-bell-o:before {
+  content: "\f0a2";
+}
+.fa-coffee:before {
+  content: "\f0f4";
+}
+.fa-cutlery:before {
+  content: "\f0f5";
+}
+.fa-file-text-o:before {
+  content: "\f0f6";
+}
+.fa-building-o:before {
+  content: "\f0f7";
+}
+.fa-hospital-o:before {
+  content: "\f0f8";
+}
+.fa-ambulance:before {
+  content: "\f0f9";
+}
+.fa-medkit:before {
+  content: "\f0fa";
+}
+.fa-fighter-jet:before {
+  content: "\f0fb";
+}
+.fa-beer:before {
+  content: "\f0fc";
+}
+.fa-h-square:before {
+  content: "\f0fd";
+}
+.fa-plus-square:before {
+  content: "\f0fe";
+}
+.fa-angle-double-left:before {
+  content: "\f100";
+}
+.fa-angle-double-right:before {
+  content: "\f101";
+}
+.fa-angle-double-up:before {
+  content: "\f102";
+}
+.fa-angle-double-down:before {
+  content: "\f103";
+}
+.fa-angle-left:before {
+  content: "\f104";
+}
+.fa-angle-right:before {
+  content: "\f105";
+}
+.fa-angle-up:before {
+  content: "\f106";
+}
+.fa-angle-down:before {
+  content: "\f107";
+}
+.fa-desktop:before {
+  content: "\f108";
+}
+.fa-laptop:before {
+  content: "\f109";
+}
+.fa-tablet:before {
+  content: "\f10a";
+}
+.fa-mobile-phone:before,
+.fa-mobile:before {
+  content: "\f10b";
+}
+.fa-circle-o:before {
+  content: "\f10c";
+}
+.fa-quote-left:before {
+  content: "\f10d";
+}
+.fa-quote-right:before {
+  content: "\f10e";
+}
+.fa-spinner:before {
+  content: "\f110";
+}
+.fa-circle:before {
+  content: "\f111";
+}
+.fa-mail-reply:before,
+.fa-reply:before {
+  content: "\f112";
+}
+.fa-github-alt:before {
+  content: "\f113";
+}
+.fa-folder-o:before {
+  content: "\f114";
+}
+.fa-folder-open-o:before {
+  content: "\f115";
+}
+.fa-smile-o:before {
+  content: "\f118";
+}
+.fa-frown-o:before {
+  content: "\f119";
+}
+.fa-meh-o:before {
+  content: "\f11a";
+}
+.fa-gamepad:before {
+  content: "\f11b";
+}
+.fa-keyboard-o:before {
+  content: "\f11c";
+}
+.fa-flag-o:before {
+  content: "\f11d";
+}
+.fa-flag-checkered:before {
+  content: "\f11e";
+}
+.fa-terminal:before {
+  content: "\f120";
+}
+.fa-code:before {
+  content: "\f121";
+}
+.fa-reply-all:before {
+  content: "\f122";
+}
+.fa-mail-reply-all:before {
+  content: "\f122";
+}
+.fa-star-half-empty:before,
+.fa-star-half-full:before,
+.fa-star-half-o:before {
+  content: "\f123";
+}
+.fa-location-arrow:before {
+  content: "\f124";
+}
+.fa-crop:before {
+  content: "\f125";
+}
+.fa-code-fork:before {
+  content: "\f126";
+}
+.fa-unlink:before,
+.fa-chain-broken:before {
+  content: "\f127";
+}
+.fa-question:before {
+  content: "\f128";
+}
+.fa-info:before {
+  content: "\f129";
+}
+.fa-exclamation:before {
+  content: "\f12a";
+}
+.fa-superscript:before {
+  content: "\f12b";
+}
+.fa-subscript:before {
+  content: "\f12c";
+}
+.fa-eraser:before {
+  content: "\f12d";
+}
+.fa-puzzle-piece:before {
+  content: "\f12e";
+}
+.fa-microphone:before {
+  content: "\f130";
+}
+.fa-microphone-slash:before {
+  content: "\f131";
+}
+.fa-shield:before {
+  content: "\f132";
+}
+.fa-calendar-o:before {
+  content: "\f133";
+}
+.fa-fire-extinguisher:before {
+  content: "\f134";
+}
+.fa-rocket:before {
+  content: "\f135";
+}
+.fa-maxcdn:before {
+  content: "\f136";
+}
+.fa-chevron-circle-left:before {
+  content: "\f137";
+}
+.fa-chevron-circle-right:before {
+  content: "\f138";
+}
+.fa-chevron-circle-up:before {
+  content: "\f139";
+}
+.fa-chevron-circle-down:before {
+  content: "\f13a";
+}
+.fa-html5:before {
+  content: "\f13b";
+}
+.fa-css3:before {
+  content: "\f13c";
+}
+.fa-anchor:before {
+  content: "\f13d";
+}
+.fa-unlock-alt:before {
+  content: "\f13e";
+}
+.fa-bullseye:before {
+  content: "\f140";
+}
+.fa-ellipsis-h:before {
+  content: "\f141";
+}
+.fa-ellipsis-v:before {
+  content: "\f142";
+}
+.fa-rss-square:before {
+  content: "\f143";
+}
+.fa-play-circle:before {
+  content: "\f144";
+}
+.fa-ticket:before {
+  content: "\f145";
+}
+.fa-minus-square:before {
+  content: "\f146";
+}
+.fa-minus-square-o:before {
+  content: "\f147";
+}
+.fa-level-up:before {
+  content: "\f148";
+}
+.fa-level-down:before {
+  content: "\f149";
+}
+.fa-check-square:before {
+  content: "\f14a";
+}
+.fa-pencil-square:before {
+  content: "\f14b";
+}
+.fa-external-link-square:before {
+  content: "\f14c";
+}
+.fa-share-square:before {
+  content: "\f14d";
+}
+.fa-compass:before {
+  content: "\f14e";
+}
+.fa-toggle-down:before,
+.fa-caret-square-o-down:before {
+  content: "\f150";
+}
+.fa-toggle-up:before,
+.fa-caret-square-o-up:before {
+  content: "\f151";
+}
+.fa-toggle-right:before,
+.fa-caret-square-o-right:before {
+  content: "\f152";
+}
+.fa-euro:before,
+.fa-eur:before {
+  content: "\f153";
+}
+.fa-gbp:before {
+  content: "\f154";
+}
+.fa-dollar:before,
+.fa-usd:before {
+  content: "\f155";
+}
+.fa-rupee:before,
+.fa-inr:before {
+  content: "\f156";
+}
+.fa-cny:before,
+.fa-rmb:before,
+.fa-yen:before,
+.fa-jpy:before {
+  content: "\f157";
+}
+.fa-ruble:before,
+.fa-rouble:before,
+.fa-rub:before {
+  content: "\f158";
+}
+.fa-won:before,
+.fa-krw:before {
+  content: "\f159";
+}
+.fa-bitcoin:before,
+.fa-btc:before {
+  content: "\f15a";
+}
+.fa-file:before {
+  content: "\f15b";
+}
+.fa-file-text:before {
+  content: "\f15c";
+}
+.fa-sort-alpha-asc:before {
+  content: "\f15d";
+}
+.fa-sort-alpha-desc:before {
+  content: "\f15e";
+}
+.fa-sort-amount-asc:before {
+  content: "\f160";
+}
+.fa-sort-amount-desc:before {
+  content: "\f161";
+}
+.fa-sort-numeric-asc:before {
+  content: "\f162";
+}
+.fa-sort-numeric-desc:before {
+  content: "\f163";
+}
+.fa-thumbs-up:before {
+  content: "\f164";
+}
+.fa-thumbs-down:before {
+  content: "\f165";
+}
+.fa-youtube-square:before {
+  content: "\f166";
+}
+.fa-youtube:before {
+  content: "\f167";
+}
+.fa-xing:before {
+  content: "\f168";
+}
+.fa-xing-square:before {
+  content: "\f169";
+}
+.fa-youtube-play:before {
+  content: "\f16a";
+}
+.fa-dropbox:before {
+  content: "\f16b";
+}
+.fa-stack-overflow:before {
+  content: "\f16c";
+}
+.fa-instagram:before {
+  content: "\f16d";
+}
+.fa-flickr:before {
+  content: "\f16e";
+}
+.fa-adn:before {
+  content: "\f170";
+}
+.fa-bitbucket:before {
+  content: "\f171";
+}
+.fa-bitbucket-square:before {
+  content: "\f172";
+}
+.fa-tumblr:before {
+  content: "\f173";
+}
+.fa-tumblr-square:before {
+  content: "\f174";
+}
+.fa-long-arrow-down:before {
+  content: "\f175";
+}
+.fa-long-arrow-up:before {
+  content: "\f176";
+}
+.fa-long-arrow-left:before {
+  content: "\f177";
+}
+.fa-long-arrow-right:before {
+  content: "\f178";
+}
+.fa-apple:before {
+  content: "\f179";
+}
+.fa-windows:before {
+  content: "\f17a";
+}
+.fa-android:before {
+  content: "\f17b";
+}
+.fa-linux:before {
+  content: "\f17c";
+}
+.fa-dribbble:before {
+  content: "\f17d";
+}
+.fa-skype:before {
+  content: "\f17e";
+}
+.fa-foursquare:before {
+  content: "\f180";
+}
+.fa-trello:before {
+  content: "\f181";
+}
+.fa-female:before {
+  content: "\f182";
+}
+.fa-male:before {
+  content: "\f183";
+}
+.fa-gittip:before {
+  content: "\f184";
+}
+.fa-sun-o:before {
+  content: "\f185";
+}
+.fa-moon-o:before {
+  content: "\f186";
+}
+.fa-archive:before {
+  content: "\f187";
+}
+.fa-bug:before {
+  content: "\f188";
+}
+.fa-vk:before {
+  content: "\f189";
+}
+.fa-weibo:before {
+  content: "\f18a";
+}
+.fa-renren:before {
+  content: "\f18b";
+}
+.fa-pagelines:before {
+  content: "\f18c";
+}
+.fa-stack-exchange:before {
+  content: "\f18d";
+}
+.fa-arrow-circle-o-right:before {
+  content: "\f18e";
+}
+.fa-arrow-circle-o-left:before {
+  content: "\f190";
+}
+.fa-toggle-left:before,
+.fa-caret-square-o-left:before {
+  content: "\f191";
+}
+.fa-dot-circle-o:before {
+  content: "\f192";
+}
+.fa-wheelchair:before {
+  content: "\f193";
+}
+.fa-vimeo-square:before {
+  content: "\f194";
+}
+.fa-turkish-lira:before,
+.fa-try:before {
+  content: "\f195";
+}
+.fa-plus-square-o:before {
+  content: "\f196";
+}
diff --git a/doc/website-v1/css/font-awesome.min.css b/doc/website-v1/css/font-awesome.min.css
new file mode 100644
index 0000000..449d6ac
--- /dev/null
+++ b/doc/website-v1/css/font-awesome.min.css
@@ -0,0 +1,4 @@
+/*!
+ *  Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome
+ *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:norm [...]
\ No newline at end of file
diff --git a/doc/website-v1/development.txt b/doc/website-v1/development.txt
new file mode 100644
index 0000000..6b4e603
--- /dev/null
+++ b/doc/website-v1/development.txt
@@ -0,0 +1,62 @@
+= Development =
+
+`crmsh` is a free software project, and is open to contributors.
+Patches, comments, documentation, testing and so on are
+all very much welcome!
+
+== Tools ==
+
+++++
+<ul class="nav">
+<li><a href="https://github.com/ClusterLabs/crmsh"><i class="fa fa-archive fa-3x fa-fw"></i> Source Repository</a></li>
+<li><a href="http://clusterlabs.org/mailman/listinfo/users"><i class="fa fa-envelope fa-3x fa-fw"></i> Mailing List</a></li>
+<li><a href="https://github.com/ClusterLabs/crmsh/issues"><i class="fa fa-bug fa-3x fa-fw"></i> Issue Tracker</a></li>
+<li><a href="irc://freenode.net/#clusterlabs"><i class="fa fa-comments fa-3x fa-fw"></i> IRC: #clusterlabs on Freenode</a></li>
+<li><a href="https://github.com/ClusterLabs/crmsh/commits/master.atom"><i class="fa fa-rss fa-3x fa-fw"></i> Atom feed</a></li>
+</ul>
+++++
+
+== Get the source ==
+
+The source code for `crmsh` is kept in a
+http://git-scm.com/[git] repository
+hosted at https://github.com[github]. Use +git+ to get a working copy:
+
+----
+git clone https://github.com/ClusterLabs/crmsh.git
+----
+
+`crmsh` needs the development headers for link:http://hg.linux-ha.org/glue[cluster-glue] and 
+link:https://github.com/ClusterLabs/pacemaker[pacemaker] when
+installing. On openSUSE, these are packaged as `libglue-devel` and
+`libpacemaker-devel`.
+
+.Building
+----
+./autogen.sh
+./configure
+make
+make install
+----
+
+=== Tests ===
+
+The unit tests for `crmsh` require +nose+ to run. On most distributions, this can be installed
+by installing the package +python-nose+, or using +pip+.
+
+To run the unit test suite, go to the source code directory of `crmsh`
+and call:
+
+----
+./test/unit-tests.sh
+----
+
+`crmsh` also comes with a comprehensive regression test suite. The regression tests need
+to run after installation, on a system which has both crmsh and pacemaker installed.
+
+To execute the tests, call:
+
+----
+/usr/share/crmsh/tests/regression.sh
+cat crmtestout/regression.out
+----
diff --git a/doc/website-v1/documentation.txt b/doc/website-v1/documentation.txt
new file mode 100644
index 0000000..4f6a72b
--- /dev/null
+++ b/doc/website-v1/documentation.txt
@@ -0,0 +1,43 @@
+= Documentation =
+
+`crmsh` begun as a bundled interface to the Pacemaker cluster manager,
+but has grown beyond simply configuring Pacemaker to handling many
+more aspects of the Linux HA stack. It also serves as the backend for
+the Hawk web interface.
+
+The main documentation for `crmsh` comes in the form of the 
+_manual_, which is the same help as found using the `help`
+command in the interactive shell.
+
+Beyond this, we've also written a couple of guides and other documents
+that will hopefully make using the shell as simple and painless as
+possible.
+
+== Documents ==
+
+* link:/man[Manual (Development)], link:/man-2.0[(Release 2.x)], link:/man-1.2[(Release 1.2)]
+* link:/installation[Installation]
+* link:/start-guide[Getting Started]
+* link:/rsctest-guide[Resource Testing Guide]
+* link:/configuration[Configuration]
+* link:/scripts[Cluster scripts]
+* link:/faq[Frequently Asked Questions]
+
+== External documentation ==
+
+The SUSE
+https://www.suse.com/documentation/sle_ha/book_sleha/?page=/documentation/sle_ha/book_sleha/data/book_sleha.html[High
+Availability Guide] provides a guide to
+installing and configuring a complete cluster solution including both
+the `crm` shell and Hawk, the web GUI which uses the `crm` shell as
+its backend.
+
+For more information on Pacemaker in general, see the
+http://clusterlabs.org/doc/[Pacemaker documentation] at `clusterlabs.org`.
+
+On the Pacemaker website, there is a set of guides to configuring
+Pacemaker using `crmsh`. To find these guides, go to the documentation
+page and search for `crmsh`.
+
+For details on command line usage, see the link:/man[Manual].
+
diff --git a/doc/website-v1/faq.txt b/doc/website-v1/faq.txt
new file mode 100644
index 0000000..c9c5d02
--- /dev/null
+++ b/doc/website-v1/faq.txt
@@ -0,0 +1,60 @@
+= Frequently Asked Questions
+
+== What is the crm shell?
+
+The `crm` shell is a command-line interface to the Pacemaker cluster
+resource management stack. If that doesn't make any sense to you, the
+easiest way to get up to speed is to go to the
+http://clusterlabs.org/[Pacemaker] website and read more about what it
+does there.
+
+The `crm` shell provides a simpler interface to configuring Pacemaker
+than manipulating the XML of the CIB (Cluster Information Base)
+directly. With its command-line style interface, changes to the
+cluster can be performed quickly and painlessly. It also works as a
+scripting tool, allowing more complicated changes to be applied to the
+cluster.
+
+The `crm` shell also functions as a management console, providing a
+unified interface to a number of other auxilliary tools related to
+Pacemaker and cluster management.
+
+== What distributions does the shell run on?
+
+Many distributions provide packages for the `crm` shell in their
+respective package repositories. The best and safest way to obtain the
+`crm` shell is via the distribution repositories, so look there first.
+
+The intention is for the `crm` shell to work well on all the major
+distributions. Pre-built packages are provided for the
+following distros:
+
+    * openSUSE
+    * Fedora
+    * CentOS
+    * Red Hat Linux
+
+More information can be found on the
+link:/documentation#_installation[Documentation] page.
+
+== Didn't crm use to be part of Pacemaker?
+
+Yes, initially, the `crm` shell was distributed as part of the
+Pacemaker project. It was split into its own, separate project in
+2011.
+
+A common misconception is that `crm` has been replaced by `pcs`
+(available at https://github.com/feist/pcs[github.com/feist/pcs]). `pcs`
+is an alternative command line interface similar to `crm`. Both
+projects are being actively developed, with slightly different
+goals. Our recommendation is to use whatever shell your distribution
+of choice comes with and supports, unless you have a particular
+preference or are on a distribution which doesn't bundle either. In
+that case, we are obviously biased towards one of the available
+choices. ;)
+
+== Command-line is well and good, but is there a web interface?
+
+Yes! Take a look at https://github.com/ClusterLabs/hawk[Hawk].
+
+Hawk uses the `crm` shell as its backend to interact with the cluster.
diff --git a/doc/website-v1/fonts/FontAwesome.otf b/doc/website-v1/fonts/FontAwesome.otf
new file mode 100644
index 0000000..8b0f54e
Binary files /dev/null and b/doc/website-v1/fonts/FontAwesome.otf differ
diff --git a/doc/website-v1/fonts/fontawesome-webfont.eot b/doc/website-v1/fonts/fontawesome-webfont.eot
new file mode 100755
index 0000000..7c79c6a
Binary files /dev/null and b/doc/website-v1/fonts/fontawesome-webfont.eot differ
diff --git a/doc/website-v1/fonts/fontawesome-webfont.svg b/doc/website-v1/fonts/fontawesome-webfont.svg
new file mode 100755
index 0000000..45fdf33
--- /dev/null
+++ b/doc/website-v1/fonts/fontawesome-webfont.svg
@@ -0,0 +1,414 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="fontawesomeregular" horiz-adv-x="1536" >
+<font-face units-per-em="1792" ascent="1536" descent="-256" />
+<missing-glyph horiz-adv-x="448" />
+<glyph unicode=" "  horiz-adv-x="448" />
+<glyph unicode="&#x09;" horiz-adv-x="448" />
+<glyph unicode="&#xa0;" horiz-adv-x="448" />
+<glyph unicode="&#xa8;" horiz-adv-x="1792" />
+<glyph unicode="&#xa9;" horiz-adv-x="1792" />
+<glyph unicode="&#xae;" horiz-adv-x="1792" />
+<glyph unicode="&#xb4;" horiz-adv-x="1792" />
+<glyph unicode="&#xc6;" horiz-adv-x="1792" />
+<glyph unicode="&#x2000;" horiz-adv-x="768" />
+<glyph unicode="&#x2001;" />
+<glyph unicode="&#x2002;" horiz-adv-x="768" />
+<glyph unicode="&#x2003;" />
+<glyph unicode="&#x2004;" horiz-adv-x="512" />
+<glyph unicode="&#x2005;" horiz-adv-x="384" />
+<glyph unicode="&#x2006;" horiz-adv-x="256" />
+<glyph unicode="&#x2007;" horiz-adv-x="256" />
+<glyph unicode="&#x2008;" horiz-adv-x="192" />
+<glyph unicode="&#x2009;" horiz-adv-x="307" />
+<glyph unicode="&#x200a;" horiz-adv-x="85" />
+<glyph unicode="&#x202f;" horiz-adv-x="307" />
+<glyph unicode="&#x205f;" horiz-adv-x="384" />
+<glyph unicode="&#x2122;" horiz-adv-x="1792" />
+<glyph unicode="&#x221e;" horiz-adv-x="1792" />
+<glyph unicode="&#x2260;" horiz-adv-x="1792" />
+<glyph unicode="&#xe000;" horiz-adv-x="500" d="M0 0z" />
+<glyph unicode="&#xf000;" horiz-adv-x="1792" d="M1699 1350q0 -35 -43 -78l-632 -632v-768h320q26 0 45 -19t19 -45t-19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45t45 19h320v768l-632 632q-43 43 -43 78q0 23 18 36.5t38 17.5t43 4h1408q23 0 43 -4t38 -17.5t18 -36.5z" />
+<glyph unicode="&#xf001;" d="M1536 1312v-1120q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v537l-768 -237v-709q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89 t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v967q0 31 19 56.5t49 35.5l832 256q12 4 28 4q40 0 68 -28t28 -68z" />
+<glyph unicode="&#xf002;" horiz-adv-x="1664" d="M1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -52 -38 -90t-90 -38q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5 t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" />
+<glyph unicode="&#xf003;" horiz-adv-x="1792" d="M1664 32v768q-32 -36 -69 -66q-268 -206 -426 -338q-51 -43 -83 -67t-86.5 -48.5t-102.5 -24.5h-1h-1q-48 0 -102.5 24.5t-86.5 48.5t-83 67q-158 132 -426 338q-37 30 -69 66v-768q0 -13 9.5 -22.5t22.5 -9.5h1472q13 0 22.5 9.5t9.5 22.5zM1664 1083v11v13.5t-0.5 13 t-3 12.5t-5.5 9t-9 7.5t-14 2.5h-1472q-13 0 -22.5 -9.5t-9.5 -22.5q0 -168 147 -284q193 -152 401 -317q6 -5 35 -29.5t46 -37.5t44.5 -31.5t50.5 -27.5t43 -9h1h1q20 0 43 9t50.5 27.5t44.5 31.5t46 37.5t35 [...]
+<glyph unicode="&#xf004;" horiz-adv-x="1792" d="M896 -128q-26 0 -44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124t127 -344q0 -221 -229 -450l-623 -600 q-18 -18 -44 -18z" />
+<glyph unicode="&#xf005;" horiz-adv-x="1664" d="M1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -21 -10.5 -35.5t-30.5 -14.5q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455 l502 -73q56 -9 56 -46z" />
+<glyph unicode="&#xf006;" horiz-adv-x="1664" d="M1137 532l306 297l-422 62l-189 382l-189 -382l-422 -62l306 -297l-73 -421l378 199l377 -199zM1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -50 -41 -50q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500 l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455l502 -73q56 -9 56 -46z" />
+<glyph unicode="&#xf007;" horiz-adv-x="1408" d="M1408 131q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q9 0 42 -21.5t74.5 -48t108 -48t133.5 -21.5t133.5 21.5t108 48t74.5 48t42 21.5q61 0 111.5 -20t85.5 -53.5t62 -81 t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5z" />
+<glyph unicode="&#xf008;" horiz-adv-x="1920" d="M384 -64v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 320v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 704v128q0 26 -19 45t-45 19h-128 q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1408 -64v512q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-512q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM384 1088v128q0 26 -19 [...]
+<glyph unicode="&#xf009;" horiz-adv-x="1664" d="M768 512v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM768 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 512v-384q0 -52 -38 -90t-90 -38 h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" />
+<glyph unicode="&#xf00a;" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 288v-192q0 -40 -28 -68t-68 -28h-320 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 800v-192q0 -40  [...]
+<glyph unicode="&#xf00b;" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 288v-192q0 -40 -28 -68t-68 -28h-960 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h960q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 800v-192q0 -40  [...]
+<glyph unicode="&#xf00c;" horiz-adv-x="1792" d="M1671 970q0 -40 -28 -68l-724 -724l-136 -136q-28 -28 -68 -28t-68 28l-136 136l-362 362q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -295l656 657q28 28 68 28t68 -28l136 -136q28 -28 28 -68z" />
+<glyph unicode="&#xf00d;" horiz-adv-x="1408" d="M1298 214q0 -40 -28 -68l-136 -136q-28 -28 -68 -28t-68 28l-294 294l-294 -294q-28 -28 -68 -28t-68 28l-136 136q-28 28 -28 68t28 68l294 294l-294 294q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -294l294 294q28 28 68 28t68 -28l136 -136q28 -28 28 -68 t-28 -68l-294 -294l294 -294q28 -28 28 -68z" />
+<glyph unicode="&#xf00e;" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-224q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v224h-224q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h224v224q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5v-224h224 q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343  [...]
+<glyph unicode="&#xf010;" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-576q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h576q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5z M1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150  [...]
+<glyph unicode="&#xf011;" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61t-298 61t-245 164t-164 245t-61 298q0 182 80.5 343t226.5 270q43 32 95.5 25t83.5 -50q32 -42 24.5 -94.5t-49.5 -84.5q-98 -74 -151.5 -181t-53.5 -228q0 -104 40.5 -198.5t109.5 -163.5t163.5 -109.5 t198.5 -40.5t198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5q0 121 -53.5 228t-151.5 181q-42 32 -49.5 84.5t24.5 94.5q31 43 84 50t95 -25q146 -109 226.5 -270t80.5 -343zM896 1408v-640q0 -52 -38 -90t-90 -38t-90 38t-38 90v640q0  [...]
+<glyph unicode="&#xf012;" horiz-adv-x="1792" d="M256 96v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM640 224v-320q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1024 480v-576q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23 v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1408 864v-960q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 1376v-1472q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t [...]
+<glyph unicode="&#xf013;" d="M1024 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1536 749v-222q0 -12 -8 -23t-20 -13l-185 -28q-19 -54 -39 -91q35 -50 107 -138q10 -12 10 -25t-9 -23q-27 -37 -99 -108t-94 -71q-12 0 -26 9l-138 108q-44 -23 -91 -38 q-16 -136 -29 -186q-7 -28 -36 -28h-222q-14 0 -24.5 8.5t-11.5 21.5l-28 184q-49 16 -90 37l-141 -107q-10 -9 -25 -9q-14 0 -25 11q-126 114 -165 168q-7 10 -7 23q0 12 8 23q15 21 51 66.5t54 70.5q-27 50 -41 99l-183 27q-13 2 -21 12.5 [...]
+<glyph unicode="&#xf014;" horiz-adv-x="1408" d="M512 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM768 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1024 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1152 76v948h-896v-948q0 -22 7 -40.5t14.5 -27t10.5 -8.5h832q3 0 10.5 8.5t14.5 27t7 40.5zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q [...]
+<glyph unicode="&#xf015;" horiz-adv-x="1664" d="M1408 544v-480q0 -26 -19 -45t-45 -19h-384v384h-256v-384h-384q-26 0 -45 19t-19 45v480q0 1 0.5 3t0.5 3l575 474l575 -474q1 -2 1 -6zM1631 613l-62 -74q-8 -9 -21 -11h-3q-13 0 -21 7l-692 577l-692 -577q-12 -8 -24 -7q-13 2 -21 11l-62 74q-8 10 -7 23.5t11 21.5 l719 599q32 26 76 26t76 -26l244 -204v195q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-408l219 -182q10 -8 11 -21.5t-7 -23.5z" />
+<glyph unicode="&#xf016;" horiz-adv-x="1280" d="M128 0h1024v768h-416q-40 0 -68 28t-28 68v416h-512v-1280zM768 896h376q-10 29 -22 41l-313 313q-12 12 -41 22v-376zM1280 864v-896q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h640q40 0 88 -20t76 -48l312 -312q28 -28 48 -76t20 -88z " />
+<glyph unicode="&#xf017;" d="M896 992v-448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf018;" horiz-adv-x="1920" d="M1111 540v4l-24 320q-1 13 -11 22.5t-23 9.5h-186q-13 0 -23 -9.5t-11 -22.5l-24 -320v-4q-1 -12 8 -20t21 -8h244q12 0 21 8t8 20zM1870 73q0 -73 -46 -73h-704q13 0 22 9.5t8 22.5l-20 256q-1 13 -11 22.5t-23 9.5h-272q-13 0 -23 -9.5t-11 -22.5l-20 -256 q-1 -13 8 -22.5t22 -9.5h-704q-46 0 -46 73q0 54 26 116l417 1044q8 19 26 33t38 14h339q-13 0 -23 -9.5t-11 -22.5l-15 -192q-1 -14 8 -23t22 -9h166q13 0 22 9t8 23l-15 192q-1 13 -11 22.5t-23 9.5h339q20 0 38 -14t2 [...]
+<glyph unicode="&#xf019;" horiz-adv-x="1664" d="M1280 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 416v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h465l135 -136 q58 -56 136 -56t136 56l136 136h464q40 0 68 -28t28 -68zM1339 985q17 -41 -14 -70l-448 -448q-18 -19 -45 -19t-45 19l-448 448q-31 29 -14 70q17 39 59 39h256v448q0 26 19 45t45 19h256q26 0 45 -19t19 -45v-448h256q4 [...]
+<glyph unicode="&#xf01a;" d="M1120 608q0 -12 -10 -24l-319 -319q-11 -9 -23 -9t-23 9l-320 320q-15 16 -7 35q8 20 30 20h192v352q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-352h192q14 0 23 -9t9 -23zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273 t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf01b;" d="M1118 660q-8 -20 -30 -20h-192v-352q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v352h-192q-14 0 -23 9t-9 23q0 12 10 24l319 319q11 9 23 9t23 -9l320 -320q15 -16 7 -35zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198 t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf01c;" d="M1023 576h316q-1 3 -2.5 8t-2.5 8l-212 496h-708l-212 -496q-1 -2 -2.5 -8t-2.5 -8h316l95 -192h320zM1536 546v-482q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v482q0 62 25 123l238 552q10 25 36.5 42t52.5 17h832q26 0 52.5 -17t36.5 -42l238 -552 q25 -61 25 -123z" />
+<glyph unicode="&#xf01d;" d="M1184 640q0 -37 -32 -55l-544 -320q-15 -9 -32 -9q-16 0 -32 8q-32 19 -32 56v640q0 37 32 56q33 18 64 -1l544 -320q32 -18 32 -55zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf01e;" d="M1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l138 138q-148 137 -349 137q-104 0 -198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5q119 0 225 52t179 147q7 10 23 12q14 0 25 -9 l137 -138q9 -8 9.5 -20.5t-7.5 -22.5q-109 -132 -264 -204.5t-327 -72.5q-156 0 -298 61t-245 164t-164 245t-61 298t61 298t164 245t245 164t298 61q147 0 284.5 -55.5t244.5 -156.5l130 129q29 31 70 14q39 -17 39 -59z" />
+<glyph unicode="&#xf021;" d="M1511 480q0 -5 -1 -7q-64 -268 -268 -434.5t-478 -166.5q-146 0 -282.5 55t-243.5 157l-129 -129q-19 -19 -45 -19t-45 19t-19 45v448q0 26 19 45t45 19h448q26 0 45 -19t19 -45t-19 -45l-137 -137q71 -66 161 -102t187 -36q134 0 250 65t186 179q11 17 53 117 q8 23 30 23h192q13 0 22.5 -9.5t9.5 -22.5zM1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-26 0 -45 19t-19 45t19 45l138 138q-148 137 -349 137q-134 0 -250 -65t-186 -179q-11 -17 -53 -117q-8 -23 -30 -23h-199q-13 0 -22.5 9.5t-9.5 2 [...]
+<glyph unicode="&#xf022;" horiz-adv-x="1792" d="M384 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 608v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M384 864v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1536 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5  [...]
+<glyph unicode="&#xf023;" horiz-adv-x="1152" d="M320 768h512v192q0 106 -75 181t-181 75t-181 -75t-75 -181v-192zM1152 672v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v192q0 184 132 316t316 132t316 -132t132 -316v-192h32q40 0 68 -28t28 -68z" />
+<glyph unicode="&#xf024;" horiz-adv-x="1792" d="M320 1280q0 -72 -64 -110v-1266q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v1266q-64 38 -64 110q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -25 -12.5 -38.5t-39.5 -27.5q-215 -116 -369 -116q-61 0 -123.5 22t-108.5 48 t-115.5 48t-142.5 22q-192 0 -464 -146q-17 -9 -33 -9q-26 0 -45 19t-19 45v742q0 32 31 55q21 14 79 43q236 120 421 120q107 0 200 -29t219 -88q38 -19 88 -19q54 0 117.5 21t110 47t88 47t54.5 21q26 0 45 -1 [...]
+<glyph unicode="&#xf025;" horiz-adv-x="1664" d="M1664 650q0 -166 -60 -314l-20 -49l-185 -33q-22 -83 -90.5 -136.5t-156.5 -53.5v-32q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-32q71 0 130 -35.5t93 -95.5l68 12q29 95 29 193q0 148 -88 279t-236.5 209t-315.5 78 t-315.5 -78t-236.5 -209t-88 -279q0 -98 29 -193l68 -12q34 60 93 95.5t130 35.5v32q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v32q-88 0 -156.5 53.5t-90.5 136.5l-185  [...]
+<glyph unicode="&#xf026;" horiz-adv-x="768" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45z" />
+<glyph unicode="&#xf027;" horiz-adv-x="1152" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142z" />
+<glyph unicode="&#xf028;" horiz-adv-x="1664" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142zM1408 640q0 -153 -85 -282.5t-225 -188.5q-13 -5 -25 -5q-27 0 -46 19t-19 45q0 39 39 59q56  [...]
+<glyph unicode="&#xf029;" horiz-adv-x="1408" d="M384 384v-128h-128v128h128zM384 1152v-128h-128v128h128zM1152 1152v-128h-128v128h128zM128 129h384v383h-384v-383zM128 896h384v384h-384v-384zM896 896h384v384h-384v-384zM640 640v-640h-640v640h640zM1152 128v-128h-128v128h128zM1408 128v-128h-128v128h128z M1408 640v-384h-384v128h-128v-384h-128v640h384v-128h128v128h128zM640 1408v-640h-640v640h640zM1408 1408v-640h-640v640h640z" />
+<glyph unicode="&#xf02a;" horiz-adv-x="1792" d="M63 0h-63v1408h63v-1408zM126 1h-32v1407h32v-1407zM220 1h-31v1407h31v-1407zM377 1h-31v1407h31v-1407zM534 1h-62v1407h62v-1407zM660 1h-31v1407h31v-1407zM723 1h-31v1407h31v-1407zM786 1h-31v1407h31v-1407zM943 1h-63v1407h63v-1407zM1100 1h-63v1407h63v-1407z M1226 1h-63v1407h63v-1407zM1352 1h-63v1407h63v-1407zM1446 1h-63v1407h63v-1407zM1635 1h-94v1407h94v-1407zM1698 1h-32v1407h32v-1407zM1792 0h-63v1408h63v-1408z" />
+<glyph unicode="&#xf02b;" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91z" />
+<glyph unicode="&#xf02c;" horiz-adv-x="1920" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91zM1899 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-36 0 -59 14t-53 45l470 470q37 37 37 90q0 52 -37 91l-715 714q-38 38 -102 64.5t-117 26.5h224q53 0 117 -26.5t102 -64.5l7 [...]
+<glyph unicode="&#xf02d;" horiz-adv-x="1664" d="M1639 1058q40 -57 18 -129l-275 -906q-19 -64 -76.5 -107.5t-122.5 -43.5h-923q-77 0 -148.5 53.5t-99.5 131.5q-24 67 -2 127q0 4 3 27t4 37q1 8 -3 21.5t-3 19.5q2 11 8 21t16.5 23.5t16.5 23.5q23 38 45 91.5t30 91.5q3 10 0.5 30t-0.5 28q3 11 17 28t17 23 q21 36 42 92t25 90q1 9 -2.5 32t0.5 28q4 13 22 30.5t22 22.5q19 26 42.5 84.5t27.5 96.5q1 8 -3 25.5t-2 26.5q2 8 9 18t18 23t17 21q8 12 16.5 30.5t15 35t16 36t19.5 32t26.5 23.5t36 11.5t47.5 -5.5l-1 -3q38 9 51 [...]
+<glyph unicode="&#xf02e;" horiz-adv-x="1280" d="M1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289q0 34 19.5 62t52.5 41q21 9 44 9h1048z" />
+<glyph unicode="&#xf02f;" horiz-adv-x="1664" d="M384 0h896v256h-896v-256zM384 640h896v384h-160q-40 0 -68 28t-28 68v160h-640v-640zM1536 576q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 576v-416q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-160q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68 v160h-224q-13 0 -22.5 9.5t-9.5 22.5v416q0 79 56.5 135.5t135.5 56.5h64v544q0 40 28 68t68 28h672q40 0 88 -20t76 -48l152 -152q28 -28 48 -76t20 -88v-256h64q79 0 135.5 -56.5t56.5 -135.5z" />
+<glyph unicode="&#xf030;" horiz-adv-x="1920" d="M960 864q119 0 203.5 -84.5t84.5 -203.5t-84.5 -203.5t-203.5 -84.5t-203.5 84.5t-84.5 203.5t84.5 203.5t203.5 84.5zM1664 1280q106 0 181 -75t75 -181v-896q0 -106 -75 -181t-181 -75h-1408q-106 0 -181 75t-75 181v896q0 106 75 181t181 75h224l51 136 q19 49 69.5 84.5t103.5 35.5h512q53 0 103.5 -35.5t69.5 -84.5l51 -136h224zM960 128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
+<glyph unicode="&#xf031;" horiz-adv-x="1664" d="M725 977l-170 -450q73 -1 153.5 -2t119 -1.5t52.5 -0.5l29 2q-32 95 -92 241q-53 132 -92 211zM21 -128h-21l2 79q22 7 80 18q89 16 110 31q20 16 48 68l237 616l280 724h75h53l11 -21l205 -480q103 -242 124 -297q39 -102 96 -235q26 -58 65 -164q24 -67 65 -149 q22 -49 35 -57q22 -19 69 -23q47 -6 103 -27q6 -39 6 -57q0 -14 -1 -26q-80 0 -192 8q-93 8 -189 8q-79 0 -135 -2l-200 -11l-58 -2q0 45 4 78l131 28q56 13 68 23q12 12 12 27t-6 32l-47 114l-92 228l-450 2q-29 - [...]
+<glyph unicode="&#xf032;" horiz-adv-x="1408" d="M555 15q76 -32 140 -32q131 0 216 41t122 113q38 70 38 181q0 114 -41 180q-58 94 -141 126q-80 32 -247 32q-74 0 -101 -10v-144l-1 -173l3 -270q0 -15 12 -44zM541 761q43 -7 109 -7q175 0 264 65t89 224q0 112 -85 187q-84 75 -255 75q-52 0 -130 -13q0 -44 2 -77 q7 -122 6 -279l-1 -98q0 -43 1 -77zM0 -128l2 94q45 9 68 12q77 12 123 31q17 27 21 51q9 66 9 194l-2 497q-5 256 -9 404q-1 87 -11 109q-1 4 -12 12q-18 12 -69 15q-30 2 -114 13l-4 83l260 6l380 13l45 1q5 0 [...]
+<glyph unicode="&#xf033;" horiz-adv-x="1024" d="M0 -126l17 85q4 1 77 20q76 19 116 39q29 37 41 101l27 139l56 268l12 64q8 44 17 84.5t16 67t12.5 46.5t9 30.5t3.5 11.5l29 157l16 63l22 135l8 50v38q-41 22 -144 28q-28 2 -38 4l19 103l317 -14q39 -2 73 -2q66 0 214 9q33 2 68 4.5t36 2.5q-2 -19 -6 -38 q-7 -29 -13 -51q-55 -19 -109 -31q-64 -16 -101 -31q-12 -31 -24 -88q-9 -44 -13 -82q-44 -199 -66 -306l-61 -311l-38 -158l-43 -235l-12 -45q-2 -7 1 -27q64 -15 119 -21q36 -5 66 -10q-1 -29 -7 -58q-7 -31 -9 -41q- [...]
+<glyph unicode="&#xf034;" horiz-adv-x="1792" d="M81 1407l54 -27q20 -5 211 -5h130l19 3l115 1l215 -1h293l34 -2q14 -1 28 7t21 16l7 8l42 1q15 0 28 -1v-104.5t1 -131.5l1 -100l-1 -58q0 -32 -4 -51q-39 -15 -68 -18q-25 43 -54 128q-8 24 -15.5 62.5t-11.5 65.5t-6 29q-13 15 -27 19q-7 2 -42.5 2t-103.5 -1t-111 -1 q-34 0 -67 -5q-10 -97 -8 -136l1 -152v-332l3 -359l-1 -147q-1 -46 11 -85q49 -25 89 -32q2 0 18 -5t44 -13t43 -12q30 -8 50 -18q5 -45 5 -50q0 -10 -3 -29q-14 -1 -34 -1q-110 0 -187 10q-72 8 -238 8q-88  [...]
+<glyph unicode="&#xf035;" d="M81 1407l54 -27q20 -5 211 -5h130l19 3l115 1l446 -1h318l34 -2q14 -1 28 7t21 16l7 8l42 1q15 0 28 -1v-104.5t1 -131.5l1 -100l-1 -58q0 -32 -4 -51q-39 -15 -68 -18q-25 43 -54 128q-8 24 -15.5 62.5t-11.5 65.5t-6 29q-13 15 -27 19q-7 2 -58.5 2t-138.5 -1t-128 -1 q-94 0 -127 -5q-10 -97 -8 -136l1 -152v52l3 -359l-1 -147q-1 -46 11 -85q49 -25 89 -32q2 0 18 -5t44 -13t43 -12q30 -8 50 -18q5 -45 5 -50q0 -10 -3 -29q-14 -1 -34 -1q-110 0 -187 10q-72 8 -238 8q-82 0 -233 -13q-45 -5 -7 [...]
+<glyph unicode="&#xf036;" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf037;" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h896q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45t-45 -19 h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h640q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf038;" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf039;" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf03a;" horiz-adv-x="1792" d="M256 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM256 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5 t9.5 -22.5zM256 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 - [...]
+<glyph unicode="&#xf03b;" horiz-adv-x="1792" d="M384 992v-576q0 -13 -9.5 -22.5t-22.5 -9.5q-14 0 -23 9l-288 288q-9 9 -9 23t9 23l288 288q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22 [...]
+<glyph unicode="&#xf03c;" horiz-adv-x="1792" d="M352 704q0 -14 -9 -23l-288 -288q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v576q0 13 9.5 22.5t22.5 9.5q14 0 23 -9l288 -288q9 -9 9 -23zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q- [...]
+<glyph unicode="&#xf03d;" horiz-adv-x="1792" d="M1792 1184v-1088q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-403 403v-166q0 -119 -84.5 -203.5t-203.5 -84.5h-704q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h704q119 0 203.5 -84.5t84.5 -203.5v-165l403 402q18 19 45 19q12 0 25 -5 q39 -17 39 -59z" />
+<glyph unicode="&#xf03e;" horiz-adv-x="1920" d="M640 960q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 576v-448h-1408v192l320 320l160 -160l512 512zM1760 1280h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-1216q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5v1216 q0 13 -9.5 22.5t-22.5 9.5zM1920 1248v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
+<glyph unicode="&#xf040;" d="M363 0l91 91l-235 235l-91 -91v-107h128v-128h107zM886 928q0 22 -22 22q-10 0 -17 -7l-542 -542q-7 -7 -7 -17q0 -22 22 -22q10 0 17 7l542 542q7 7 7 17zM832 1120l416 -416l-832 -832h-416v416zM1515 1024q0 -53 -37 -90l-166 -166l-416 416l166 165q36 38 90 38 q53 0 91 -38l235 -234q37 -39 37 -91z" />
+<glyph unicode="&#xf041;" horiz-adv-x="1024" d="M768 896q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1024 896q0 -109 -33 -179l-364 -774q-16 -33 -47.5 -52t-67.5 -19t-67.5 19t-46.5 52l-365 774q-33 70 -33 179q0 212 150 362t362 150t362 -150t150 -362z" />
+<glyph unicode="&#xf042;" d="M768 96v1088q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf043;" horiz-adv-x="1024" d="M512 384q0 36 -20 69q-1 1 -15.5 22.5t-25.5 38t-25 44t-21 50.5q-4 16 -21 16t-21 -16q-7 -23 -21 -50.5t-25 -44t-25.5 -38t-15.5 -22.5q-20 -33 -20 -69q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 512q0 -212 -150 -362t-362 -150t-362 150t-150 362 q0 145 81 275q6 9 62.5 90.5t101 151t99.5 178t83 201.5q9 30 34 47t51 17t51.5 -17t33.5 -47q28 -93 83 -201.5t99.5 -178t101 -151t62.5 -90.5q81 -127 81 -275z" />
+<glyph unicode="&#xf044;" horiz-adv-x="1792" d="M888 352l116 116l-152 152l-116 -116v-56h96v-96h56zM1328 1072q-16 16 -33 -1l-350 -350q-17 -17 -1 -33t33 1l350 350q17 17 1 33zM1408 478v-190q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-14 -14 -32 -8q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v126q0 13 9 22l64 64q15 15 35 7t20 -29zM1312 12 [...]
+<glyph unicode="&#xf045;" horiz-adv-x="1664" d="M1408 547v-259q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h255v0q13 0 22.5 -9.5t9.5 -22.5q0 -27 -26 -32q-77 -26 -133 -60q-10 -4 -16 -4h-112q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832 q66 0 113 47t47 113v214q0 19 18 29q28 13 54 37q16 16 35 8q21 -9 21 -29zM1645 1043l-384 -384q-18 -19 -45 -19q-12 0 -25 5q-39 17 -39 59v192h-160q-323 0 -438 -131q-119 -137 -74 -473q3 -23 -20 -34 [...]
+<glyph unicode="&#xf046;" horiz-adv-x="1664" d="M1408 606v-318q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-10 -10 -23 -10q-3 0 -9 2q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832 q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v254q0 13 9 22l64 64q10 10 23 10q6 0 12 -3q20 -8 20 -29zM1639 1095l-814 -814q-24 -24 -57 -24t-57 24l-430 430q-24 24 -24 57t24 57l110 110q24 24 57 24t57 -24l263  [...]
+<glyph unicode="&#xf047;" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-384v-384h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v384h-384v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45 t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h384v384h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45t-19 -45t-45 -19h-128v-384h384v128q0 26 19 45t45 19t45 -19l256  [...]
+<glyph unicode="&#xf048;" horiz-adv-x="1024" d="M979 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19z" />
+<glyph unicode="&#xf049;" horiz-adv-x="1792" d="M1747 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19l710 710 q19 19 32 13t13 -32v-710q4 11 13 19z" />
+<glyph unicode="&#xf04a;" horiz-adv-x="1664" d="M1619 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-8 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-19 19 -19 45t19 45l710 710q19 19 32 13t13 -32v-710q5 11 13 19z" />
+<glyph unicode="&#xf04b;" horiz-adv-x="1408" d="M1384 609l-1328 -738q-23 -13 -39.5 -3t-16.5 36v1472q0 26 16.5 36t39.5 -3l1328 -738q23 -13 23 -31t-23 -31z" />
+<glyph unicode="&#xf04c;" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45zM640 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf04d;" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf04e;" horiz-adv-x="1664" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q19 -19 19 -45t-19 -45l-710 -710q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" />
+<glyph unicode="&#xf050;" horiz-adv-x="1792" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19l-710 -710 q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" />
+<glyph unicode="&#xf051;" horiz-adv-x="1024" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19z" />
+<glyph unicode="&#xf052;" horiz-adv-x="1538" d="M14 557l710 710q19 19 45 19t45 -19l710 -710q19 -19 13 -32t-32 -13h-1472q-26 0 -32 13t13 32zM1473 0h-1408q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1408q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19z" />
+<glyph unicode="&#xf053;" horiz-adv-x="1152" d="M742 -37l-652 651q-37 37 -37 90.5t37 90.5l652 651q37 37 90.5 37t90.5 -37l75 -75q37 -37 37 -90.5t-37 -90.5l-486 -486l486 -485q37 -38 37 -91t-37 -90l-75 -75q-37 -37 -90.5 -37t-90.5 37z" />
+<glyph unicode="&#xf054;" horiz-adv-x="1152" d="M1099 704q0 -52 -37 -91l-652 -651q-37 -37 -90 -37t-90 37l-76 75q-37 39 -37 91q0 53 37 90l486 486l-486 485q-37 39 -37 91q0 53 37 90l76 75q36 38 90 38t90 -38l652 -651q37 -37 37 -90z" />
+<glyph unicode="&#xf055;" d="M1216 576v128q0 26 -19 45t-45 19h-256v256q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-256h-256q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h256v-256q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v256h256q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5 t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf056;" d="M1216 576v128q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" />
+<glyph unicode="&#xf057;" d="M1149 414q0 26 -19 45l-181 181l181 181q19 19 19 45q0 27 -19 46l-90 90q-19 19 -46 19q-26 0 -45 -19l-181 -181l-181 181q-19 19 -45 19q-27 0 -46 -19l-90 -90q-19 -19 -19 -46q0 -26 19 -45l181 -181l-181 -181q-19 -19 -19 -45q0 -27 19 -46l90 -90q19 -19 46 -19 q26 0 45 19l181 181l181 -181q19 -19 45 -19q27 0 46 19l90 90q19 19 19 46zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 - [...]
+<glyph unicode="&#xf058;" d="M1284 802q0 28 -18 46l-91 90q-19 19 -45 19t-45 -19l-408 -407l-226 226q-19 19 -45 19t-45 -19l-91 -90q-18 -18 -18 -46q0 -27 18 -45l362 -362q19 -19 45 -19q27 0 46 19l543 543q18 18 18 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf059;" d="M896 160v192q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h192q14 0 23 9t9 23zM1152 832q0 88 -55.5 163t-138.5 116t-170 41q-243 0 -371 -213q-15 -24 8 -42l132 -100q7 -6 19 -6q16 0 25 12q53 68 86 92q34 24 86 24q48 0 85.5 -26t37.5 -59 q0 -38 -20 -61t-68 -45q-63 -28 -115.5 -86.5t-52.5 -125.5v-36q0 -14 9 -23t23 -9h192q14 0 23 9t9 23q0 19 21.5 49.5t54.5 49.5q32 18 49 28.5t46 35t44.5 48t28 60.5t12.5 81zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-38 [...]
+<glyph unicode="&#xf05a;" d="M1024 160v160q0 14 -9 23t-23 9h-96v512q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h96v-320h-96q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h448q14 0 23 9t9 23zM896 1056v160q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23 t23 -9h192q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf05b;" d="M1197 512h-109q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h109q-32 108 -112.5 188.5t-188.5 112.5v-109q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v109q-108 -32 -188.5 -112.5t-112.5 -188.5h109q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-109 q32 -108 112.5 -188.5t188.5 -112.5v109q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-109q108 32 188.5 112.5t112.5 188.5zM1536 704v-128q0 -26 -19 -45t-45 -19h-143q-37 -161 -154.5 -278.5t-278.5 -154.5v-143q0 -26 -19 -45t-45 -19h [...]
+<glyph unicode="&#xf05c;" d="M1097 457l-146 -146q-10 -10 -23 -10t-23 10l-137 137l-137 -137q-10 -10 -23 -10t-23 10l-146 146q-10 10 -10 23t10 23l137 137l-137 137q-10 10 -10 23t10 23l146 146q10 10 23 10t23 -10l137 -137l137 137q10 10 23 10t23 -10l146 -146q10 -10 10 -23t-10 -23 l-137 -137l137 -137q10 -10 10 -23t-10 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385. [...]
+<glyph unicode="&#xf05d;" d="M1171 723l-422 -422q-19 -19 -45 -19t-45 19l-294 294q-19 19 -19 45t19 45l102 102q19 19 45 19t45 -19l147 -147l275 275q19 19 45 19t45 -19l102 -102q19 -19 19 -45t-19 -45zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198 t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf05e;" d="M1312 643q0 161 -87 295l-754 -753q137 -89 297 -89q111 0 211.5 43.5t173.5 116.5t116 174.5t43 212.5zM313 344l755 754q-135 91 -300 91q-148 0 -273 -73t-198 -199t-73 -274q0 -162 89 -299zM1536 643q0 -157 -61 -300t-163.5 -246t-245 -164t-298.5 -61t-298.5 61 t-245 164t-163.5 246t-61 300t61 299.5t163.5 245.5t245 164t298.5 61t298.5 -61t245 -164t163.5 -245.5t61 -299.5z" />
+<glyph unicode="&#xf060;" d="M1536 640v-128q0 -53 -32.5 -90.5t-84.5 -37.5h-704l293 -294q38 -36 38 -90t-38 -90l-75 -76q-37 -37 -90 -37q-52 0 -91 37l-651 652q-37 37 -37 90q0 52 37 91l651 650q38 38 91 38q52 0 90 -38l75 -74q38 -38 38 -91t-38 -91l-293 -293h704q52 0 84.5 -37.5 t32.5 -90.5z" />
+<glyph unicode="&#xf061;" d="M1472 576q0 -54 -37 -91l-651 -651q-39 -37 -91 -37q-51 0 -90 37l-75 75q-38 38 -38 91t38 91l293 293h-704q-52 0 -84.5 37.5t-32.5 90.5v128q0 53 32.5 90.5t84.5 37.5h704l-293 294q-38 36 -38 90t38 90l75 75q38 38 90 38q53 0 91 -38l651 -651q37 -35 37 -90z" />
+<glyph unicode="&#xf062;" horiz-adv-x="1664" d="M1611 565q0 -51 -37 -90l-75 -75q-38 -38 -91 -38q-54 0 -90 38l-294 293v-704q0 -52 -37.5 -84.5t-90.5 -32.5h-128q-53 0 -90.5 32.5t-37.5 84.5v704l-294 -293q-36 -38 -90 -38t-90 38l-75 75q-38 38 -38 90q0 53 38 91l651 651q35 37 90 37q54 0 91 -37l651 -651 q37 -39 37 -91z" />
+<glyph unicode="&#xf063;" horiz-adv-x="1664" d="M1611 704q0 -53 -37 -90l-651 -652q-39 -37 -91 -37q-53 0 -90 37l-651 652q-38 36 -38 90q0 53 38 91l74 75q39 37 91 37q53 0 90 -37l294 -294v704q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-704l294 294q37 37 90 37q52 0 91 -37l75 -75q37 -39 37 -91z" />
+<glyph unicode="&#xf064;" horiz-adv-x="1792" d="M1792 896q0 -26 -19 -45l-512 -512q-19 -19 -45 -19t-45 19t-19 45v256h-224q-98 0 -175.5 -6t-154 -21.5t-133 -42.5t-105.5 -69.5t-80 -101t-48.5 -138.5t-17.5 -181q0 -55 5 -123q0 -6 2.5 -23.5t2.5 -26.5q0 -15 -8.5 -25t-23.5 -10q-16 0 -28 17q-7 9 -13 22 t-13.5 30t-10.5 24q-127 285 -127 451q0 199 53 333q162 403 875 403h224v256q0 26 19 45t45 19t45 -19l512 -512q19 -19 19 -45z" />
+<glyph unicode="&#xf065;" d="M755 480q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23zM1536 1344v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332 q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf066;" d="M768 576v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45zM1523 1248q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45 t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23z" />
+<glyph unicode="&#xf067;" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-416v-416q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v416h-416q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h416v416q0 40 28 68t68 28h192q40 0 68 -28t28 -68v-416h416q40 0 68 -28t28 -68z" />
+<glyph unicode="&#xf068;" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-1216q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h1216q40 0 68 -28t28 -68z" />
+<glyph unicode="&#xf069;" horiz-adv-x="1664" d="M1482 486q46 -26 59.5 -77.5t-12.5 -97.5l-64 -110q-26 -46 -77.5 -59.5t-97.5 12.5l-266 153v-307q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v307l-266 -153q-46 -26 -97.5 -12.5t-77.5 59.5l-64 110q-26 46 -12.5 97.5t59.5 77.5l266 154l-266 154 q-46 26 -59.5 77.5t12.5 97.5l64 110q26 46 77.5 59.5t97.5 -12.5l266 -153v307q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-307l266 153q46 26 97.5 12.5t77.5 -59.5l64 -110q26 -46 12.5 -97.5t-59.5 -77.5l-266 -154z" />
+<glyph unicode="&#xf06a;" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM896 161v190q0 14 -9 23.5t-22 9.5h-192q-13 0 -23 -10t-10 -23v-190q0 -13 10 -23t23 -10h192 q13 0 22 9.5t9 23.5zM894 505l18 621q0 12 -10 18q-10 8 -24 8h-220q-14 0 -24 -8q-10 -6 -10 -18l17 -621q0 -10 10 -17.5t24 -7.5h185q14 0 23.5 7.5t10.5 17.5z" />
+<glyph unicode="&#xf06b;" d="M928 180v56v468v192h-320v-192v-468v-56q0 -25 18 -38.5t46 -13.5h192q28 0 46 13.5t18 38.5zM472 1024h195l-126 161q-26 31 -69 31q-40 0 -68 -28t-28 -68t28 -68t68 -28zM1160 1120q0 40 -28 68t-68 28q-43 0 -69 -31l-125 -161h194q40 0 68 28t28 68zM1536 864v-320 q0 -14 -9 -23t-23 -9h-96v-416q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v416h-96q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h440q-93 0 -158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5q107 0 168 -77l128 -165l128 165q61 [...]
+<glyph unicode="&#xf06c;" horiz-adv-x="1792" d="M1280 832q0 26 -19 45t-45 19q-172 0 -318 -49.5t-259.5 -134t-235.5 -219.5q-19 -21 -19 -45q0 -26 19 -45t45 -19q24 0 45 19q27 24 74 71t67 66q137 124 268.5 176t313.5 52q26 0 45 19t19 45zM1792 1030q0 -95 -20 -193q-46 -224 -184.5 -383t-357.5 -268 q-214 -108 -438 -108q-148 0 -286 47q-15 5 -88 42t-96 37q-16 0 -39.5 -32t-45 -70t-52.5 -70t-60 -32q-30 0 -51 11t-31 24t-27 42q-2 4 -6 11t-5.5 10t-3 9.5t-1.5 13.5q0 35 31 73.5t68 65.5t68 56t31 48q0 4 -14 3 [...]
+<glyph unicode="&#xf06d;" horiz-adv-x="1408" d="M1408 -160v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1152 896q0 -78 -24.5 -144t-64 -112.5t-87.5 -88t-96 -77.5t-87.5 -72t-64 -81.5t-24.5 -96.5q0 -96 67 -224l-4 1l1 -1 q-90 41 -160 83t-138.5 100t-113.5 122.5t-72.5 150.5t-27.5 184q0 78 24.5 144t64 112.5t87.5 88t96 77.5t87.5 72t64 81.5t24.5 96.5q0 94 -66 224l3 -1l-1 1q90 -41 160 -83t138.5 -100t113.5 -122.5t72.5 -150. [...]
+<glyph unicode="&#xf06e;" horiz-adv-x="1792" d="M1664 576q-152 236 -381 353q61 -104 61 -225q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5q0 121 61 225q-229 -117 -381 -353q133 -205 333.5 -326.5t434.5 -121.5t434.5 121.5t333.5 326.5zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5 t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1792 576q0 -34 -20 -69q-140 -230 -376.5 -368.5t-499.5 -138.5t-499.5 139t-376.5 368q-20 35 -20 69t20 69q140 229 376.5 [...]
+<glyph unicode="&#xf070;" horiz-adv-x="1792" d="M555 201l78 141q-87 63 -136 159t-49 203q0 121 61 225q-229 -117 -381 -353q167 -258 427 -375zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1307 1151q0 -7 -1 -9 q-105 -188 -315 -566t-316 -567l-49 -89q-10 -16 -28 -16q-12 0 -134 70q-16 10 -16 28q0 12 44 87q-143 65 -263.5 173t-208.5 245q-20 31 -20 69t20 69q153 235 380 371t496 136q89 0 180 -17l54 97q10 16 28 16q5 0 [...]
+<glyph unicode="&#xf071;" horiz-adv-x="1792" d="M1024 161v190q0 14 -9.5 23.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -23.5v-190q0 -14 9.5 -23.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 23.5zM1022 535l18 459q0 12 -10 19q-13 11 -24 11h-220q-11 0 -24 -11q-10 -7 -10 -21l17 -457q0 -10 10 -16.5t24 -6.5h185 q14 0 23.5 6.5t10.5 16.5zM1008 1469l768 -1408q35 -63 -2 -126q-17 -29 -46.5 -46t-63.5 -17h-1536q-34 0 -63.5 17t-46.5 46q-37 63 -2 126l768 1408q17 31 47 49t65 18t65 -18t47 -49z" />
+<glyph unicode="&#xf072;" horiz-adv-x="1408" d="M1376 1376q44 -52 12 -148t-108 -172l-161 -161l160 -696q5 -19 -12 -33l-128 -96q-7 -6 -19 -6q-4 0 -7 1q-15 3 -21 16l-279 508l-259 -259l53 -194q5 -17 -8 -31l-96 -96q-9 -9 -23 -9h-2q-15 2 -24 13l-189 252l-252 189q-11 7 -13 23q-1 13 9 25l96 97q9 9 23 9 q6 0 8 -1l194 -53l259 259l-508 279q-14 8 -17 24q-2 16 9 27l128 128q14 13 30 8l665 -159l160 160q76 76 172 108t148 -12z" />
+<glyph unicode="&#xf073;" horiz-adv-x="1664" d="M128 -128h288v288h-288v-288zM480 -128h320v288h-320v-288zM128 224h288v320h-288v-320zM480 224h320v320h-320v-320zM128 608h288v288h-288v-288zM864 -128h320v288h-320v-288zM480 608h320v288h-320v-288zM1248 -128h288v288h-288v-288zM864 224h320v320h-320v-320z M512 1088v288q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-288q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1248 224h288v320h-288v-320zM864 608h320v288h-320v-288zM1248 608h28 [...]
+<glyph unicode="&#xf074;" horiz-adv-x="1792" d="M666 1055q-60 -92 -137 -273q-22 45 -37 72.5t-40.5 63.5t-51 56.5t-63 35t-81.5 14.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q250 0 410 -225zM1792 256q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192q-32 0 -85 -0.5t-81 -1t-73 1 t-71 5t-64 10.5t-63 18.5t-58 28.5t-59 40t-55 53.5t-56 69.5q59 93 136 273q22 -45 37 -72.5t40.5 -63.5t51 -56.5t63 -35t81.5 -14.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1792 1152q0 -1 [...]
+<glyph unicode="&#xf075;" horiz-adv-x="1792" d="M1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22q-17 -2 -30.5 9t-17.5 29v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281 q0 130 71 248.5t191 204.5t286 136.5t348 50.5q244 0 450 -85.5t326 -233t120 -321.5z" />
+<glyph unicode="&#xf076;" d="M1536 704v-128q0 -201 -98.5 -362t-274 -251.5t-395.5 -90.5t-395.5 90.5t-274 251.5t-98.5 362v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-128q0 -52 23.5 -90t53.5 -57t71 -30t64 -13t44 -2t44 2t64 13t71 30t53.5 57t23.5 90v128q0 26 19 45t45 19h384 q26 0 45 -19t19 -45zM512 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45zM1536 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 [...]
+<glyph unicode="&#xf077;" horiz-adv-x="1664" d="M1611 320q0 -53 -37 -90l-75 -75q-38 -38 -91 -38q-54 0 -90 38l-486 485l-486 -485q-36 -38 -90 -38t-90 38l-75 75q-38 36 -38 90q0 53 38 91l651 651q37 37 90 37q52 0 91 -37l650 -651q38 -38 38 -91z" />
+<glyph unicode="&#xf078;" horiz-adv-x="1664" d="M1611 832q0 -53 -37 -90l-651 -651q-38 -38 -91 -38q-54 0 -90 38l-651 651q-38 36 -38 90q0 53 38 91l74 75q39 37 91 37q53 0 90 -37l486 -486l486 486q37 37 90 37q52 0 91 -37l75 -75q37 -39 37 -91z" />
+<glyph unicode="&#xf079;" horiz-adv-x="1920" d="M1280 32q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-8 0 -13.5 2t-9 7t-5.5 8t-3 11.5t-1 11.5v13v11v160v416h-192q-26 0 -45 19t-19 45q0 24 15 41l320 384q19 22 49 22t49 -22l320 -384q15 -17 15 -41q0 -26 -19 -45t-45 -19h-192v-384h576q16 0 25 -11l160 -192q7 -11 7 -21 zM1920 448q0 -24 -15 -41l-320 -384q-20 -23 -49 -23t-49 23l-320 384q-15 17 -15 41q0 26 19 45t45 19h192v384h-576q-16 0 -25 12l-160 192q-7 9 -7 20q0 13 9.5 22.5t22.5 9.5h960q8 0 13.5 -2t9 -7t5.5  [...]
+<glyph unicode="&#xf07a;" horiz-adv-x="1664" d="M640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5 l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h [...]
+<glyph unicode="&#xf07b;" horiz-adv-x="1664" d="M1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" />
+<glyph unicode="&#xf07c;" horiz-adv-x="1920" d="M1879 584q0 -31 -31 -66l-336 -396q-43 -51 -120.5 -86.5t-143.5 -35.5h-1088q-34 0 -60.5 13t-26.5 43q0 31 31 66l336 396q43 51 120.5 86.5t143.5 35.5h1088q34 0 60.5 -13t26.5 -43zM1536 928v-160h-832q-94 0 -197 -47.5t-164 -119.5l-337 -396l-5 -6q0 4 -0.5 12.5 t-0.5 12.5v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158z" />
+<glyph unicode="&#xf07d;" horiz-adv-x="768" d="M704 1216q0 -26 -19 -45t-45 -19h-128v-1024h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v1024h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45z" />
+<glyph unicode="&#xf07e;" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-1024v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h1024v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" />
+<glyph unicode="&#xf080;" horiz-adv-x="1920" d="M512 512v-384h-256v384h256zM896 1024v-896h-256v896h256zM1280 768v-640h-256v640h256zM1664 1152v-1024h-256v1024h256zM1792 32v1216q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-1216q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5z M1920 1248v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
+<glyph unicode="&#xf081;" d="M1280 926q-56 -25 -121 -34q68 40 93 117q-65 -38 -134 -51q-61 66 -153 66q-87 0 -148.5 -61.5t-61.5 -148.5q0 -29 5 -48q-129 7 -242 65t-192 155q-29 -50 -29 -106q0 -114 91 -175q-47 1 -100 26v-2q0 -75 50 -133.5t123 -72.5q-29 -8 -51 -8q-13 0 -39 4 q21 -63 74.5 -104t121.5 -42q-116 -90 -261 -90q-26 0 -50 3q148 -94 322 -94q112 0 210 35.5t168 95t120.5 137t75 162t24.5 168.5q0 18 -1 27q63 45 105 109zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t [...]
+<glyph unicode="&#xf082;" d="M1307 618l23 219h-198v109q0 49 15.5 68.5t71.5 19.5h110v219h-175q-152 0 -218 -72t-66 -213v-131h-131v-219h131v-635h262v635h175zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960 q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf083;" horiz-adv-x="1792" d="M928 704q0 14 -9 23t-23 9q-66 0 -113 -47t-47 -113q0 -14 9 -23t23 -9t23 9t9 23q0 40 28 68t68 28q14 0 23 9t9 23zM1152 574q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM128 0h1536v128h-1536v-128zM1280 574q0 159 -112.5 271.5 t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM256 1216h384v128h-384v-128zM128 1024h1536v118v138h-828l-64 -128h-644v-128zM1792 1280v-1280q0 -53 -37.5 - [...]
+<glyph unicode="&#xf084;" horiz-adv-x="1792" d="M832 1024q0 80 -56 136t-136 56t-136 -56t-56 -136q0 -42 19 -83q-41 19 -83 19q-80 0 -136 -56t-56 -136t56 -136t136 -56t136 56t56 136q0 42 -19 83q41 -19 83 -19q80 0 136 56t56 136zM1683 320q0 -17 -49 -66t-66 -49q-9 0 -28.5 16t-36.5 33t-38.5 40t-24.5 26 l-96 -96l220 -220q28 -28 28 -68q0 -42 -39 -81t-81 -39q-40 0 -68 28l-671 671q-176 -131 -365 -131q-163 0 -265.5 102.5t-102.5 265.5q0 160 95 313t248 248t313 95q163 0 265.5 -102.5t102.5 -265.5q0 -189  [...]
+<glyph unicode="&#xf085;" horiz-adv-x="1920" d="M896 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1664 128q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1152q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1280 731v-185q0 -10 -7 -19.5t-16 -10.5l-155 -24q-11 -35 -32 -76q34 -48 90 -115q7 -10 7 -20q0 -12 -7 -19q-23 -30 -82.5 -89.5t-78.5 -59.5q-11 0 -21 7l-115 90q-37 -19 -77 -31q-11  [...]
+<glyph unicode="&#xf086;" horiz-adv-x="1792" d="M1408 768q0 -139 -94 -257t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q-124 72 -195 177t-71 224 q0 139 94 257t256.5 186.5t353.5 68.5t353.5 -68.5t256.5 -186.5t94 -257zM1792 512q0 -120 -71 -224.5t-195 -176.5q10 -24 20.5 -44t25 -38.5t22.5 -29t26 -29.5t23 -25q1 -1 4 -4.5t4.5 -5t4 -5t3.5 -5.5l2.5 [...]
+<glyph unicode="&#xf087;" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 768q0 51 -39 89.5t-89 38.5h-352q0 58 48 159.5t48 160.5q0 98 -32 145t-128 47q-26 -26 -38 -85t-30.5 -125.5t-59.5 -109.5q-22 -23 -77 -91q-4 -5 -23 -30t-31.5 -41t-34.5 -42.5 t-40 -44t-38.5 -35.5t-40 -27t-35.5 -9h-32v-640h32q13 0 31.5 -3t33 -6.5t38 -11t35 -11.5t35.5 -12.5t29 -10.5q211 -73 342 -73h121q192 0 192 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 4 [...]
+<glyph unicode="&#xf088;" d="M256 1088q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 512q0 35 -21.5 81t-53.5 47q15 17 25 47.5t10 55.5q0 69 -53 119q18 32 18 69t-17.5 73.5t-47.5 52.5q5 30 5 56q0 85 -49 126t-136 41h-128q-131 0 -342 -73q-5 -2 -29 -10.5 t-35.5 -12.5t-35 -11.5t-38 -11t-33 -6.5t-31.5 -3h-32v-640h32q16 0 35.5 -9t40 -27t38.5 -35.5t40 -44t34.5 -42.5t31.5 -41t23 -30q55 -68 77 -91q41 -43 59.5 -109.5t30.5 -125.5t38 -85q96 0 128 47t32 145q0 59 -48 160.5t-48 159.5h [...]
+<glyph unicode="&#xf089;" horiz-adv-x="896" d="M832 1504v-1339l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41z" />
+<glyph unicode="&#xf08a;" horiz-adv-x="1792" d="M1664 940q0 81 -21.5 143t-55 98.5t-81.5 59.5t-94 31t-98 8t-112 -25.5t-110.5 -64t-86.5 -72t-60 -61.5q-18 -22 -49 -22t-49 22q-24 28 -60 61.5t-86.5 72t-110.5 64t-112 25.5t-98 -8t-94 -31t-81.5 -59.5t-55 -98.5t-21.5 -143q0 -168 187 -355l581 -560l580 559 q188 188 188 356zM1792 940q0 -221 -229 -450l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -6 [...]
+<glyph unicode="&#xf08b;" horiz-adv-x="1664" d="M640 96q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h320q13 0 22.5 -9.5t9.5 -22.5q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-66 0 -113 -47t-47 -113v-704 q0 -66 47 -113t113 -47h288h11h13t11.5 -1t11.5 -3t8 -5.5t7 -9t2 -13.5zM1568 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t4 [...]
+<glyph unicode="&#xf08c;" d="M237 122h231v694h-231v-694zM483 1030q-1 52 -36 86t-93 34t-94.5 -34t-36.5 -86q0 -51 35.5 -85.5t92.5 -34.5h1q59 0 95 34.5t36 85.5zM1068 122h231v398q0 154 -73 233t-193 79q-136 0 -209 -117h2v101h-231q3 -66 0 -694h231v388q0 38 7 56q15 35 45 59.5t74 24.5 q116 0 116 -157v-371zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf08d;" horiz-adv-x="1152" d="M480 672v448q0 14 -9 23t-23 9t-23 -9t-9 -23v-448q0 -14 9 -23t23 -9t23 9t9 23zM1152 320q0 -26 -19 -45t-45 -19h-429l-51 -483q-2 -12 -10.5 -20.5t-20.5 -8.5h-1q-27 0 -32 27l-76 485h-404q-26 0 -45 19t-19 45q0 123 78.5 221.5t177.5 98.5v512q-52 0 -90 38 t-38 90t38 90t90 38h640q52 0 90 -38t38 -90t-38 -90t-90 -38v-512q99 0 177.5 -98.5t78.5 -221.5z" />
+<glyph unicode="&#xf08e;" horiz-adv-x="1792" d="M1408 608v-320q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v320 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1792 1472v-512q0 -26 -19 -45t-45 -19t-45 19l-176 176l-652 -652q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l652 652l-176 176q-19 19 -19 45t19 45t45 19h512q2 [...]
+<glyph unicode="&#xf090;" d="M1184 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45zM1536 992v-704q0 -119 -84.5 -203.5t-203.5 -84.5h-320q-13 0 -22.5 9.5t-9.5 22.5 q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q66 0 113 47t47 113v704q0 66 -47 113t-113 47h-288h-11h-13t-11.5 1t-11.5 3t-8 5.5t-7 9t-2 13.5q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q119 0 203.5 -84.5t84.5 -2 [...]
+<glyph unicode="&#xf091;" horiz-adv-x="1664" d="M458 653q-74 162 -74 371h-256v-96q0 -78 94.5 -162t235.5 -113zM1536 928v96h-256q0 -209 -74 -371q141 29 235.5 113t94.5 162zM1664 1056v-128q0 -71 -41.5 -143t-112 -130t-173 -97.5t-215.5 -44.5q-42 -54 -95 -95q-38 -34 -52.5 -72.5t-14.5 -89.5q0 -54 30.5 -91 t97.5 -37q75 0 133.5 -45.5t58.5 -114.5v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 69 58.5 114.5t133.5 45.5q67 0 97.5 37t30.5 91q0 51 -14.5 89.5t-52.5 72.5q-53 41 -95 95q-113 5 -215.5 4 [...]
+<glyph unicode="&#xf092;" d="M394 184q-8 -9 -20 3q-13 11 -4 19q8 9 20 -3q12 -11 4 -19zM352 245q9 -12 0 -19q-8 -6 -17 7t0 18q9 7 17 -6zM291 305q-5 -7 -13 -2q-10 5 -7 12q3 5 13 2q10 -5 7 -12zM322 271q-6 -7 -16 3q-9 11 -2 16q6 6 16 -3q9 -11 2 -16zM451 159q-4 -12 -19 -6q-17 4 -13 15 t19 7q16 -5 13 -16zM514 154q0 -11 -16 -11q-17 -2 -17 11q0 11 16 11q17 2 17 -11zM572 164q2 -10 -14 -14t-18 8t14 15q16 2 18 -9zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-224q-16 0 -24.5 1t-19.5 5t-16 14.5t-5 [...]
+<glyph unicode="&#xf093;" horiz-adv-x="1664" d="M1280 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 288v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h427q21 -56 70.5 -92 t110.5 -36h256q61 0 110.5 36t70.5 92h427q40 0 68 -28t28 -68zM1339 936q-17 -40 -59 -40h-256v-448q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v448h-256q-42 0 -59 40q-17 39 14 69l448 448q18 19 45 19t45 [...]
+<glyph unicode="&#xf094;" d="M1407 710q0 44 -7 113.5t-18 96.5q-12 30 -17 44t-9 36.5t-4 48.5q0 23 5 68.5t5 67.5q0 37 -10 55q-4 1 -13 1q-19 0 -58 -4.5t-59 -4.5q-60 0 -176 24t-175 24q-43 0 -94.5 -11.5t-85 -23.5t-89.5 -34q-137 -54 -202 -103q-96 -73 -159.5 -189.5t-88 -236t-24.5 -248.5 q0 -40 12.5 -120t12.5 -121q0 -23 -11 -66.5t-11 -65.5t12 -36.5t34 -14.5q24 0 72.5 11t73.5 11q57 0 169.5 -15.5t169.5 -15.5q181 0 284 36q129 45 235.5 152.5t166 245.5t59.5 275zM1535 712q0 -165 -70 -327.5t-196 -288t- [...]
+<glyph unicode="&#xf095;" horiz-adv-x="1408" d="M1408 296q0 -27 -10 -70.5t-21 -68.5q-21 -50 -122 -106q-94 -51 -186 -51q-27 0 -52.5 3.5t-57.5 12.5t-47.5 14.5t-55.5 20.5t-49 18q-98 35 -175 83q-128 79 -264.5 215.5t-215.5 264.5q-48 77 -83 175q-3 9 -18 49t-20.5 55.5t-14.5 47.5t-12.5 57.5t-3.5 52.5 q0 92 51 186q56 101 106 122q25 11 68.5 21t70.5 10q14 0 21 -3q18 -6 53 -76q11 -19 30 -54t35 -63.5t31 -53.5q3 -4 17.5 -25t21.5 -35.5t7 -28.5q0 -20 -28.5 -50t-62 -55t-62 -53t-28.5 -46q0 -9 5 -22.5t8.5  [...]
+<glyph unicode="&#xf096;" horiz-adv-x="1408" d="M1120 1280h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v832q0 66 -47 113t-113 47zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf097;" horiz-adv-x="1280" d="M1152 1280h-1024v-1242l423 406l89 85l89 -85l423 -406v1242zM1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289 q0 34 19.5 62t52.5 41q21 9 44 9h1048z" />
+<glyph unicode="&#xf098;" d="M1280 343q0 11 -2 16q-3 8 -38.5 29.5t-88.5 49.5l-53 29q-5 3 -19 13t-25 15t-21 5q-18 0 -47 -32.5t-57 -65.5t-44 -33q-7 0 -16.5 3.5t-15.5 6.5t-17 9.5t-14 8.5q-99 55 -170.5 126.5t-126.5 170.5q-2 3 -8.5 14t-9.5 17t-6.5 15.5t-3.5 16.5q0 13 20.5 33.5t45 38.5 t45 39.5t20.5 36.5q0 10 -5 21t-15 25t-13 19q-3 6 -15 28.5t-25 45.5t-26.5 47.5t-25 40.5t-16.5 18t-16 2q-48 0 -101 -22q-46 -21 -80 -94.5t-34 -130.5q0 -16 2.5 -34t5 -30.5t9 -33t10 -29.5t12.5 -33t11 -30q60 -164 216. [...]
+<glyph unicode="&#xf099;" horiz-adv-x="1664" d="M1620 1128q-67 -98 -162 -167q1 -14 1 -42q0 -130 -38 -259.5t-115.5 -248.5t-184.5 -210.5t-258 -146t-323 -54.5q-271 0 -496 145q35 -4 78 -4q225 0 401 138q-105 2 -188 64.5t-114 159.5q33 -5 61 -5q43 0 85 11q-112 23 -185.5 111.5t-73.5 205.5v4q68 -38 146 -41 q-66 44 -105 115t-39 154q0 88 44 163q121 -149 294.5 -238.5t371.5 -99.5q-8 38 -8 74q0 134 94.5 228.5t228.5 94.5q140 0 236 -102q109 21 205 78q-37 -115 -142 -178q93 10 186 50z" />
+<glyph unicode="&#xf09a;" horiz-adv-x="768" d="M511 980h257l-30 -284h-227v-824h-341v824h-170v284h170v171q0 182 86 275.5t283 93.5h227v-284h-142q-39 0 -62.5 -6.5t-34 -23.5t-13.5 -34.5t-3 -49.5v-142z" />
+<glyph unicode="&#xf09b;" d="M1536 640q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -39.5 7t-12.5 30v211q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5 q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 - [...]
+<glyph unicode="&#xf09c;" horiz-adv-x="1664" d="M1664 960v-256q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-192h96q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h672v192q0 185 131.5 316.5t316.5 131.5 t316.5 -131.5t131.5 -316.5z" />
+<glyph unicode="&#xf09d;" horiz-adv-x="1920" d="M1760 1408q66 0 113 -47t47 -113v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600zM160 1280q-13 0 -22.5 -9.5t-9.5 -22.5v-224h1664v224q0 13 -9.5 22.5t-22.5 9.5h-1600zM1760 0q13 0 22.5 9.5t9.5 22.5v608h-1664v-608 q0 -13 9.5 -22.5t22.5 -9.5h1600zM256 128v128h256v-128h-256zM640 128v128h384v-128h-384z" />
+<glyph unicode="&#xf09e;" horiz-adv-x="1408" d="M384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 69q2 -28 -17 -48q-18 -21 -47 -21h-135q-25 0 -43 16.5t-20 41.5q-22 229 -184.5 391.5t-391.5 184.5q-25 2 -41.5 20t-16.5 43v135q0 29 21 47q17 17 43 17h5q160 -13 306 -80.5 t259 -181.5q114 -113 181.5 -259t80.5 -306zM1408 67q2 -27 -18 -47q-18 -20 -46 -20h-143q-26 0 -44.5 17.5t-19.5 42.5q-12 215 -101 408.5t-231.5 336t-336 231.5t-408.5 102q-25 1 -42.5 19.5t-17.5 43.5 [...]
+<glyph unicode="&#xf0a0;" d="M1040 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1296 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1408 160v320q0 13 -9.5 22.5t-22.5 9.5 h-1216q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h1216q13 0 22.5 9.5t9.5 22.5zM178 640h1180l-157 482q-4 13 -16 21.5t-26 8.5h-782q-14 0 -26 -8.5t-16 -21.5zM1536 480v-320q0 -66 -47 -113t-113 -47h-1216q- [...]
+<glyph unicode="&#xf0a1;" horiz-adv-x="1792" d="M1664 896q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5v-384q0 -52 -38 -90t-90 -38q-417 347 -812 380q-58 -19 -91 -66t-31 -100.5t40 -92.5q-20 -33 -23 -65.5t6 -58t33.5 -55t48 -50t61.5 -50.5q-29 -58 -111.5 -83t-168.5 -11.5t-132 55.5q-7 23 -29.5 87.5 t-32 94.5t-23 89t-15 101t3.5 98.5t22 110.5h-122q-66 0 -113 47t-47 113v192q0 66 47 113t113 47h480q435 0 896 384q52 0 90 -38t38 -90v-384zM1536 292v954q-394 -302 -768 -343v-270q377 -42 768 -341z" />
+<glyph unicode="&#xf0a2;" horiz-adv-x="1664" d="M848 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM183 128h1298q-164 181 -246.5 411.5t-82.5 484.5q0 256 -320 256t-320 -256q0 -254 -82.5 -484.5t-246.5 -411.5zM1664 128q0 -52 -38 -90t-90 -38 h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q190 161 287 397.5t97 498.5q0 165 96 262t264 117q-8 18 -8 37q0 40 28 68t68 28t68 -28t28 -68q0 -19 -8 -37q168 -20 264 -117 [...]
+<glyph unicode="&#xf0a3;" d="M1376 640l138 -135q30 -28 20 -70q-12 -41 -52 -51l-188 -48l53 -186q12 -41 -19 -70q-29 -31 -70 -19l-186 53l-48 -188q-10 -40 -51 -52q-12 -2 -19 -2q-31 0 -51 22l-135 138l-135 -138q-28 -30 -70 -20q-41 11 -51 52l-48 188l-186 -53q-41 -12 -70 19q-31 29 -19 70 l53 186l-188 48q-40 10 -52 51q-10 42 20 70l138 135l-138 135q-30 28 -20 70q12 41 52 51l188 48l-53 186q-12 41 19 70q29 31 70 19l186 -53l48 188q10 41 51 51q41 12 70 -19l135 -139l135 139q29 30 70 19q41 -10 51 -51l48 [...]
+<glyph unicode="&#xf0a4;" horiz-adv-x="1792" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 768q0 51 -39 89.5t-89 38.5h-576q0 20 15 48.5t33 55t33 68t15 84.5q0 67 -44.5 97.5t-115.5 30.5q-24 0 -90 -139q-24 -44 -37 -65q-40 -64 -112 -145q-71 -81 -101 -106 q-69 -57 -140 -57h-32v-640h32q72 0 167 -32t193.5 -64t179.5 -32q189 0 189 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5h331q52 0 90 38t38 90zM1792 769q0 -105 -75.5 -181t-1 [...]
+<glyph unicode="&#xf0a5;" horiz-adv-x="1792" d="M1376 128h32v640h-32q-35 0 -67.5 12t-62.5 37t-50 46t-49 54q-2 3 -3.5 4.5t-4 4.5t-4.5 5q-72 81 -112 145q-14 22 -38 68q-1 3 -10.5 22.5t-18.5 36t-20 35.5t-21.5 30.5t-18.5 11.5q-71 0 -115.5 -30.5t-44.5 -97.5q0 -43 15 -84.5t33 -68t33 -55t15 -48.5h-576 q-50 0 -89 -38.5t-39 -89.5q0 -52 38 -90t90 -38h331q-15 -17 -25 -47.5t-10 -55.5q0 -69 53 -119q-18 -32 -18 -69t17.5 -73.5t47.5 -52.5q-4 -24 -4 -56q0 -85 48.5 -126t135.5 -41q84 0 183 32t194 64t167 32z [...]
+<glyph unicode="&#xf0a6;" d="M1280 -64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 700q0 189 -167 189q-26 0 -56 -5q-16 30 -52.5 47.5t-73.5 17.5t-69 -18q-50 53 -119 53q-25 0 -55.5 -10t-47.5 -25v331q0 52 -38 90t-90 38q-51 0 -89.5 -39t-38.5 -89v-576 q-20 0 -48.5 15t-55 33t-68 33t-84.5 15q-67 0 -97.5 -44.5t-30.5 -115.5q0 -24 139 -90q44 -24 65 -37q64 -40 145 -112q81 -71 106 -101q57 -69 57 -140v-32h640v32q0 72 32 167t64 193.5t32 179.5zM1536 705q0 -133 -69 -322q-59 -164 - [...]
+<glyph unicode="&#xf0a7;" d="M1408 576q0 84 -32 183t-64 194t-32 167v32h-640v-32q0 -35 -12 -67.5t-37 -62.5t-46 -50t-54 -49q-9 -8 -14 -12q-81 -72 -145 -112q-22 -14 -68 -38q-3 -1 -22.5 -10.5t-36 -18.5t-35.5 -20t-30.5 -21.5t-11.5 -18.5q0 -71 30.5 -115.5t97.5 -44.5q43 0 84.5 15t68 33 t55 33t48.5 15v-576q0 -50 38.5 -89t89.5 -39q52 0 90 38t38 90v331q46 -35 103 -35q69 0 119 53q32 -18 69 -18t73.5 17.5t52.5 47.5q24 -4 56 -4q85 0 126 48.5t41 135.5zM1280 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -4 [...]
+<glyph unicode="&#xf0a8;" d="M1280 576v128q0 26 -19 45t-45 19h-502l189 189q19 19 19 45t-19 45l-91 91q-18 18 -45 18t-45 -18l-362 -362l-91 -91q-18 -18 -18 -45t18 -45l91 -91l362 -362q18 -18 45 -18t45 18l91 91q18 18 18 45t-18 45l-189 189h502q26 0 45 19t19 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf0a9;" d="M1285 640q0 27 -18 45l-91 91l-362 362q-18 18 -45 18t-45 -18l-91 -91q-18 -18 -18 -45t18 -45l189 -189h-502q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h502l-189 -189q-19 -19 -19 -45t19 -45l91 -91q18 -18 45 -18t45 18l362 362l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf0aa;" d="M1284 641q0 27 -18 45l-362 362l-91 91q-18 18 -45 18t-45 -18l-91 -91l-362 -362q-18 -18 -18 -45t18 -45l91 -91q18 -18 45 -18t45 18l189 189v-502q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v502l189 -189q19 -19 45 -19t45 19l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf0ab;" d="M1284 639q0 27 -18 45l-91 91q-18 18 -45 18t-45 -18l-189 -189v502q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-502l-189 189q-19 19 -45 19t-45 -19l-91 -91q-18 -18 -18 -45t18 -45l362 -362l91 -91q18 -18 45 -18t45 18l91 91l362 362q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf0ac;" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1042 887q-2 -1 -9.5 -9.5t-13.5 -9.5q2 0 4.5 5t5 11t3.5 7q6 7 22 15q14 6 52 12q34 8 51 -11 q-2 2 9.5 13t14.5 12q3 2 15 4.5t15 7.5l2 22q-12 -1 -17.5 7t-6.5 21q0 -2 -6 -8q0 7 -4.5 8t-11.5 -1t-9 -1q-10 3 -15 7.5t-8 16.5t-4 15q-2 5 -9.5 10.5t-9.5 10.5q-1 2 -2.5 5.5t-3 6.5t-4 5.5t-5.5 2.5t-7 -5t-7.5 -10t-4.5 -5 [...]
+<glyph unicode="&#xf0ad;" horiz-adv-x="1664" d="M384 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1028 484l-682 -682q-37 -37 -90 -37q-52 0 -91 37l-106 108q-38 36 -38 90q0 53 38 91l681 681q39 -98 114.5 -173.5t173.5 -114.5zM1662 919q0 -39 -23 -106q-47 -134 -164.5 -217.5 t-258.5 -83.5q-185 0 -316.5 131.5t-131.5 316.5t131.5 316.5t316.5 131.5q58 0 121.5 -16.5t107.5 -46.5q16 -11 16 -28t-16 -28l-293 -169v-224l193 -107q5 3 79 48.5t135.5 81t70.5 35.5q15 0 23.5 -10t8.5 -25z" />
+<glyph unicode="&#xf0ae;" horiz-adv-x="1792" d="M1024 128h640v128h-640v-128zM640 640h1024v128h-1024v-128zM1280 1152h384v128h-384v-128zM1792 320v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 832v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19 t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf0b0;" horiz-adv-x="1408" d="M1403 1241q17 -41 -14 -70l-493 -493v-742q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-256 256q-19 19 -19 45v486l-493 493q-31 29 -14 70q17 39 59 39h1280q42 0 59 -39z" />
+<glyph unicode="&#xf0b1;" horiz-adv-x="1792" d="M640 1280h512v128h-512v-128zM1792 640v-480q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v480h672v-160q0 -26 19 -45t45 -19h320q26 0 45 19t19 45v160h672zM1024 640v-128h-256v128h256zM1792 1120v-384h-1792v384q0 66 47 113t113 47h352v160q0 40 28 68 t68 28h576q40 0 68 -28t28 -68v-160h352q66 0 113 -47t47 -113z" />
+<glyph unicode="&#xf0b2;" d="M1283 995l-355 -355l355 -355l144 144q29 31 70 14q39 -17 39 -59v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l144 144l-355 355l-355 -355l144 -144q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l144 -144 l355 355l-355 355l-144 -144q-19 -19 -45 -19q-12 0 -24 5q-40 17 -40 59v448q0 26 19 45t45 19h448q42 0 59 -40q17 -39 -14 -69l-144 -144l355 -355l355 355l-144 144q-31 30 -14 69q17 40 59 40h448q26 0 45 -19t19 -45v-448q0  [...]
+<glyph unicode="&#xf0c0;" horiz-adv-x="1920" d="M593 640q-162 -5 -265 -128h-134q-82 0 -138 40.5t-56 118.5q0 353 124 353q6 0 43.5 -21t97.5 -42.5t119 -21.5q67 0 133 23q-5 -37 -5 -66q0 -139 81 -256zM1664 3q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q10 0 43 -21.5t73 -48t107 -48t135 -21.5t135 21.5t107 48t73 48t43 21.5q61 0 111.5 -20t85.5 -53.5t62 -81t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM640 1280q0 -106 -7 [...]
+<glyph unicode="&#xf0c1;" horiz-adv-x="1664" d="M1456 320q0 40 -28 68l-208 208q-28 28 -68 28q-42 0 -72 -32q3 -3 19 -18.5t21.5 -21.5t15 -19t13 -25.5t3.5 -27.5q0 -40 -28 -68t-68 -28q-15 0 -27.5 3.5t-25.5 13t-19 15t-21.5 21.5t-18.5 19q-33 -31 -33 -73q0 -40 28 -68l206 -207q27 -27 68 -27q40 0 68 26 l147 146q28 28 28 67zM753 1025q0 40 -28 68l-206 207q-28 28 -68 28q-39 0 -68 -27l-147 -146q-28 -28 -28 -67q0 -40 28 -68l208 -208q27 -27 68 -27q42 0 72 31q-3 3 -19 18.5t-21.5 21.5t-15 19t-13 25.5t-3. [...]
+<glyph unicode="&#xf0c2;" horiz-adv-x="1920" d="M1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088q-185 0 -316.5 131.5t-131.5 316.5q0 132 71 241.5t187 163.5q-2 28 -2 43q0 212 150 362t362 150q158 0 286.5 -88t187.5 -230q70 62 166 62q106 0 181 -75t75 -181q0 -75 -41 -138q129 -30 213 -134.5t84 -239.5z " />
+<glyph unicode="&#xf0c3;" horiz-adv-x="1664" d="M1527 88q56 -89 21.5 -152.5t-140.5 -63.5h-1152q-106 0 -140.5 63.5t21.5 152.5l503 793v399h-64q-26 0 -45 19t-19 45t19 45t45 19h512q26 0 45 -19t19 -45t-19 -45t-45 -19h-64v-399zM748 813l-272 -429h712l-272 429l-20 31v37v399h-128v-399v-37z" />
+<glyph unicode="&#xf0c4;" horiz-adv-x="1792" d="M960 640q26 0 45 -19t19 -45t-19 -45t-45 -19t-45 19t-19 45t19 45t45 19zM1260 576l507 -398q28 -20 25 -56q-5 -35 -35 -51l-128 -64q-13 -7 -29 -7q-17 0 -31 8l-690 387l-110 -66q-8 -4 -12 -5q14 -49 10 -97q-7 -77 -56 -147.5t-132 -123.5q-132 -84 -277 -84 q-136 0 -222 78q-90 84 -79 207q7 76 56 147t131 124q132 84 278 84q83 0 151 -31q9 13 22 22l122 73l-122 73q-13 9 -22 22q-68 -31 -151 -31q-146 0 -278 84q-82 53 -131 124t-56 147q-5 59 15.5 113t63.5 93q85 [...]
+<glyph unicode="&#xf0c5;" horiz-adv-x="1792" d="M1696 1152q40 0 68 -28t28 -68v-1216q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v288h-544q-40 0 -68 28t-28 68v672q0 40 20 88t48 76l408 408q28 28 76 48t88 20h416q40 0 68 -28t28 -68v-328q68 40 128 40h416zM1152 939l-299 -299h299v299zM512 1323l-299 -299 h299v299zM708 676l316 316v416h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h512v256q0 40 20 88t48 76zM1664 -128v1152h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h896z" />
+<glyph unicode="&#xf0c6;" horiz-adv-x="1408" d="M1404 151q0 -117 -79 -196t-196 -79q-135 0 -235 100l-777 776q-113 115 -113 271q0 159 110 270t269 111q158 0 273 -113l605 -606q10 -10 10 -22q0 -16 -30.5 -46.5t-46.5 -30.5q-13 0 -23 10l-606 607q-79 77 -181 77q-106 0 -179 -75t-73 -181q0 -105 76 -181 l776 -777q63 -63 145 -63q64 0 106 42t42 106q0 82 -63 145l-581 581q-26 24 -60 24q-29 0 -48 -19t-19 -48q0 -32 25 -59l410 -410q10 -10 10 -22q0 -16 -31 -47t-47 -31q-12 0 -22 10l-410 410q-63 61 -63 149q0  [...]
+<glyph unicode="&#xf0c7;" d="M384 0h768v384h-768v-384zM1280 0h128v896q0 14 -10 38.5t-20 34.5l-281 281q-10 10 -34 20t-39 10v-416q0 -40 -28 -68t-68 -28h-576q-40 0 -68 28t-28 68v416h-128v-1280h128v416q0 40 28 68t68 28h832q40 0 68 -28t28 -68v-416zM896 928v320q0 13 -9.5 22.5t-22.5 9.5 h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5zM1536 896v-928q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h928q40 0 88 -20t76 -48l280 -280q28 [...]
+<glyph unicode="&#xf0c8;" d="M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf0c9;" d="M1536 192v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 704v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 1216v-128q0 -26 -19 -45 t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf0ca;" horiz-adv-x="1792" d="M384 128q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 640q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5 t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5zM384 1152q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22. [...]
+<glyph unicode="&#xf0cb;" horiz-adv-x="1792" d="M381 -84q0 -80 -54.5 -126t-135.5 -46q-106 0 -172 66l57 88q49 -45 106 -45q29 0 50.5 14.5t21.5 42.5q0 64 -105 56l-26 56q8 10 32.5 43.5t42.5 54t37 38.5v1q-16 0 -48.5 -1t-48.5 -1v-53h-106v152h333v-88l-95 -115q51 -12 81 -49t30 -88zM383 543v-159h-362 q-6 36 -6 54q0 51 23.5 93t56.5 68t66 47.5t56.5 43.5t23.5 45q0 25 -14.5 38.5t-39.5 13.5q-46 0 -81 -58l-85 59q24 51 71.5 79.5t105.5 28.5q73 0 123 -41.5t50 -112.5q0 -50 -34 -91.5t-75 -64.5t-75.5 -50.5t- [...]
+<glyph unicode="&#xf0cc;" horiz-adv-x="1792" d="M1760 640q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1728q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h1728zM483 704q-28 35 -51 80q-48 97 -48 188q0 181 134 309q133 127 393 127q50 0 167 -19q66 -12 177 -48q10 -38 21 -118q14 -123 14 -183q0 -18 -5 -45l-12 -3l-84 6 l-14 2q-50 149 -103 205q-88 91 -210 91q-114 0 -182 -59q-67 -58 -67 -146q0 -73 66 -140t279 -129q69 -20 173 -66q58 -28 95 -52h-743zM990 448h411q7 -39 7 -92q0 -111 -41 -212q-23 -55 -71 -104q-37 -3 [...]
+<glyph unicode="&#xf0cd;" d="M48 1313q-37 2 -45 4l-3 88q13 1 40 1q60 0 112 -4q132 -7 166 -7q86 0 168 3q116 4 146 5q56 0 86 2l-1 -14l2 -64v-9q-60 -9 -124 -9q-60 0 -79 -25q-13 -14 -13 -132q0 -13 0.5 -32.5t0.5 -25.5l1 -229l14 -280q6 -124 51 -202q35 -59 96 -92q88 -47 177 -47 q104 0 191 28q56 18 99 51q48 36 65 64q36 56 53 114q21 73 21 229q0 79 -3.5 128t-11 122.5t-13.5 159.5l-4 59q-5 67 -24 88q-34 35 -77 34l-100 -2l-14 3l2 86h84l205 -10q76 -3 196 10l18 -2q6 -38 6 -51q0 -7 -4 -31q-45 -12 -84 -1 [...]
+<glyph unicode="&#xf0ce;" horiz-adv-x="1664" d="M512 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23 v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -2 [...]
+<glyph unicode="&#xf0d0;" horiz-adv-x="1664" d="M1190 955l293 293l-107 107l-293 -293zM1637 1248q0 -27 -18 -45l-1286 -1286q-18 -18 -45 -18t-45 18l-198 198q-18 18 -18 45t18 45l1286 1286q18 18 45 18t45 -18l198 -198q18 -18 18 -45zM286 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM636 1276 l196 -60l-196 -60l-60 -196l-60 196l-196 60l196 60l60 196zM1566 798l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM926 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98z" />
+<glyph unicode="&#xf0d1;" horiz-adv-x="1792" d="M640 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM256 640h384v256h-158q-13 0 -22 -9l-195 -195q-9 -9 -9 -22v-30zM1536 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1792 1216v-1024q0 -15 -4 -26.5t-13.5 -18.5 t-16.5 -11.5t-23.5 -6t-22.5 -2t-25.5 0t-22.5 0.5q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-64q-3 0 -22.5 -0.5t-25.5 0t-22.5 2t-23.5 6t-16.5 11.5t-13.5  [...]
+<glyph unicode="&#xf0d2;" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103q-111 0 -218 32q59 93 78 164q9 34 54 211q20 -39 73 -67.5t114 -28.5q121 0 216 68.5t147 188.5t52 270q0 114 -59.5 214t-172.5 163t-255 63q-105 0 -196 -29t-154.5 -77t-109 -110.5t-67 -129.5t-21.5 -134 q0 -104 40 -183t117 -111q30 -12 38 20q2 7 8 31t8 30q6 23 -11 43q-51 61 -51 151q0 151 104.5 259.5t273.5 108.5q151 0 235.5 -82t84.5 -213q0 -170 -68.5 -289t-175.5 -119q-61 0 -98 43.5t-23 104.5q8 35 26.5 93.5t30 103t11 [...]
+<glyph unicode="&#xf0d3;" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-725q85 122 108 210q9 34 53 209q21 -39 73.5 -67t112.5 -28q181 0 295.5 147.5t114.5 373.5q0 84 -35 162.5t-96.5 139t-152.5 97t-197 36.5q-104 0 -194.5 -28.5t-153 -76.5 t-107.5 -109.5t-66.5 -128t-21.5 -132.5q0 -102 39.5 -180t116.5 -110q13 -5 23.5 0t14.5 19q10 44 15 61q6 23 -11 42q-50 62 -50 150q0 150 103.5 256.5t270.5 106.5q149 0 232.5 -81t83.5 -210q0 -168 -67.5 -286t-173.5 -118q-60 0 - [...]
+<glyph unicode="&#xf0d4;" d="M678 -57q0 -38 -10 -71h-380q-95 0 -171.5 56.5t-103.5 147.5q24 45 69 77.5t100 49.5t107 24t107 7q32 0 49 -2q6 -4 30.5 -21t33 -23t31 -23t32 -25.5t27.5 -25.5t26.5 -29.5t21 -30.5t17.5 -34.5t9.5 -36t4.5 -40.5zM385 294q-234 -7 -385 -85v433q103 -118 273 -118 q32 0 70 5q-21 -61 -21 -86q0 -67 63 -149zM558 805q0 -100 -43.5 -160.5t-140.5 -60.5q-51 0 -97 26t-78 67.5t-56 93.5t-35.5 104t-11.5 99q0 96 51.5 165t144.5 69q66 0 119 -41t84 -104t47 -130t16 -128zM1536 896v-736q0 -1 [...]
+<glyph unicode="&#xf0d5;" horiz-adv-x="1664" d="M876 71q0 21 -4.5 40.5t-9.5 36t-17.5 34.5t-21 30.5t-26.5 29.5t-27.5 25.5t-32 25.5t-31 23t-33 23t-30.5 21q-17 2 -50 2q-54 0 -106 -7t-108 -25t-98 -46t-69 -75t-27 -107q0 -68 35.5 -121.5t93 -84t120.5 -45.5t127 -15q59 0 112.5 12.5t100.5 39t74.5 73.5 t27.5 110zM756 933q0 60 -16.5 127.5t-47 130.5t-84 104t-119.5 41q-93 0 -144 -69t-51 -165q0 -47 11.5 -99t35.5 -104t56 -93.5t78 -67.5t97 -26q97 0 140.5 60.5t43.5 160.5zM625 1408h437l-135 -79h-135q71 -45 [...]
+<glyph unicode="&#xf0d6;" horiz-adv-x="1920" d="M768 384h384v96h-128v448h-114l-148 -137l77 -80q42 37 55 57h2v-288h-128v-96zM1280 640q0 -70 -21 -142t-59.5 -134t-101.5 -101t-138 -39t-138 39t-101.5 101t-59.5 134t-21 142t21 142t59.5 134t101.5 101t138 39t138 -39t101.5 -101t59.5 -134t21 -142zM1792 384 v512q-106 0 -181 75t-75 181h-1152q0 -106 -75 -181t-181 -75v-512q106 0 181 -75t75 -181h1152q0 106 75 181t181 75zM1920 1216v-1152q0 -26 -19 -45t-45 -19h-1792q-26 0 -45 19t-19 45v1152q0 26 19 45t45  [...]
+<glyph unicode="&#xf0d7;" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf0d8;" horiz-adv-x="1024" d="M1024 320q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" />
+<glyph unicode="&#xf0d9;" horiz-adv-x="640" d="M640 1088v-896q0 -26 -19 -45t-45 -19t-45 19l-448 448q-19 19 -19 45t19 45l448 448q19 19 45 19t45 -19t19 -45z" />
+<glyph unicode="&#xf0da;" horiz-adv-x="640" d="M576 640q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19t-19 45v896q0 26 19 45t45 19t45 -19l448 -448q19 -19 19 -45z" />
+<glyph unicode="&#xf0db;" horiz-adv-x="1664" d="M160 0h608v1152h-640v-1120q0 -13 9.5 -22.5t22.5 -9.5zM1536 32v1120h-640v-1152h608q13 0 22.5 9.5t9.5 22.5zM1664 1248v-1216q0 -66 -47 -113t-113 -47h-1344q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1344q66 0 113 -47t47 -113z" />
+<glyph unicode="&#xf0dc;" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45zM1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" />
+<glyph unicode="&#xf0dd;" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf0de;" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" />
+<glyph unicode="&#xf0e0;" horiz-adv-x="1792" d="M1792 826v-794q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v794q44 -49 101 -87q362 -246 497 -345q57 -42 92.5 -65.5t94.5 -48t110 -24.5h1h1q51 0 110 24.5t94.5 48t92.5 65.5q170 123 498 345q57 39 100 87zM1792 1120q0 -79 -49 -151t-122 -123 q-376 -261 -468 -325q-10 -7 -42.5 -30.5t-54 -38t-52 -32.5t-57.5 -27t-50 -9h-1h-1q-23 0 -50 9t-57.5 27t-52 32.5t-54 38t-42.5 30.5q-91 64 -262 182.5t-205 142.5q-62 42 -117 115.5t-55 136.5q0 78 41.5 130t11 [...]
+<glyph unicode="&#xf0e1;" d="M349 911v-991h-330v991h330zM370 1217q1 -73 -50.5 -122t-135.5 -49h-2q-82 0 -132 49t-50 122q0 74 51.5 122.5t134.5 48.5t133 -48.5t51 -122.5zM1536 488v-568h-329v530q0 105 -40.5 164.5t-126.5 59.5q-63 0 -105.5 -34.5t-63.5 -85.5q-11 -30 -11 -81v-553h-329 q2 399 2 647t-1 296l-1 48h329v-144h-2q20 32 41 56t56.5 52t87 43.5t114.5 15.5q171 0 275 -113.5t104 -332.5z" />
+<glyph unicode="&#xf0e2;" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-172 0 -327 72.5t-264 204.5q-7 10 -6.5 22.5t8.5 20.5l137 138q10 9 25 9q16 -2 23 -12q73 -95 179 -147t225 -52q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5 t-163.5 109.5t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 - [...]
+<glyph unicode="&#xf0e3;" horiz-adv-x="1792" d="M1771 0q0 -53 -37 -90l-107 -108q-39 -37 -91 -37q-53 0 -90 37l-363 364q-38 36 -38 90q0 53 43 96l-256 256l-126 -126q-14 -14 -34 -14t-34 14q2 -2 12.5 -12t12.5 -13t10 -11.5t10 -13.5t6 -13.5t5.5 -16.5t1.5 -18q0 -38 -28 -68q-3 -3 -16.5 -18t-19 -20.5 t-18.5 -16.5t-22 -15.5t-22 -9t-26 -4.5q-40 0 -68 28l-408 408q-28 28 -28 68q0 13 4.5 26t9 22t15.5 22t16.5 18.5t20.5 19t18 16.5q30 28 68 28q10 0 18 -1.5t16.5 -5.5t13.5 -6t13.5 -10t11.5 -10t13 -12.5t12 - [...]
+<glyph unicode="&#xf0e4;" horiz-adv-x="1792" d="M384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM576 832q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1004 351l101 382q6 26 -7.5 48.5t-38.5 29.5 t-48 -6.5t-30 -39.5l-101 -382q-60 -5 -107 -43.5t-63 -98.5q-20 -77 20 -146t117 -89t146 20t89 117q16 60 -6 117t-72 91zM1664 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37 [...]
+<glyph unicode="&#xf0e5;" horiz-adv-x="1792" d="M896 1152q-204 0 -381.5 -69.5t-282 -187.5t-104.5 -255q0 -112 71.5 -213.5t201.5 -175.5l87 -50l-27 -96q-24 -91 -70 -172q152 63 275 171l43 38l57 -6q69 -8 130 -8q204 0 381.5 69.5t282 187.5t104.5 255t-104.5 255t-282 187.5t-381.5 69.5zM1792 640 q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22h-5q-15 0 -27 10.5t-16 27.5v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26  [...]
+<glyph unicode="&#xf0e6;" horiz-adv-x="1792" d="M704 1152q-153 0 -286 -52t-211.5 -141t-78.5 -191q0 -82 53 -158t149 -132l97 -56l-35 -84q34 20 62 39l44 31l53 -10q78 -14 153 -14q153 0 286 52t211.5 141t78.5 191t-78.5 191t-211.5 141t-286 52zM704 1280q191 0 353.5 -68.5t256.5 -186.5t94 -257t-94 -257 t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q [...]
+<glyph unicode="&#xf0e7;" horiz-adv-x="896" d="M885 970q18 -20 7 -44l-540 -1157q-13 -25 -42 -25q-4 0 -14 2q-17 5 -25.5 19t-4.5 30l197 808l-406 -101q-4 -1 -12 -1q-18 0 -31 11q-18 15 -13 39l201 825q4 14 16 23t28 9h328q19 0 32 -12.5t13 -29.5q0 -8 -5 -18l-171 -463l396 98q8 2 12 2q19 0 34 -15z" />
+<glyph unicode="&#xf0e8;" horiz-adv-x="1792" d="M1792 288v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320 q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192q0 52 38 90t90 38h512v192h-96q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h320q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-96v-192h [...]
+<glyph unicode="&#xf0e9;" horiz-adv-x="1664" d="M896 708v-580q0 -104 -76 -180t-180 -76t-180 76t-76 180q0 26 19 45t45 19t45 -19t19 -45q0 -50 39 -89t89 -39t89 39t39 89v580q33 11 64 11t64 -11zM1664 681q0 -13 -9.5 -22.5t-22.5 -9.5q-11 0 -23 10q-49 46 -93 69t-102 23q-68 0 -128 -37t-103 -97 q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -28 -17q-18 0 -29 17q-4 6 -14.5 24t-17.5 28q-43 60 -102.5 97t-127.5 37t-127.5 -37t-102.5 -97q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -29 -17q-17 0 -28 17q-4 6 -14.5 24t-17.5 2 [...]
+<glyph unicode="&#xf0ea;" horiz-adv-x="1792" d="M768 -128h896v640h-416q-40 0 -68 28t-28 68v416h-384v-1152zM1024 1312v64q0 13 -9.5 22.5t-22.5 9.5h-704q-13 0 -22.5 -9.5t-9.5 -22.5v-64q0 -13 9.5 -22.5t22.5 -9.5h704q13 0 22.5 9.5t9.5 22.5zM1280 640h299l-299 299v-299zM1792 512v-672q0 -40 -28 -68t-68 -28 h-960q-40 0 -68 28t-28 68v160h-544q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1088q40 0 68 -28t28 -68v-328q21 -13 36 -28l408 -408q28 -28 48 -76t20 -88z" />
+<glyph unicode="&#xf0eb;" horiz-adv-x="1024" d="M736 960q0 -13 -9.5 -22.5t-22.5 -9.5t-22.5 9.5t-9.5 22.5q0 46 -54 71t-106 25q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5q50 0 99.5 -16t87 -54t37.5 -90zM896 960q0 72 -34.5 134t-90 101.5t-123 62t-136.5 22.5t-136.5 -22.5t-123 -62t-90 -101.5t-34.5 -134 q0 -101 68 -180q10 -11 30.5 -33t30.5 -33q128 -153 141 -298h228q13 145 141 298q10 11 30.5 33t30.5 33q68 79 68 180zM1024 960q0 -155 -103 -268q-45 -49 -74.5 -87t-59.5 -95.5t-34 -107.5q47 -28 47 -82q [...]
+<glyph unicode="&#xf0ec;" horiz-adv-x="1792" d="M1792 352v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5q-12 0 -24 10l-319 320q-9 9 -9 22q0 14 9 23l320 320q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5v-192h1376q13 0 22.5 -9.5t9.5 -22.5zM1792 896q0 -14 -9 -23l-320 -320q-9 -9 -23 -9 q-13 0 -22.5 9.5t-9.5 22.5v192h-1376q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1376v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" />
+<glyph unicode="&#xf0ed;" horiz-adv-x="1920" d="M1280 608q0 14 -9 23t-23 9h-224v352q0 13 -9.5 22.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-352h-224q-13 0 -22.5 -9.5t-9.5 -22.5q0 -14 9 -23l352 -352q9 -9 23 -9t23 9l351 351q10 12 10 24zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" />
+<glyph unicode="&#xf0ee;" horiz-adv-x="1920" d="M1280 672q0 14 -9 23l-352 352q-9 9 -23 9t-23 -9l-351 -351q-10 -12 -10 -24q0 -14 9 -23t23 -9h224v-352q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5v352h224q13 0 22.5 9.5t9.5 22.5zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" />
+<glyph unicode="&#xf0f0;" horiz-adv-x="1408" d="M384 192q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 68 5.5 131t24 138t47.5 132.5t81 103t120 60.5q-22 -52 -22 -120v-203q-58 -20 -93 -70t-35 -111q0 -80 56 -136t136 -56 t136 56t56 136q0 61 -35.5 111t-92.5 70v203q0 62 25 93q132 -104 295 -104t295 104q25 -31 25 -93v-64q-106 0 -181 -75t-75 -181v-89q-32 -29 -32 -71q0 -40 28 -68t68 -28t68 28t28 68q0 42 -32 71v89q0 52 [...]
+<glyph unicode="&#xf0f1;" horiz-adv-x="1408" d="M1280 832q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 832q0 -62 -35.5 -111t-92.5 -70v-395q0 -159 -131.5 -271.5t-316.5 -112.5t-316.5 112.5t-131.5 271.5v132q-164 20 -274 128t-110 252v512q0 26 19 45t45 19q6 0 16 -2q17 30 47 48 t65 18q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5q-33 0 -64 18v-402q0 -106 94 -181t226 -75t226 75t94 181v402q-31 -18 -64 -18q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5q35 0 65 -18t47 - [...]
+<glyph unicode="&#xf0f2;" horiz-adv-x="1792" d="M640 1152h512v128h-512v-128zM288 1152v-1280h-64q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h64zM1408 1152v-1280h-1024v1280h128v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h128zM1792 928v-832q0 -92 -66 -158t-158 -66h-64v1280h64q92 0 158 -66 t66 -158z" />
+<glyph unicode="&#xf0f3;" horiz-adv-x="1664" d="M848 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM1664 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q190 161 287 397.5t97 498.5 q0 165 96 262t264 117q-8 18 -8 37q0 40 28 68t68 28t68 -28t28 -68q0 -19 -8 -37q168 -20 264 -117t96 -262q0 -262 97 -498.5t287 -397.5z" />
+<glyph unicode="&#xf0f4;" horiz-adv-x="1920" d="M1664 896q0 80 -56 136t-136 56h-64v-384h64q80 0 136 56t56 136zM0 128h1792q0 -106 -75 -181t-181 -75h-1280q-106 0 -181 75t-75 181zM1856 896q0 -159 -112.5 -271.5t-271.5 -112.5h-64v-32q0 -92 -66 -158t-158 -66h-704q-92 0 -158 66t-66 158v736q0 26 19 45 t45 19h1152q159 0 271.5 -112.5t112.5 -271.5z" />
+<glyph unicode="&#xf0f5;" horiz-adv-x="1408" d="M640 1472v-640q0 -61 -35.5 -111t-92.5 -70v-779q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v779q-57 20 -92.5 70t-35.5 111v640q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45 t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45zM1408 1472v-1600q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v512h-224q-13 0 -22.5 9.5t-9.5 22.5v800q0 132 94 226t226 94h256q26 0 45 -19t1 [...]
+<glyph unicode="&#xf0f6;" horiz-adv-x="1280" d="M1024 352v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704q14 0 23 -9t9 -23zM1024 608v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704q14 0 23 -9t9 -23zM128 0h1024v768h-416q-40 0 -68 28t-28 68v416h-512v-1280z M768 896h376q-10 29 -22 41l-313 313q-12 12 -41 22v-376zM1280 864v-896q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h640q40 0 88 -20t76 -48l312 -312q28 -28 48 -76t20 -88z" />
+<glyph unicode="&#xf0f7;" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22 [...]
+<glyph unicode="&#xf0f8;" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22 [...]
+<glyph unicode="&#xf0f9;" horiz-adv-x="1920" d="M640 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM256 640h384v256h-158q-14 -2 -22 -9l-195 -195q-7 -12 -9 -22v-30zM1536 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1664 800v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23  [...]
+<glyph unicode="&#xf0fa;" horiz-adv-x="1792" d="M1280 416v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM640 1152h512v128h-512v-128zM256 1152v-1280h-32 q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h32zM1440 1152v-1280h-1088v1280h160v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h160zM1792 928v-832q0 -92 -66 -158t-158 -66h-32v1280h32q92 0 15 [...]
+<glyph unicode="&#xf0fb;" horiz-adv-x="1920" d="M1920 576q-1 -32 -288 -96l-352 -32l-224 -64h-64l-293 -352h69q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-96h-160h-64v32h64v416h-160l-192 -224h-96l-32 32v192h32v32h128v8l-192 24v128l192 24v8h-128v32h-32v192l32 32h96l192 -224h160v416h-64v32h64h160h96 q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-69l293 -352h64l224 -64l352 -32q261 -58 287 -93z" />
+<glyph unicode="&#xf0fc;" horiz-adv-x="1664" d="M640 640v384h-256v-256q0 -53 37.5 -90.5t90.5 -37.5h128zM1664 192v-192h-1152v192l128 192h-128q-159 0 -271.5 112.5t-112.5 271.5v320l-64 64l32 128h480l32 128h960l32 -192l-64 -32v-800z" />
+<glyph unicode="&#xf0fd;" d="M1280 192v896q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-512v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-896q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h512v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf0fe;" d="M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf100;" horiz-adv-x="1024" d="M627 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23zM1011 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23z" />
+<glyph unicode="&#xf101;" horiz-adv-x="1024" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM979 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23 l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" />
+<glyph unicode="&#xf102;" horiz-adv-x="1152" d="M1075 224q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM1075 608q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393 q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" />
+<glyph unicode="&#xf103;" horiz-adv-x="1152" d="M1075 672q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23zM1075 1056q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" />
+<glyph unicode="&#xf104;" horiz-adv-x="640" d="M627 992q0 -13 -10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" />
+<glyph unicode="&#xf105;" horiz-adv-x="640" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" />
+<glyph unicode="&#xf106;" horiz-adv-x="1152" d="M1075 352q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" />
+<glyph unicode="&#xf107;" horiz-adv-x="1152" d="M1075 800q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" />
+<glyph unicode="&#xf108;" horiz-adv-x="1920" d="M1792 544v832q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-832q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5zM1920 1376v-1088q0 -66 -47 -113t-113 -47h-544q0 -37 16 -77.5t32 -71t16 -43.5q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19 t-19 45q0 14 16 44t32 70t16 78h-544q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
+<glyph unicode="&#xf109;" horiz-adv-x="1920" d="M416 256q-66 0 -113 47t-47 113v704q0 66 47 113t113 47h1088q66 0 113 -47t47 -113v-704q0 -66 -47 -113t-113 -47h-1088zM384 1120v-704q0 -13 9.5 -22.5t22.5 -9.5h1088q13 0 22.5 9.5t9.5 22.5v704q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5z M1760 192h160v-96q0 -40 -47 -68t-113 -28h-1600q-66 0 -113 28t-47 68v96h160h1600zM1040 96q16 0 16 16t-16 16h-160q-16 0 -16 -16t16 -16h160z" />
+<glyph unicode="&#xf10a;" horiz-adv-x="1152" d="M640 128q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1024 288v960q0 13 -9.5 22.5t-22.5 9.5h-832q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h832q13 0 22.5 9.5t9.5 22.5zM1152 1248v-1088q0 -66 -47 -113t-113 -47h-832 q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h832q66 0 113 -47t47 -113z" />
+<glyph unicode="&#xf10b;" horiz-adv-x="768" d="M464 128q0 33 -23.5 56.5t-56.5 23.5t-56.5 -23.5t-23.5 -56.5t23.5 -56.5t56.5 -23.5t56.5 23.5t23.5 56.5zM672 288v704q0 13 -9.5 22.5t-22.5 9.5h-512q-13 0 -22.5 -9.5t-9.5 -22.5v-704q0 -13 9.5 -22.5t22.5 -9.5h512q13 0 22.5 9.5t9.5 22.5zM480 1136 q0 16 -16 16h-160q-16 0 -16 -16t16 -16h160q16 0 16 16zM768 1152v-1024q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v1024q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" />
+<glyph unicode="&#xf10c;" d="M768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103 t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf10d;" horiz-adv-x="1664" d="M768 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z M1664 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75  [...]
+<glyph unicode="&#xf10e;" horiz-adv-x="1664" d="M768 1216v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136zM1664 1216 v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 [...]
+<glyph unicode="&#xf110;" horiz-adv-x="1568" d="M496 192q0 -60 -42.5 -102t-101.5 -42q-60 0 -102 42t-42 102t42 102t102 42q59 0 101.5 -42t42.5 -102zM928 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM320 640q0 -66 -47 -113t-113 -47t-113 47t-47 113 t47 113t113 47t113 -47t47 -113zM1360 192q0 -46 -33 -79t-79 -33t-79 33t-33 79t33 79t79 33t79 -33t33 -79zM528 1088q0 -73 -51.5 -124.5t-124.5 -51.5t-124.5 51.5t-51.5 124.5t51.5 124.5t124.5 51.5t124.5 [...]
+<glyph unicode="&#xf111;" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf112;" horiz-adv-x="1792" d="M1792 416q0 -166 -127 -451q-3 -7 -10.5 -24t-13.5 -30t-13 -22q-12 -17 -28 -17q-15 0 -23.5 10t-8.5 25q0 9 2.5 26.5t2.5 23.5q5 68 5 123q0 101 -17.5 181t-48.5 138.5t-80 101t-105.5 69.5t-133 42.5t-154 21.5t-175.5 6h-224v-256q0 -26 -19 -45t-45 -19t-45 19 l-512 512q-19 19 -19 45t19 45l512 512q19 19 45 19t45 -19t19 -45v-256h224q713 0 875 -403q53 -134 53 -333z" />
+<glyph unicode="&#xf113;" horiz-adv-x="1664" d="M640 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1280 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1440 320 q0 120 -69 204t-187 84q-41 0 -195 -21q-71 -11 -157 -11t-157 11q-152 21 -195 21q-118 0 -187 -84t-69 -204q0 -88 32 -153.5t81 -103t122 -60t140 -29.5t149 -7h168q82 0 149 7t140 29.5t122 60t81 103t32 153.5zM16 [...]
+<glyph unicode="&#xf114;" horiz-adv-x="1664" d="M1536 224v704q0 40 -28 68t-68 28h-704q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68v-960q0 -40 28 -68t68 -28h1216q40 0 68 28t28 68zM1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320 q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" />
+<glyph unicode="&#xf115;" horiz-adv-x="1920" d="M1781 605q0 35 -53 35h-1088q-40 0 -85.5 -21.5t-71.5 -52.5l-294 -363q-18 -24 -18 -40q0 -35 53 -35h1088q40 0 86 22t71 53l294 363q18 22 18 39zM640 768h768v160q0 40 -28 68t-68 28h-576q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68 v-853l256 315q44 53 116 87.5t140 34.5zM1909 605q0 -62 -46 -120l-295 -363q-43 -53 -116 -87.5t-140 -34.5h-1088q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 [...]
+<glyph unicode="&#xf116;" horiz-adv-x="1792" />
+<glyph unicode="&#xf117;" horiz-adv-x="1792" />
+<glyph unicode="&#xf118;" d="M1134 461q-37 -121 -138 -195t-228 -74t-228 74t-138 195q-8 25 4 48.5t38 31.5q25 8 48.5 -4t31.5 -38q25 -80 92.5 -129.5t151.5 -49.5t151.5 49.5t92.5 129.5q8 26 32 38t49 4t37 -31.5t4 -48.5zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-2 [...]
+<glyph unicode="&#xf119;" d="M1134 307q8 -25 -4 -48.5t-37 -31.5t-49 4t-32 38q-25 80 -92.5 129.5t-151.5 49.5t-151.5 -49.5t-92.5 -129.5q-8 -26 -31.5 -38t-48.5 -4q-26 8 -38 31.5t-4 48.5q37 121 138 195t228 74t228 -74t138 -195zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248 [...]
+<glyph unicode="&#xf11a;" d="M1152 448q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h640q26 0 45 -19t19 -45zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136. [...]
+<glyph unicode="&#xf11b;" horiz-adv-x="1920" d="M832 448v128q0 14 -9 23t-23 9h-192v192q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-192h-192q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h192v-192q0 -14 9 -23t23 -9h128q14 0 23 9t9 23v192h192q14 0 23 9t9 23zM1408 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1920 512q0 -212 -150 -362t-362 -1 [...]
+<glyph unicode="&#xf11c;" horiz-adv-x="1920" d="M384 368v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM512 624v-96q0 -16 -16 -16h-224q-16 0 -16 16v96q0 16 16 16h224q16 0 16 -16zM384 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1408 368v-96q0 -16 -16 -16 h-864q-16 0 -16 16v96q0 16 16 16h864q16 0 16 -16zM768 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM640 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16z [...]
+<glyph unicode="&#xf11d;" horiz-adv-x="1792" d="M1664 491v616q-169 -91 -306 -91q-82 0 -145 32q-100 49 -184 76.5t-178 27.5q-173 0 -403 -127v-599q245 113 433 113q55 0 103.5 -7.5t98 -26t77 -31t82.5 -39.5l28 -14q44 -22 101 -22q120 0 293 92zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q0 -14 -9 -23t-23 -9 h-64q-14 0 -23 9t-9 23v1266q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -39 -35 -57q-10 -5 -17 -9q-218 -116 -369 -116q-88 0 -158 35l-28 14q-64 33 -9 [...]
+<glyph unicode="&#xf11e;" horiz-adv-x="1792" d="M832 536v192q-181 -16 -384 -117v-185q205 96 384 110zM832 954v197q-172 -8 -384 -126v-189q215 111 384 118zM1664 491v184q-235 -116 -384 -71v224q-20 6 -39 15q-5 3 -33 17t-34.5 17t-31.5 15t-34.5 15.5t-32.5 13t-36 12.5t-35 8.5t-39.5 7.5t-39.5 4t-44 2 q-23 0 -49 -3v-222h19q102 0 192.5 -29t197.5 -82q19 -9 39 -15v-188q42 -17 91 -17q120 0 293 92zM1664 918v189q-169 -91 -306 -91q-45 0 -78 8v-196q148 -42 384 90zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q [...]
+<glyph unicode="&#xf120;" horiz-adv-x="1664" d="M585 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23t-10 -23zM1664 96v-64q0 -14 -9 -23t-23 -9h-960q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h960q14 0 23 -9 t9 -23z" />
+<glyph unicode="&#xf121;" horiz-adv-x="1920" d="M617 137l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23zM1208 1204l-373 -1291q-4 -13 -15.5 -19.5t-23.5 -2.5l-62 17q-13 4 -19.5 15.5t-2.5 24.5 l373 1291q4 13 15.5 19.5t23.5 2.5l62 -17q13 -4 19.5 -15.5t2.5 -24.5zM1865 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10 [...]
+<glyph unicode="&#xf122;" horiz-adv-x="1792" d="M640 454v-70q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-69l-397 -398q-19 -19 -19 -45t19 -45zM1792 416q0 -58 -17 -133.5t-38.5 -138t-48 -125t-40.5 -90.5l-20 -40q-8 -17 -28 -17q-6 0 -9 1 q-25 8 -23 34q43 400 -106 565q-64 71 -170.5 110.5t-267.5 52.5v-251q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-262q411 -28 599 -221q169 - [...]
+<glyph unicode="&#xf123;" horiz-adv-x="1664" d="M1186 579l257 250l-356 52l-66 10l-30 60l-159 322v-963l59 -31l318 -168l-60 355l-12 66zM1638 841l-363 -354l86 -500q5 -33 -6 -51.5t-34 -18.5q-17 0 -40 12l-449 236l-449 -236q-23 -12 -40 -12q-23 0 -34 18.5t-6 51.5l86 500l-364 354q-32 32 -23 59.5t54 34.5 l502 73l225 455q20 41 49 41q28 0 49 -41l225 -455l502 -73q45 -7 54 -34.5t-24 -59.5z" />
+<glyph unicode="&#xf124;" horiz-adv-x="1408" d="M1401 1187l-640 -1280q-17 -35 -57 -35q-5 0 -15 2q-22 5 -35.5 22.5t-13.5 39.5v576h-576q-22 0 -39.5 13.5t-22.5 35.5t4 42t29 30l1280 640q13 7 29 7q27 0 45 -19q15 -14 18.5 -34.5t-6.5 -39.5z" />
+<glyph unicode="&#xf125;" horiz-adv-x="1664" d="M557 256h595v595zM512 301l595 595h-595v-595zM1664 224v-192q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v224h-864q-14 0 -23 9t-9 23v864h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224v224q0 14 9 23t23 9h192q14 0 23 -9t9 -23 v-224h851l246 247q10 9 23 9t23 -9q9 -10 9 -23t-9 -23l-247 -246v-851h224q14 0 23 -9t9 -23z" />
+<glyph unicode="&#xf126;" horiz-adv-x="1024" d="M288 64q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM288 1216q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM928 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1024 1088q0 -52 -26 -96.5t-70 -69.5 q-2 -287 -226 -414q-68 -38 -203 -81q-128 -40 -169.5 -71t-41.5 -100v-26q44 -25 70 -69.5t26 -96.5q0 -80 -56 -136t-136 -56t-136 56t-56 136q0 52 26 96.5t70 69.5v820q-44 25 -70 69.5t-26 96.5q0 80 56 13 [...]
+<glyph unicode="&#xf127;" horiz-adv-x="1664" d="M439 265l-256 -256q-10 -9 -23 -9q-12 0 -23 9q-9 10 -9 23t9 23l256 256q10 9 23 9t23 -9q9 -10 9 -23t-9 -23zM608 224v-320q0 -14 -9 -23t-23 -9t-23 9t-9 23v320q0 14 9 23t23 9t23 -9t9 -23zM384 448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23t9 23t23 9h320 q14 0 23 -9t9 -23zM1648 320q0 -120 -85 -203l-147 -146q-83 -83 -203 -83q-121 0 -204 85l-334 335q-21 21 -42 56l239 18l273 -274q27 -27 68 -27.5t68 26.5l147 146q28 28 28 67q0 40 -28 68l-274 275l18 239 [...]
+<glyph unicode="&#xf128;" horiz-adv-x="1024" d="M704 280v-240q0 -16 -12 -28t-28 -12h-240q-16 0 -28 12t-12 28v240q0 16 12 28t28 12h240q16 0 28 -12t12 -28zM1020 880q0 -54 -15.5 -101t-35 -76.5t-55 -59.5t-57.5 -43.5t-61 -35.5q-41 -23 -68.5 -65t-27.5 -67q0 -17 -12 -32.5t-28 -15.5h-240q-15 0 -25.5 18.5 t-10.5 37.5v45q0 83 65 156.5t143 108.5q59 27 84 56t25 76q0 42 -46.5 74t-107.5 32q-65 0 -108 -29q-35 -25 -107 -115q-13 -16 -31 -16q-12 0 -25 8l-164 125q-13 10 -15.5 25t5.5 28q160 266 464 266q80 0 [...]
+<glyph unicode="&#xf129;" horiz-adv-x="640" d="M640 192v-128q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64v384h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-576h64q26 0 45 -19t19 -45zM512 1344v-192q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v192 q0 26 19 45t45 19h256q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf12a;" horiz-adv-x="640" d="M512 288v-224q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v224q0 26 19 45t45 19h256q26 0 45 -19t19 -45zM542 1344l-28 -768q-1 -26 -20.5 -45t-45.5 -19h-256q-26 0 -45.5 19t-20.5 45l-28 768q-1 26 17.5 45t44.5 19h320q26 0 44.5 -19t17.5 -45z" />
+<glyph unicode="&#xf12b;" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1534 846v-206h-514l-3 27 q-4 28 -4 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q83 65 188 65q110 0 178 -59.5t68 -158.5q0 -56 -24.5 -103t-62 -76.5t-81.5 -58.5t- [...]
+<glyph unicode="&#xf12c;" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1536 -50v-206h-514l-4 27 q-3 45 -3 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q80 65 188 65q110 0 178 -59.5t68 -158.5q0 -66 -34.5 -118.5t-84 -86t-99.5 -62.5t- [...]
+<glyph unicode="&#xf12d;" horiz-adv-x="1920" d="M896 128l336 384h-768l-336 -384h768zM1909 1205q15 -34 9.5 -71.5t-30.5 -65.5l-896 -1024q-38 -44 -96 -44h-768q-38 0 -69.5 20.5t-47.5 54.5q-15 34 -9.5 71.5t30.5 65.5l896 1024q38 44 96 44h768q38 0 69.5 -20.5t47.5 -54.5z" />
+<glyph unicode="&#xf12e;" horiz-adv-x="1664" d="M1664 438q0 -81 -44.5 -135t-123.5 -54q-41 0 -77.5 17.5t-59 38t-56.5 38t-71 17.5q-110 0 -110 -124q0 -39 16 -115t15 -115v-5q-22 0 -33 -1q-34 -3 -97.5 -11.5t-115.5 -13.5t-98 -5q-61 0 -103 26.5t-42 83.5q0 37 17.5 71t38 56.5t38 59t17.5 77.5q0 79 -54 123.5 t-135 44.5q-84 0 -143 -45.5t-59 -127.5q0 -43 15 -83t33.5 -64.5t33.5 -53t15 -50.5q0 -45 -46 -89q-37 -35 -117 -35q-95 0 -245 24q-9 2 -27.5 4t-27.5 4l-13 2q-1 0 -3 1q-2 0 -2 1v1024q2 -1 17.5 -3.5t [...]
+<glyph unicode="&#xf130;" horiz-adv-x="1152" d="M1152 832v-128q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-217 24 -364.5 187.5t-147.5 384.5v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -185 131.5 -316.5t316.5 -131.5 t316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45zM896 1216v-512q0 -132 -94 -226t-226 -94t-226 94t-94 226v512q0 132 94 226t226 94t226 -94t94 -226z" />
+<glyph unicode="&#xf131;" horiz-adv-x="1408" d="M271 591l-101 -101q-42 103 -42 214v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -53 15 -113zM1385 1193l-361 -361v-128q0 -132 -94 -226t-226 -94q-55 0 -109 19l-96 -96q97 -51 205 -51q185 0 316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45v-128 q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-125 13 -235 81l-254 -254q-10 -10 -23 -10t-23 10l-82 82q-10 10 -10 23t10 [...]
+<glyph unicode="&#xf132;" horiz-adv-x="1280" d="M1088 576v640h-448v-1137q119 63 213 137q235 184 235 360zM1280 1344v-768q0 -86 -33.5 -170.5t-83 -150t-118 -127.5t-126.5 -103t-121 -77.5t-89.5 -49.5t-42.5 -20q-12 -6 -26 -6t-26 6q-16 7 -42.5 20t-89.5 49.5t-121 77.5t-126.5 103t-118 127.5t-83 150 t-33.5 170.5v768q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf133;" horiz-adv-x="1664" d="M128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280 q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" />
+<glyph unicode="&#xf134;" horiz-adv-x="1408" d="M512 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 1376v-320q0 -16 -12 -25q-8 -7 -20 -7q-4 0 -7 1l-448 96q-11 2 -18 11t-7 20h-256v-102q111 -23 183.5 -111t72.5 -203v-800q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v800 q0 106 62.5 190.5t161.5 114.5v111h-32q-59 0 -115 -23.5t-91.5 -53t-66 -66.5t-40.5 -53.5t-14 -24.5q-17 -35 -57 -35q-16 0 -29 7q-23 12 -31.5 37t3.5 49q5 10 14.5 26t37.5 53.5t60.5 70t85 67t108.5 52.5q-2 [...]
+<glyph unicode="&#xf135;" horiz-adv-x="1664" d="M1440 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1664 1376q0 -249 -75.5 -430.5t-253.5 -360.5q-81 -80 -195 -176l-20 -379q-2 -16 -16 -26l-384 -224q-7 -4 -16 -4q-12 0 -23 9l-64 64q-13 14 -8 32l85 276l-281 281l-276 -85q-3 -1 -9 -1 q-14 0 -23 9l-64 64q-17 19 -5 39l224 384q10 14 26 16l379 20q96 114 176 195q188 187 358 258t431 71q14 0 24 -9.5t10 -22.5z" />
+<glyph unicode="&#xf136;" horiz-adv-x="1792" d="M1745 763l-164 -763h-334l178 832q13 56 -15 88q-27 33 -83 33h-169l-204 -953h-334l204 953h-286l-204 -953h-334l204 953l-153 327h1276q101 0 189.5 -40.5t147.5 -113.5q60 -73 81 -168.5t0 -194.5z" />
+<glyph unicode="&#xf137;" d="M909 141l102 102q19 19 19 45t-19 45l-307 307l307 307q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf138;" d="M717 141l454 454q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l307 -307l-307 -307q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf139;" d="M1165 397l102 102q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l307 307l307 -307q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf13a;" d="M813 237l454 454q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-307 -307l-307 307q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf13b;" horiz-adv-x="1408" d="M1130 939l16 175h-884l47 -534h612l-22 -228l-197 -53l-196 53l-13 140h-175l22 -278l362 -100h4v1l359 99l50 544h-644l-15 181h674zM0 1408h1408l-128 -1438l-578 -162l-574 162z" />
+<glyph unicode="&#xf13c;" horiz-adv-x="1792" d="M275 1408h1505l-266 -1333l-804 -267l-698 267l71 356h297l-29 -147l422 -161l486 161l68 339h-1208l58 297h1209l38 191h-1208z" />
+<glyph unicode="&#xf13d;" horiz-adv-x="1792" d="M960 1280q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1792 352v-352q0 -22 -20 -30q-8 -2 -12 -2q-13 0 -23 9l-93 93q-119 -143 -318.5 -226.5t-429.5 -83.5t-429.5 83.5t-318.5 226.5l-93 -93q-9 -9 -23 -9q-4 0 -12 2q-20 8 -20 30v352 q0 14 9 23t23 9h352q22 0 30 -20q8 -19 -7 -35l-100 -100q67 -91 189.5 -153.5t271.5 -82.5v647h-192q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h192v163q-58 34 -93 92.5t-35 128.5q0 106 75 181t181 75t181 -75t75 [...]
+<glyph unicode="&#xf13e;" horiz-adv-x="1152" d="M1056 768q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v320q0 185 131.5 316.5t316.5 131.5t316.5 -131.5t131.5 -316.5q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45q0 106 -75 181t-181 75t-181 -75t-75 -181 v-320h736z" />
+<glyph unicode="&#xf140;" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM1152 640q0 159 -112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM1280 640q0 -212 -150 -362t-362 -150t-362 150 t-150 362t150 362t362 150t362 -150t150 -362zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 2 [...]
+<glyph unicode="&#xf141;" horiz-adv-x="1408" d="M384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM896 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM1408 800v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" />
+<glyph unicode="&#xf142;" horiz-adv-x="384" d="M384 288v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 1312v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" />
+<glyph unicode="&#xf143;" d="M512 256q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM863 162q-13 232 -177 396t-396 177q-14 1 -24 -9t-10 -23v-128q0 -13 8.5 -22t21.5 -10q154 -11 264 -121t121 -264q1 -13 10 -21.5t22 -8.5h128q13 0 23 10 t9 24zM1247 161q-5 154 -56 297.5t-139.5 260t-205 205t-260 139.5t-297.5 56q-14 1 -23 -9q-10 -10 -10 -23v-128q0 -13 9 -22t22 -10q204 -7 378 -111.5t278.5 -278.5t111.5 -378q1 -13 10 -22t22 -9h128q13 0 23 10q11 9 9 23z [...]
+<glyph unicode="&#xf144;" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1152 585q32 18 32 55t-32 55l-544 320q-31 19 -64 1q-32 -19 -32 -56v-640q0 -37 32 -56 q16 -8 32 -8q17 0 32 9z" />
+<glyph unicode="&#xf145;" horiz-adv-x="1792" d="M1024 1084l316 -316l-572 -572l-316 316zM813 105l618 618q19 19 19 45t-19 45l-362 362q-18 18 -45 18t-45 -18l-618 -618q-19 -19 -19 -45t19 -45l362 -362q18 -18 45 -18t45 18zM1702 742l-907 -908q-37 -37 -90.5 -37t-90.5 37l-126 126q56 56 56 136t-56 136 t-136 56t-136 -56l-125 126q-37 37 -37 90.5t37 90.5l907 906q37 37 90.5 37t90.5 -37l125 -125q-56 -56 -56 -136t56 -136t136 -56t136 56l126 -125q37 -37 37 -90.5t-37 -90.5z" />
+<glyph unicode="&#xf146;" d="M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" />
+<glyph unicode="&#xf147;" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h832q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5 t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf148;" horiz-adv-x="1024" d="M1018 933q-18 -37 -58 -37h-192v-864q0 -14 -9 -23t-23 -9h-704q-21 0 -29 18q-8 20 4 35l160 192q9 11 25 11h320v640h-192q-40 0 -58 37q-17 37 9 68l320 384q18 22 49 22t49 -22l320 -384q27 -32 9 -68z" />
+<glyph unicode="&#xf149;" horiz-adv-x="1024" d="M32 1280h704q13 0 22.5 -9.5t9.5 -23.5v-863h192q40 0 58 -37t-9 -69l-320 -384q-18 -22 -49 -22t-49 22l-320 384q-26 31 -9 69q18 37 58 37h192v640h-320q-14 0 -25 11l-160 192q-13 14 -4 34q9 19 29 19z" />
+<glyph unicode="&#xf14a;" d="M685 237l614 614q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-467 -467l-211 211q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l358 -358q19 -19 45 -19t45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5 t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf14b;" d="M404 428l152 -152l-52 -52h-56v96h-96v56zM818 818q14 -13 -3 -30l-291 -291q-17 -17 -30 -3q-14 13 3 30l291 291q17 17 30 3zM544 128l544 544l-288 288l-544 -544v-288h288zM1152 736l92 92q28 28 28 68t-28 68l-152 152q-28 28 -68 28t-68 -28l-92 -92zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf14c;" d="M1280 608v480q0 26 -19 45t-45 19h-480q-42 0 -59 -39q-17 -41 14 -70l144 -144l-534 -534q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l534 534l144 -144q18 -19 45 -19q12 0 25 5q39 17 39 59zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960 q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf14d;" d="M1005 435l352 352q19 19 19 45t-19 45l-352 352q-30 31 -69 14q-40 -17 -40 -59v-160q-119 0 -216 -19.5t-162.5 -51t-114 -79t-76.5 -95.5t-44.5 -109t-21.5 -111.5t-5 -110.5q0 -181 167 -404q10 -12 25 -12q7 0 13 3q22 9 19 33q-44 354 62 473q46 52 130 75.5 t224 23.5v-160q0 -42 40 -59q12 -5 24 -5q26 0 45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf14e;" d="M640 448l256 128l-256 128v-256zM1024 1039v-542l-512 -256v542zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf150;" d="M1145 861q18 -35 -5 -66l-320 -448q-19 -27 -52 -27t-52 27l-320 448q-23 31 -5 66q17 35 57 35h640q40 0 57 -35zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf151;" d="M1145 419q-17 -35 -57 -35h-640q-40 0 -57 35q-18 35 5 66l320 448q19 27 52 27t52 -27l320 -448q23 -31 5 -66zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf152;" d="M1088 640q0 -33 -27 -52l-448 -320q-31 -23 -66 -5q-35 17 -35 57v640q0 40 35 57q35 18 66 -5l448 -320q27 -19 27 -52zM1280 160v960q0 14 -9 23t-23 9h-960q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h960q14 0 23 9t9 23zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf153;" horiz-adv-x="1024" d="M976 229l35 -159q3 -12 -3 -22.5t-17 -14.5l-5 -1q-4 -2 -10.5 -3.5t-16 -4.5t-21.5 -5.5t-25.5 -5t-30 -5t-33.5 -4.5t-36.5 -3t-38.5 -1q-234 0 -409 130.5t-238 351.5h-95q-13 0 -22.5 9.5t-9.5 22.5v113q0 13 9.5 22.5t22.5 9.5h66q-2 57 1 105h-67q-14 0 -23 9 t-9 23v114q0 14 9 23t23 9h98q67 210 243.5 338t400.5 128q102 0 194 -23q11 -3 20 -15q6 -11 3 -24l-43 -159q-3 -13 -14 -19.5t-24 -2.5l-4 1q-4 1 -11.5 2.5l-17.5 3.5t-22.5 3.5t-26 3t-29 2.5t-29.5 1q-126  [...]
+<glyph unicode="&#xf154;" horiz-adv-x="1024" d="M1020 399v-367q0 -14 -9 -23t-23 -9h-956q-14 0 -23 9t-9 23v150q0 13 9.5 22.5t22.5 9.5h97v383h-95q-14 0 -23 9.5t-9 22.5v131q0 14 9 23t23 9h95v223q0 171 123.5 282t314.5 111q185 0 335 -125q9 -8 10 -20.5t-7 -22.5l-103 -127q-9 -11 -22 -12q-13 -2 -23 7 q-5 5 -26 19t-69 32t-93 18q-85 0 -137 -47t-52 -123v-215h305q13 0 22.5 -9t9.5 -23v-131q0 -13 -9.5 -22.5t-22.5 -9.5h-305v-379h414v181q0 13 9 22.5t23 9.5h162q14 0 23 -9.5t9 -22.5z" />
+<glyph unicode="&#xf155;" horiz-adv-x="1024" d="M978 351q0 -153 -99.5 -263.5t-258.5 -136.5v-175q0 -14 -9 -23t-23 -9h-135q-13 0 -22.5 9.5t-9.5 22.5v175q-66 9 -127.5 31t-101.5 44.5t-74 48t-46.5 37.5t-17.5 18q-17 21 -2 41l103 135q7 10 23 12q15 2 24 -9l2 -2q113 -99 243 -125q37 -8 74 -8q81 0 142.5 43 t61.5 122q0 28 -15 53t-33.5 42t-58.5 37.5t-66 32t-80 32.5q-39 16 -61.5 25t-61.5 26.5t-62.5 31t-56.5 35.5t-53.5 42.5t-43.5 49t-35.5 58t-21 66.5t-8.5 78q0 138 98 242t255 134v180q0 13 9.5 22.5t22.5  [...]
+<glyph unicode="&#xf156;" horiz-adv-x="898" d="M898 1066v-102q0 -14 -9 -23t-23 -9h-168q-23 -144 -129 -234t-276 -110q167 -178 459 -536q14 -16 4 -34q-8 -18 -29 -18h-195q-16 0 -25 12q-306 367 -498 571q-9 9 -9 22v127q0 13 9.5 22.5t22.5 9.5h112q132 0 212.5 43t102.5 125h-427q-14 0 -23 9t-9 23v102 q0 14 9 23t23 9h413q-57 113 -268 113h-145q-13 0 -22.5 9.5t-9.5 22.5v133q0 14 9 23t23 9h832q14 0 23 -9t9 -23v-102q0 -14 -9 -23t-23 -9h-233q47 -61 64 -144h171q14 0 23 -9t9 -23z" />
+<glyph unicode="&#xf157;" horiz-adv-x="1027" d="M603 0h-172q-13 0 -22.5 9t-9.5 23v330h-288q-13 0 -22.5 9t-9.5 23v103q0 13 9.5 22.5t22.5 9.5h288v85h-288q-13 0 -22.5 9t-9.5 23v104q0 13 9.5 22.5t22.5 9.5h214l-321 578q-8 16 0 32q10 16 28 16h194q19 0 29 -18l215 -425q19 -38 56 -125q10 24 30.5 68t27.5 61 l191 420q8 19 29 19h191q17 0 27 -16q9 -14 1 -31l-313 -579h215q13 0 22.5 -9.5t9.5 -22.5v-104q0 -14 -9.5 -23t-22.5 -9h-290v-85h290q13 0 22.5 -9.5t9.5 -22.5v-103q0 -14 -9.5 -23t-22.5 -9h-290v-330q [...]
+<glyph unicode="&#xf158;" horiz-adv-x="1280" d="M1043 971q0 100 -65 162t-171 62h-320v-448h320q106 0 171 62t65 162zM1280 971q0 -193 -126.5 -315t-326.5 -122h-340v-118h505q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9h-505v-192q0 -14 -9.5 -23t-22.5 -9h-167q-14 0 -23 9t-9 23v192h-224q-14 0 -23 9t-9 23v128 q0 14 9 23t23 9h224v118h-224q-14 0 -23 9t-9 23v149q0 13 9 22.5t23 9.5h224v629q0 14 9 23t23 9h539q200 0 326.5 -122t126.5 -315z" />
+<glyph unicode="&#xf159;" horiz-adv-x="1792" d="M514 341l81 299h-159l75 -300q1 -1 1 -3t1 -3q0 1 0.5 3.5t0.5 3.5zM630 768l35 128h-292l32 -128h225zM822 768h139l-35 128h-70zM1271 340l78 300h-162l81 -299q0 -1 0.5 -3.5t1.5 -3.5q0 1 0.5 3t0.5 3zM1382 768l33 128h-297l34 -128h230zM1792 736v-64q0 -14 -9 -23 t-23 -9h-213l-164 -616q-7 -24 -31 -24h-159q-24 0 -31 24l-166 616h-209l-167 -616q-7 -24 -31 -24h-159q-11 0 -19.5 7t-10.5 17l-160 616h-208q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h175l-33 128h-142q-1 [...]
+<glyph unicode="&#xf15a;" horiz-adv-x="1280" d="M1167 896q18 -182 -131 -258q117 -28 175 -103t45 -214q-7 -71 -32.5 -125t-64.5 -89t-97 -58.5t-121.5 -34.5t-145.5 -15v-255h-154v251q-80 0 -122 1v-252h-154v255q-18 0 -54 0.5t-55 0.5h-200l31 183h111q50 0 58 51v402h16q-6 1 -16 1v287q-13 68 -89 68h-111v164 l212 -1q64 0 97 1v252h154v-247q82 2 122 2v245h154v-252q79 -7 140 -22.5t113 -45t82.5 -78t36.5 -114.5zM952 351q0 36 -15 64t-37 46t-57.5 30.5t-65.5 18.5t-74 9t-69 3t-64.5 -1t-47.5 -1v-338q8 0 37 -0 [...]
+<glyph unicode="&#xf15b;" horiz-adv-x="1280" d="M1280 768v-800q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h544v-544q0 -40 28 -68t68 -28h544zM1277 896h-509v509q82 -15 132 -65l312 -312q50 -50 65 -132z" />
+<glyph unicode="&#xf15c;" horiz-adv-x="1280" d="M1024 160v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23zM1024 416v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23zM1280 768v-800q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28 t-28 68v1344q0 40 28 68t68 28h544v-544q0 -40 28 -68t68 -28h544zM1277 896h-509v509q82 -15 132 -65l312 -312q50 -50 65 -132z" />
+<glyph unicode="&#xf15d;" horiz-adv-x="1664" d="M1191 1128h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1572 -23 v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530q-6 -8 -21 -26l-11 -11v-2l14 2q9 2 30 2h248v119h121zM1661 874v-106h-288v106h75l- [...]
+<glyph unicode="&#xf15e;" horiz-adv-x="1664" d="M1191 104h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1661 -150 v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162l230 -662h70zM1572 1001v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567 [...]
+<glyph unicode="&#xf160;" horiz-adv-x="1792" d="M736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1792 -32v-192q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h832 q14 0 23 -9t9 -23zM1600 480v-192q0 -14 -9 -23t-23 -9h-640q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h640q14 0 23 -9t9 -23zM1408 992v-192q0 -14 -9 -23t-23 -9h-448q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h448q14  [...]
+<glyph unicode="&#xf161;" horiz-adv-x="1792" d="M1216 -32v-192q0 -14 -9 -23t-23 -9h-256q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h256q14 0 23 -9t9 -23zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192 q14 0 23 -9t9 -23zM1408 480v-192q0 -14 -9 -23t-23 -9h-448q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h448q14 0 23 -9t9 -23zM1600 992v-192q0 -14 -9 -23t-23 -9h-640q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h640q14  [...]
+<glyph unicode="&#xf162;" d="M1346 223q0 63 -44 116t-103 53q-52 0 -83 -37t-31 -94t36.5 -95t104.5 -38q50 0 85 27t35 68zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23 zM1486 165q0 -62 -13 -121.5t-41 -114t-68 -95.5t-98.5 -65.5t-127.5 -24.5q-62 0 -108 16q-24 8 -42 15l39 113q15 -7 31 -11q37 -13 75 -13q84 0 134.5 58.5t66.5 145.5h-2q-21 -23 -61.5 -37t-84.5 -14q-106 0 -173 71.5t-67 [...]
+<glyph unicode="&#xf163;" d="M1346 1247q0 63 -44 116t-103 53q-52 0 -83 -37t-31 -94t36.5 -95t104.5 -38q50 0 85 27t35 68zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9 t9 -23zM1456 -142v-114h-469v114h167v432q0 7 0.5 19t0.5 17v16h-2l-7 -12q-8 -13 -26 -31l-62 -58l-82 86l192 185h123v-654h165zM1486 1189q0 -62 -13 -121.5t-41 -114t-68 -95.5t-98.5 -65.5t-127.5 -24.5q-62 0 -108 16q-24 8 -4 [...]
+<glyph unicode="&#xf164;" horiz-adv-x="1664" d="M256 192q0 26 -19 45t-45 19q-27 0 -45.5 -19t-18.5 -45q0 -27 18.5 -45.5t45.5 -18.5q26 0 45 18.5t19 45.5zM416 704v-640q0 -26 -19 -45t-45 -19h-288q-26 0 -45 19t-19 45v640q0 26 19 45t45 19h288q26 0 45 -19t19 -45zM1600 704q0 -86 -55 -149q15 -44 15 -76 q3 -76 -43 -137q17 -56 0 -117q-15 -57 -54 -94q9 -112 -49 -181q-64 -76 -197 -78h-36h-76h-17q-66 0 -144 15.5t-121.5 29t-120.5 39.5q-123 43 -158 44q-26 1 -45 19.5t-19 44.5v641q0 25 18 43.5t43 20.5q24  [...]
+<glyph unicode="&#xf165;" horiz-adv-x="1664" d="M256 960q0 -26 -19 -45t-45 -19q-27 0 -45.5 19t-18.5 45q0 27 18.5 45.5t45.5 18.5q26 0 45 -18.5t19 -45.5zM416 448v640q0 26 -19 45t-45 19h-288q-26 0 -45 -19t-19 -45v-640q0 -26 19 -45t45 -19h288q26 0 45 19t19 45zM1545 597q55 -61 55 -149q-1 -78 -57.5 -135 t-134.5 -57h-277q4 -14 8 -24t11 -22t10 -18q18 -37 27 -57t19 -58.5t10 -76.5q0 -24 -0.5 -39t-5 -45t-12 -50t-24 -45t-40 -40.5t-60 -26t-82.5 -10.5q-26 0 -45 19q-20 20 -34 50t-19.5 52t-12.5 61q-9 42 [...]
+<glyph unicode="&#xf166;" d="M919 233v157q0 50 -29 50q-17 0 -33 -16v-224q16 -16 33 -16q29 0 29 49zM1103 355h66v34q0 51 -33 51t-33 -51v-34zM532 621v-70h-80v-423h-74v423h-78v70h232zM733 495v-367h-67v40q-39 -45 -76 -45q-33 0 -42 28q-6 16 -6 54v290h66v-270q0 -24 1 -26q1 -15 15 -15 q20 0 42 31v280h67zM985 384v-146q0 -52 -7 -73q-12 -42 -53 -42q-35 0 -68 41v-36h-67v493h67v-161q32 40 68 40q41 0 53 -42q7 -21 7 -74zM1236 255v-9q0 -29 -2 -43q-3 -22 -15 -40q-27 -40 -80 -40q-52 0 -81 38q-21 27 -21 86 [...]
+<glyph unicode="&#xf167;" d="M971 292v-211q0 -67 -39 -67q-23 0 -45 22v301q22 22 45 22q39 0 39 -67zM1309 291v-46h-90v46q0 68 45 68t45 -68zM343 509h107v94h-312v-94h105v-569h100v569zM631 -60h89v494h-89v-378q-30 -42 -57 -42q-18 0 -21 21q-1 3 -1 35v364h-89v-391q0 -49 8 -73 q12 -37 58 -37q48 0 102 61v-54zM1060 88v197q0 73 -9 99q-17 56 -71 56q-50 0 -93 -54v217h-89v-663h89v48q45 -55 93 -55q54 0 71 55q9 27 9 100zM1398 98v13h-91q0 -51 -2 -61q-7 -36 -40 -36q-46 0 -46 69v87h179v103q0 79 -27 116q-39  [...]
+<glyph unicode="&#xf168;" horiz-adv-x="1408" d="M597 869q-10 -18 -257 -456q-27 -46 -65 -46h-239q-21 0 -31 17t0 36l253 448q1 0 0 1l-161 279q-12 22 -1 37q9 15 32 15h239q40 0 66 -45zM1403 1511q11 -16 0 -37l-528 -934v-1l336 -615q11 -20 1 -37q-10 -15 -32 -15h-239q-42 0 -66 45l-339 622q18 32 531 942 q25 45 64 45h241q22 0 31 -15z" />
+<glyph unicode="&#xf169;" d="M685 771q0 1 -126 222q-21 34 -52 34h-184q-18 0 -26 -11q-7 -12 1 -29l125 -216v-1l-196 -346q-9 -14 0 -28q8 -13 24 -13h185q31 0 50 36zM1309 1268q-7 12 -24 12h-187q-30 0 -49 -35l-411 -729q1 -2 262 -481q20 -35 52 -35h184q18 0 25 12q8 13 -1 28l-260 476v1 l409 723q8 16 0 28zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf16a;" horiz-adv-x="1792" d="M1280 640q0 37 -30 54l-512 320q-31 20 -65 2q-33 -18 -33 -56v-640q0 -38 33 -56q16 -8 31 -8q20 0 34 10l512 320q30 17 30 54zM1792 640q0 -96 -1 -150t-8.5 -136.5t-22.5 -147.5q-16 -73 -69 -123t-124 -58q-222 -25 -671 -25t-671 25q-71 8 -124.5 58t-69.5 123 q-14 65 -21.5 147.5t-8.5 136.5t-1 150t1 150t8.5 136.5t22.5 147.5q16 73 69 123t124 58q222 25 671 25t671 -25q71 -8 124.5 -58t69.5 -123q14 -65 21.5 -147.5t8.5 -136.5t1 -150z" />
+<glyph unicode="&#xf16b;" horiz-adv-x="1792" d="M402 829l494 -305l-342 -285l-490 319zM1388 274v-108l-490 -293v-1l-1 1l-1 -1v1l-489 293v108l147 -96l342 284v2l1 -1l1 1v-2l343 -284zM554 1418l342 -285l-494 -304l-338 270zM1390 829l338 -271l-489 -319l-343 285zM1239 1418l489 -319l-338 -270l-494 304z" />
+<glyph unicode="&#xf16c;" horiz-adv-x="1408" d="M928 135v-151l-707 -1v151zM1169 481v-701l-1 -35v-1h-1132l-35 1h-1v736h121v-618h928v618h120zM241 393l704 -65l-13 -150l-705 65zM309 709l683 -183l-39 -146l-683 183zM472 1058l609 -360l-77 -130l-609 360zM832 1389l398 -585l-124 -85l-399 584zM1285 1536 l121 -697l-149 -26l-121 697z" />
+<glyph unicode="&#xf16d;" d="M1362 110v648h-135q20 -63 20 -131q0 -126 -64 -232.5t-174 -168.5t-240 -62q-197 0 -337 135.5t-140 327.5q0 68 20 131h-141v-648q0 -26 17.5 -43.5t43.5 -17.5h1069q25 0 43 17.5t18 43.5zM1078 643q0 124 -90.5 211.5t-218.5 87.5q-127 0 -217.5 -87.5t-90.5 -211.5 t90.5 -211.5t217.5 -87.5q128 0 218.5 87.5t90.5 211.5zM1362 1003v165q0 28 -20 48.5t-49 20.5h-174q-29 0 -49 -20.5t-20 -48.5v-165q0 -29 20 -49t49 -20h174q29 0 49 20t20 49zM1536 1211v-1142q0 -81 -58 -139t-139 -58h-11 [...]
+<glyph unicode="&#xf16e;" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM698 640q0 88 -62 150t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150zM1262 640q0 88 -62 150 t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150z" />
+<glyph unicode="&#xf170;" d="M768 914l201 -306h-402zM1133 384h94l-459 691l-459 -691h94l104 160h522zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf171;" horiz-adv-x="1408" d="M815 677q8 -63 -50.5 -101t-111.5 -6q-39 17 -53.5 58t-0.5 82t52 58q36 18 72.5 12t64 -35.5t27.5 -67.5zM926 698q-14 107 -113 164t-197 13q-63 -28 -100.5 -88.5t-34.5 -129.5q4 -91 77.5 -155t165.5 -56q91 8 152 84t50 168zM1165 1240q-20 27 -56 44.5t-58 22 t-71 12.5q-291 47 -566 -2q-43 -7 -66 -12t-55 -22t-50 -43q30 -28 76 -45.5t73.5 -22t87.5 -11.5q228 -29 448 -1q63 8 89.5 12t72.5 21.5t75 46.5zM1222 205q-8 -26 -15.5 -76.5t-14 -84t-28.5 -70t-58 -56.5q- [...]
+<glyph unicode="&#xf172;" d="M848 666q0 43 -41 66t-77 1q-43 -20 -42.5 -72.5t43.5 -70.5q39 -23 81 4t36 72zM928 682q8 -66 -36 -121t-110 -61t-119 40t-56 113q-2 49 25.5 93t72.5 64q70 31 141.5 -10t81.5 -118zM1100 1073q-20 -21 -53.5 -34t-53 -16t-63.5 -8q-155 -20 -324 0q-44 6 -63 9.5 t-52.5 16t-54.5 32.5q13 19 36 31t40 15.5t47 8.5q198 35 408 1q33 -5 51 -8.5t43 -16t39 -31.5zM1142 327q0 7 5.5 26.5t3 32t-17.5 16.5q-161 -106 -365 -106t-366 106l-12 -6l-5 -12q26 -154 41 -210q47 -81 204 -108q249 -46 4 [...]
+<glyph unicode="&#xf173;" horiz-adv-x="1024" d="M390 1408h219v-388h364v-241h-364v-394q0 -136 14 -172q13 -37 52 -60q50 -31 117 -31q117 0 232 76v-242q-102 -48 -178 -65q-77 -19 -173 -19q-105 0 -186 27q-78 25 -138 75q-58 51 -79 105q-22 54 -22 161v539h-170v217q91 30 155 84q64 55 103 132q39 78 54 196z " />
+<glyph unicode="&#xf174;" d="M1123 127v181q-88 -56 -174 -56q-51 0 -88 23q-29 17 -39 45q-11 30 -11 129v295h274v181h-274v291h-164q-11 -90 -40 -147t-78 -99q-48 -40 -116 -63v-163h127v-404q0 -78 17 -121q17 -42 59 -78q43 -37 104 -57q62 -20 140 -20q67 0 129 14q57 13 134 49zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf175;" horiz-adv-x="768" d="M765 237q8 -19 -5 -35l-350 -384q-10 -10 -23 -10q-14 0 -24 10l-355 384q-13 16 -5 35q9 19 29 19h224v1248q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1248h224q21 0 29 -19z" />
+<glyph unicode="&#xf176;" horiz-adv-x="768" d="M765 1043q-9 -19 -29 -19h-224v-1248q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v1248h-224q-21 0 -29 19t5 35l350 384q10 10 23 10q14 0 24 -10l355 -384q13 -16 5 -35z" />
+<glyph unicode="&#xf177;" horiz-adv-x="1792" d="M1792 736v-192q0 -14 -9 -23t-23 -9h-1248v-224q0 -21 -19 -29t-35 5l-384 350q-10 10 -10 23q0 14 10 24l384 354q16 14 35 6q19 -9 19 -29v-224h1248q14 0 23 -9t9 -23z" />
+<glyph unicode="&#xf178;" horiz-adv-x="1792" d="M1728 643q0 -14 -10 -24l-384 -354q-16 -14 -35 -6q-19 9 -19 29v224h-1248q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h1248v224q0 21 19 29t35 -5l384 -350q10 -10 10 -23z" />
+<glyph unicode="&#xf179;" horiz-adv-x="1408" d="M1393 321q-39 -125 -123 -250q-129 -196 -257 -196q-49 0 -140 32q-86 32 -151 32q-61 0 -142 -33q-81 -34 -132 -34q-152 0 -301 259q-147 261 -147 503q0 228 113 374q112 144 284 144q72 0 177 -30q104 -30 138 -30q45 0 143 34q102 34 173 34q119 0 213 -65 q52 -36 104 -100q-79 -67 -114 -118q-65 -94 -65 -207q0 -124 69 -223t158 -126zM1017 1494q0 -61 -29 -136q-30 -75 -93 -138q-54 -54 -108 -72q-37 -11 -104 -17q3 149 78 257q74 107 250 148q1 -3 2.5 -11t2.5 -11 [...]
+<glyph unicode="&#xf17a;" horiz-adv-x="1664" d="M682 530v-651l-682 94v557h682zM682 1273v-659h-682v565zM1664 530v-786l-907 125v661h907zM1664 1408v-794h-907v669z" />
+<glyph unicode="&#xf17b;" horiz-adv-x="1408" d="M493 1053q16 0 27.5 11.5t11.5 27.5t-11.5 27.5t-27.5 11.5t-27 -11.5t-11 -27.5t11 -27.5t27 -11.5zM915 1053q16 0 27 11.5t11 27.5t-11 27.5t-27 11.5t-27.5 -11.5t-11.5 -27.5t11.5 -27.5t27.5 -11.5zM103 869q42 0 72 -30t30 -72v-430q0 -43 -29.5 -73t-72.5 -30 t-73 30t-30 73v430q0 42 30 72t73 30zM1163 850v-666q0 -46 -32 -78t-77 -32h-75v-227q0 -43 -30 -73t-73 -30t-73 30t-30 73v227h-138v-227q0 -43 -30 -73t-73 -30q-42 0 -72 30t-30 73l-1 227h-74q-46 0 -78  [...]
+<glyph unicode="&#xf17c;" d="M663 1125q-11 -1 -15.5 -10.5t-8.5 -9.5q-5 -1 -5 5q0 12 19 15h10zM750 1111q-4 -1 -11.5 6.5t-17.5 4.5q24 11 32 -2q3 -6 -3 -9zM399 684q-4 1 -6 -3t-4.5 -12.5t-5.5 -13.5t-10 -13q-7 -10 -1 -12q4 -1 12.5 7t12.5 18q1 3 2 7t2 6t1.5 4.5t0.5 4v3t-1 2.5t-3 2z M1254 325q0 18 -55 42q4 15 7.5 27.5t5 26t3 21.5t0.5 22.5t-1 19.5t-3.5 22t-4 20.5t-5 25t-5.5 26.5q-10 48 -47 103t-72 75q24 -20 57 -83q87 -162 54 -278q-11 -40 -50 -42q-31 -4 -38.5 18.5t-8 83.5t-11.5 107q-9 39 -19.5 69 [...]
+<glyph unicode="&#xf17d;" d="M1024 36q-42 241 -140 498h-2l-2 -1q-16 -6 -43 -16.5t-101 -49t-137 -82t-131 -114.5t-103 -148l-15 11q184 -150 418 -150q132 0 256 52zM839 643q-21 49 -53 111q-311 -93 -673 -93q-1 -7 -1 -21q0 -124 44 -236.5t124 -201.5q50 89 123.5 166.5t142.5 124.5t130.5 81 t99.5 48l37 13q4 1 13 3.5t13 4.5zM732 855q-120 213 -244 378q-138 -65 -234 -186t-128 -272q302 0 606 80zM1416 536q-210 60 -409 29q87 -239 128 -469q111 75 185 189.5t96 250.5zM611 1277q-1 0 -2 -1q1 1 2 1zM1201 1132q [...]
+<glyph unicode="&#xf17e;" d="M1173 473q0 50 -19.5 91.5t-48.5 68.5t-73 49t-82.5 34t-87.5 23l-104 24q-30 7 -44 10.5t-35 11.5t-30 16t-16.5 21t-7.5 30q0 77 144 77q43 0 77 -12t54 -28.5t38 -33.5t40 -29t48 -12q47 0 75.5 32t28.5 77q0 55 -56 99.5t-142 67.5t-182 23q-68 0 -132 -15.5 t-119.5 -47t-89 -87t-33.5 -128.5q0 -61 19 -106.5t56 -75.5t80 -48.5t103 -32.5l146 -36q90 -22 112 -36q32 -20 32 -60q0 -39 -40 -64.5t-105 -25.5q-51 0 -91.5 16t-65 38.5t-45.5 45t-46 38.5t-54 16q-50 0 -75.5 -30t-25.5 -75q0 - [...]
+<glyph unicode="&#xf180;" horiz-adv-x="1664" d="M1483 512l-587 -587q-52 -53 -127.5 -53t-128.5 53l-587 587q-53 53 -53 128t53 128l587 587q53 53 128 53t128 -53l265 -265l-398 -399l-188 188q-42 42 -99 42q-59 0 -100 -41l-120 -121q-42 -40 -42 -99q0 -58 42 -100l406 -408q30 -28 67 -37l6 -4h28q60 0 99 41 l619 619l2 -3q53 -53 53 -128t-53 -128zM1406 1138l120 -120q14 -15 14 -36t-14 -36l-730 -730q-17 -15 -37 -15v0q-4 0 -6 1q-18 2 -30 14l-407 408q-14 15 -14 36t14 35l121 120q13 15 35 15t36 -15l252 -252l [...]
+<glyph unicode="&#xf181;" d="M704 192v1024q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-1024q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1376 576v640q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-640q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408 q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf182;" horiz-adv-x="1280" d="M1280 480q0 -40 -28 -68t-68 -28q-51 0 -80 43l-227 341h-45v-132l247 -411q9 -15 9 -33q0 -26 -19 -45t-45 -19h-192v-272q0 -46 -33 -79t-79 -33h-160q-46 0 -79 33t-33 79v272h-192q-26 0 -45 19t-19 45q0 18 9 33l247 411v132h-45l-227 -341q-29 -43 -80 -43 q-40 0 -68 28t-28 68q0 29 16 53l256 384q73 107 176 107h384q103 0 176 -107l256 -384q16 -24 16 -53zM864 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65. [...]
+<glyph unicode="&#xf183;" horiz-adv-x="1024" d="M1024 832v-416q0 -40 -28 -68t-68 -28t-68 28t-28 68v352h-64v-912q0 -46 -33 -79t-79 -33t-79 33t-33 79v464h-64v-464q0 -46 -33 -79t-79 -33t-79 33t-33 79v912h-64v-352q0 -40 -28 -68t-68 -28t-68 28t-28 68v416q0 80 56 136t136 56h640q80 0 136 -56t56 -136z M736 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" />
+<glyph unicode="&#xf184;" d="M773 234l350 473q16 22 24.5 59t-6 85t-61.5 79q-40 26 -83 25.5t-73.5 -17.5t-54.5 -45q-36 -40 -96 -40q-59 0 -95 40q-24 28 -54.5 45t-73.5 17.5t-84 -25.5q-46 -31 -60.5 -79t-6 -85t24.5 -59zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf185;" horiz-adv-x="1792" d="M1472 640q0 117 -45.5 223.5t-123 184t-184 123t-223.5 45.5t-223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5t45.5 -223.5t123 -184t184 -123t223.5 -45.5t223.5 45.5t184 123t123 184t45.5 223.5zM1748 363q-4 -15 -20 -20l-292 -96v-306q0 -16 -13 -26q-15 -10 -29 -4 l-292 94l-180 -248q-10 -13 -26 -13t-26 13l-180 248l-292 -94q-14 -6 -29 4q-13 10 -13 26v306l-292 96q-16 5 -20 20q-5 17 4 29l180 248l-180 248q-9 13 -4 29q4 15 20 20l292 96v306q0 16 13 26q15 10 2 [...]
+<glyph unicode="&#xf186;" d="M1262 233q-54 -9 -110 -9q-182 0 -337 90t-245 245t-90 337q0 192 104 357q-201 -60 -328.5 -229t-127.5 -384q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51q144 0 273.5 61.5t220.5 171.5zM1465 318q-94 -203 -283.5 -324.5t-413.5 -121.5q-156 0 -298 61 t-245 164t-164 245t-61 298q0 153 57.5 292.5t156 241.5t235.5 164.5t290 68.5q44 2 61 -39q18 -41 -15 -72q-86 -78 -131.5 -181.5t-45.5 -218.5q0 -148 73 -273t198 -198t273 -73q118 0 228 51q41 18 72 -13q14 -14 17.5 -34t-4.5 -38z" />
+<glyph unicode="&#xf187;" horiz-adv-x="1792" d="M1088 704q0 26 -19 45t-45 19h-256q-26 0 -45 -19t-19 -45t19 -45t45 -19h256q26 0 45 19t19 45zM1664 896v-960q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v960q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1728 1344v-256q0 -26 -19 -45t-45 -19h-1536 q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1536q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf188;" horiz-adv-x="1664" d="M1632 576q0 -26 -19 -45t-45 -19h-224q0 -171 -67 -290l208 -209q19 -19 19 -45t-19 -45q-18 -19 -45 -19t-45 19l-198 197q-5 -5 -15 -13t-42 -28.5t-65 -36.5t-82 -29t-97 -13v896h-128v-896q-51 0 -101.5 13.5t-87 33t-66 39t-43.5 32.5l-15 14l-183 -207 q-20 -21 -48 -21q-24 0 -43 16q-19 18 -20.5 44.5t15.5 46.5l202 227q-58 114 -58 274h-224q-26 0 -45 19t-19 45t19 45t45 19h224v294l-173 173q-19 19 -19 45t19 45t45 19t45 -19l173 -173h844l173 173q19 19 45 19t45 [...]
+<glyph unicode="&#xf189;" horiz-adv-x="1920" d="M1917 1016q23 -64 -150 -294q-24 -32 -65 -85q-78 -100 -90 -131q-17 -41 14 -81q17 -21 81 -82h1l1 -1l1 -1l2 -2q141 -131 191 -221q3 -5 6.5 -12.5t7 -26.5t-0.5 -34t-25 -27.5t-59 -12.5l-256 -4q-24 -5 -56 5t-52 22l-20 12q-30 21 -70 64t-68.5 77.5t-61 58 t-56.5 15.5q-3 -1 -8 -3.5t-17 -14.5t-21.5 -29.5t-17 -52t-6.5 -77.5q0 -15 -3.5 -27.5t-7.5 -18.5l-4 -5q-18 -19 -53 -22h-115q-71 -4 -146 16.5t-131.5 53t-103 66t-70.5 57.5l-25 24q-10 10 -27.5 30t-71.5 91 [...]
+<glyph unicode="&#xf18a;" horiz-adv-x="1792" d="M675 252q21 34 11 69t-45 50q-34 14 -73 1t-60 -46q-22 -34 -13 -68.5t43 -50.5t74.5 -2.5t62.5 47.5zM769 373q8 13 3.5 26.5t-17.5 18.5q-14 5 -28.5 -0.5t-21.5 -18.5q-17 -31 13 -45q14 -5 29 0.5t22 18.5zM943 266q-45 -102 -158 -150t-224 -12 q-107 34 -147.5 126.5t6.5 187.5q47 93 151.5 139t210.5 19q111 -29 158.5 -119.5t2.5 -190.5zM1255 426q-9 96 -89 170t-208.5 109t-274.5 21q-223 -23 -369.5 -141.5t-132.5 -264.5q9 -96 89 -170t208.5 -109t274.5 -21q223 23 [...]
+<glyph unicode="&#xf18b;" d="M1133 -34q-171 -94 -368 -94q-196 0 -367 94q138 87 235.5 211t131.5 268q35 -144 132.5 -268t235.5 -211zM638 1394v-485q0 -252 -126.5 -459.5t-330.5 -306.5q-181 215 -181 495q0 187 83.5 349.5t229.5 269.5t325 137zM1536 638q0 -280 -181 -495 q-204 99 -330.5 306.5t-126.5 459.5v485q179 -30 325 -137t229.5 -269.5t83.5 -349.5z" />
+<glyph unicode="&#xf18c;" horiz-adv-x="1408" d="M1402 433q-32 -80 -76 -138t-91 -88.5t-99 -46.5t-101.5 -14.5t-96.5 8.5t-86.5 22t-69.5 27.5t-46 22.5l-17 10q-113 -228 -289.5 -359.5t-384.5 -132.5q-19 0 -32 13t-13 32t13 31.5t32 12.5q173 1 322.5 107.5t251.5 294.5q-36 -14 -72 -23t-83 -13t-91 2.5t-93 28.5 t-92 59t-84.5 100t-74.5 146q114 47 214 57t167.5 -7.5t124.5 -56.5t88.5 -77t56.5 -82q53 131 79 291q-7 -1 -18 -2.5t-46.5 -2.5t-69.5 0.5t-81.5 10t-88.5 23t-84 42.5t-75 65t-54.5 94.5t-28.5 127.5q70  [...]
+<glyph unicode="&#xf18d;" horiz-adv-x="1280" d="M1259 283v-66q0 -85 -57.5 -144.5t-138.5 -59.5h-57l-260 -269v269h-529q-81 0 -138.5 59.5t-57.5 144.5v66h1238zM1259 609v-255h-1238v255h1238zM1259 937v-255h-1238v255h1238zM1259 1077v-67h-1238v67q0 84 57.5 143.5t138.5 59.5h846q81 0 138.5 -59.5t57.5 -143.5z " />
+<glyph unicode="&#xf18e;" d="M1152 640q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192h-352q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h352v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198 t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf190;" d="M1152 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-352v-192q0 -14 -9 -23t-23 -9q-12 0 -24 10l-319 319q-9 9 -9 23t9 23l320 320q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5v-192h352q13 0 22.5 -9.5t9.5 -22.5zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198 t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf191;" d="M1024 960v-640q0 -26 -19 -45t-45 -19q-20 0 -37 12l-448 320q-27 19 -27 52t27 52l448 320q17 12 37 12q26 0 45 -19t19 -45zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf192;" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5 t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf193;" horiz-adv-x="1664" d="M1023 349l102 -204q-58 -179 -210 -290t-339 -111q-156 0 -288.5 77.5t-210 210t-77.5 288.5q0 181 104.5 330t274.5 211l17 -131q-122 -54 -195 -165.5t-73 -244.5q0 -185 131.5 -316.5t316.5 -131.5q126 0 232.5 65t165 175.5t49.5 236.5zM1571 249l58 -114l-256 -128 q-13 -7 -29 -7q-40 0 -57 35l-239 477h-472q-24 0 -42.5 16.5t-21.5 40.5l-96 779q-2 16 6 42q14 51 57 82.5t97 31.5q66 0 113 -47t47 -113q0 -69 -52 -117.5t-120 -41.5l37 -289h423v-128h-407l16 -128h455 [...]
+<glyph unicode="&#xf194;" d="M1254 899q16 85 -21 132q-52 65 -187 45q-17 -3 -41 -12.5t-57.5 -30.5t-64.5 -48.5t-59.5 -70t-44.5 -91.5q80 7 113.5 -16t26.5 -99q-5 -52 -52 -143q-43 -78 -71 -99q-44 -32 -87 14q-23 24 -37.5 64.5t-19 73t-10 84t-8.5 71.5q-23 129 -34 164q-12 37 -35.5 69 t-50.5 40q-57 16 -127 -25q-54 -32 -136.5 -106t-122.5 -102v-7q16 -8 25.5 -26t21.5 -20q21 -3 54.5 8.5t58 10.5t41.5 -30q11 -18 18.5 -38.5t15 -48t12.5 -40.5q17 -46 53 -187q36 -146 57 -197q42 -99 103 -125q43 -12 85 -1.5t7 [...]
+<glyph unicode="&#xf195;" horiz-adv-x="1152" d="M1152 704q0 -191 -94.5 -353t-256.5 -256.5t-353 -94.5h-160q-14 0 -23 9t-9 23v611l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v93l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v250q0 14 9 23t23 9h160 q14 0 23 -9t9 -23v-181l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-93l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-487q188 13 318 151t130 328q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" />
+<glyph unicode="&#xf196;" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-352v-352q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v352h-352q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h352v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-352h352q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832 q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 20 [...]
+<glyph unicode="&#xf197;" horiz-adv-x="1792" />
+<glyph unicode="&#xf198;" horiz-adv-x="1792" />
+<glyph unicode="&#xf199;" horiz-adv-x="1792" />
+<glyph unicode="&#xf19a;" horiz-adv-x="1792" />
+<glyph unicode="&#xf19b;" horiz-adv-x="1792" />
+<glyph unicode="&#xf19c;" horiz-adv-x="1792" />
+<glyph unicode="&#xf19d;" horiz-adv-x="1792" />
+<glyph unicode="&#xf19e;" horiz-adv-x="1792" />
+<glyph unicode="&#xf500;" horiz-adv-x="1792" />
+</font>
+</defs></svg> 
\ No newline at end of file
diff --git a/doc/website-v1/fonts/fontawesome-webfont.ttf b/doc/website-v1/fonts/fontawesome-webfont.ttf
new file mode 100755
index 0000000..e89738d
Binary files /dev/null and b/doc/website-v1/fonts/fontawesome-webfont.ttf differ
diff --git a/doc/website-v1/fonts/fontawesome-webfont.woff b/doc/website-v1/fonts/fontawesome-webfont.woff
new file mode 100755
index 0000000..8c1748a
Binary files /dev/null and b/doc/website-v1/fonts/fontawesome-webfont.woff differ
diff --git a/doc/website-v1/history-guide.txt b/doc/website-v1/history-guide.txt
new file mode 100644
index 0000000..343658a
--- /dev/null
+++ b/doc/website-v1/history-guide.txt
@@ -0,0 +1,3 @@
+= Cluster history =
+
+Work in Progress. Stay tuned.
diff --git a/doc/website-v1/img/laptop.png b/doc/website-v1/img/laptop.png
new file mode 100644
index 0000000..2f831ba
Binary files /dev/null and b/doc/website-v1/img/laptop.png differ
diff --git a/doc/website-v1/img/loader.gif b/doc/website-v1/img/loader.gif
new file mode 100644
index 0000000..b2cfedb
Binary files /dev/null and b/doc/website-v1/img/loader.gif differ
diff --git a/doc/website-v1/img/servers.gif b/doc/website-v1/img/servers.gif
new file mode 100644
index 0000000..20afdcb
Binary files /dev/null and b/doc/website-v1/img/servers.gif differ
diff --git a/doc/website-v1/index.txt b/doc/website-v1/index.txt
new file mode 100644
index 0000000..a2dc767
--- /dev/null
+++ b/doc/website-v1/index.txt
@@ -0,0 +1,19 @@
+Cluster Management Shell
+========================
+
+++++
+<div class="frontpage-image">
+<img src="/img/laptop.png">
+</div>
+++++
+
+The `crm` shell is an advanced command-line interface for
+High-Availability cluster management in GNU/Linux. Effortlessly configure, manage and troubleshoot
+your clusters from the command line, with full tab completion and extensive help.
+`crmsh` also provides advanced features like low-level cluster configuration, 
+cluster scripting and package management, and history exploration tools giving you an instant
+view of what your cluster is doing.
+
+For more information, see the link:/documentation[Documentation]!
+
+
diff --git a/doc/website-v1/installation.txt b/doc/website-v1/installation.txt
new file mode 100644
index 0000000..ee6f10e
--- /dev/null
+++ b/doc/website-v1/installation.txt
@@ -0,0 +1,59 @@
+Installation
+============
+
+Packages for the *current stable version* for 
+SLES, openSUSE, CentOS, Fedora and RHEL:
+
+* https://build.opensuse.org/package/show/network:ha-clustering:Stable/crmsh[OBS Project Page]
+
+* http://download.opensuse.org/repositories/network:/ha-clustering:/Stable/[Direct package downloads]
+
+For other distributions, packages may be available in the respective
+package repositories.
+
+== openSUSE
+
+On openSUSE, you will want to add the
+http://download.opensuse.org/repositories/network:/ha-clustering:/Stable/[network:ha-clustering:Stable] repository, to get the latest stable version of the `crm` shell:
+
+=== Repository links ===
+
+* http://download.opensuse.org/repositories/network:/ha-clustering:/Stable/openSUSE_12.3/[openSUSE 12.3]
+* http://download.opensuse.org/repositories/network:/ha-clustering:/Stable/openSUSE_13.1/[openSUSE 13.1]
+
+
+.Adding a repository using zypper:
+----
+zypper ar <repository> network:ha-clustering:Stable
+----
+
+Once added, you can run
+
+----
+zypper in crmsh
+----
+
+== Fedora 19
+
+Download http://download.opensuse.org/repositories/network:/ha-clustering:/Stable/Fedora_19/network:ha-clustering:Stable.repo[network:ha-clustering:Stable.repo]
+and copy it to the '/etc/yum.repos.d/' directory as `root`.
+
+Then, you can run
+
+----
+yum install crmsh
+----
+
+== Debian
+
+Packages for Debian are provided by the Debian HA maintainers. For more
+information, see the Debian packages list:
+
+- http://packages.debian.org/search?keywords=crmsh
+
+== Ubuntu
+
+There are packages that are fairly up-to-date in the more recent
+versions of Ubuntu:
+
+- https://launchpad.net/ubuntu/+source/crmsh
diff --git a/doc/website-v1/make-news.py b/doc/website-v1/make-news.py
new file mode 100644
index 0000000..22fb77a
--- /dev/null
+++ b/doc/website-v1/make-news.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+"""
+Output a combined news.txt document
+Also write an Atom feed document
+"""
+
+import os
+import sys
+import hashlib
+import datetime
+import time
+
+OUTPUT_HEADER = """= News
+
+"""
+OUTPUT_FOOTER = """
+link:https://savannah.nongnu.org/news/?group_id=10890[Old News Archive]
+"""
+
+ATOM_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+<title>crmsh</title>
+<subtitle>Cluster manager shell news</subtitle>
+<link href="http://crmsh.github.io/atom.xml" rel="self" />
+<link href="http://crmsh.github.io/" />
+<id>%(id)s</id>
+<updated>%(updated)s</updated>
+%(entries)s
+</feed>
+"""
+
+ATOM_NAME = "gen/atom.xml"
+
+root_id = "tag:crmsh.github.io,2014:/atom"
+
+def escape(s):
+    s = s.replace('&', '&')
+    s = s.replace('<', '<')
+    s = s.replace('>', '>')
+    s = s.replace('"', """)
+    return s
+
+class Entry(object):
+    def __init__(self, fname):
+        self.filename = fname
+        self.name = os.path.splitext(os.path.basename(fname))[0]
+        with open(fname) as f:
+            self.title = f.readline().strip()
+            f.readline()
+            l = f.readline()
+            while l.startswith(':'):
+                k, v = l[1:].split(':', 1)
+                k = k.lower()
+                v = v.strip()
+                setattr(self, k, v)
+                l = f.readline()
+            self.content = l + f.read()
+        if not hasattr(self, 'author'):
+            raise ValueError("Missing author")
+        if not hasattr(self, 'email'):
+            raise ValueError("Missing email")
+        if not hasattr(self, 'date'):
+            raise ValueError("Missing date")
+
+    def atom_id(self):
+        return root_id + '::' + hashlib.sha1(self.filename).hexdigest()
+
+    def atom_date(self):
+        return self.date.replace(' ', 'T') + ':00' + time.tzname[0]
+
+    def date_obj(self):
+        from dateutil import parser
+        return (parser.parse(self.date) -
+                datetime.datetime(1970, 1, 1)).total_seconds()
+
+    def atom_content(self):
+        return escape('<pre>\n' + self.content + '\n</pre>\n')
+
+    def atom(self):
+        data = {'title': self.title,
+                'id': self.atom_id(),
+                'updated': self.atom_date(),
+                'name': self.name,
+                'content': self.atom_content(),
+                'author': self.author,
+                'email': self.email}
+        return """<entry>
+<title>%(title)s</title>
+<id>%(id)s</id>
+<updated>%(updated)s</updated>
+<link>http://crmsh.github.io/news/%(name)s</link>
+<content type="html">
+%(content)s
+</content>
+<author>
+<name>%(author)s</name>
+<email>%(email)s</email>
+</author>
+</entry>
+""" % data
+
+
+def sort_entries(entries):
+    return list(reversed(sorted(entries, key=lambda e: e.date_obj())))
+
+
+def make_atom():
+    inputs = sort_entries([Entry(f) for f in sys.argv[2:]])
+    with open(ATOM_NAME, 'w') as output:
+        output.write(ATOM_TEMPLATE % {
+            'id': root_id,
+            'updated': inputs[0].atom_date(),
+            'entries': '\n'.join(f.atom() for f in inputs)
+        })
+
+
+def main():
+    # TODO: sort by date
+    inputs = sort_entries([Entry(f) for f in sys.argv[2:]])
+    with open(sys.argv[1], 'w') as output:
+        output.write(OUTPUT_HEADER)
+        e = inputs[0]
+        output.write("link:/news/%s[%s]\n\n" % (e.name, e.date))
+        output.write(":leveloffset: 1\n")
+        output.write("include::%s[]\n" % (e.filename))
+        output.write(":leveloffset: 0\n\n")
+
+        output.write("''''\n")
+        for e in inputs[1:]:
+            output.write("* link:/news/%s[%s %s]\n" % (e.name, e.date, e.title))
+        output.write(OUTPUT_FOOTER)
+
+if __name__ == "__main__":
+    if sys.argv[1] == ATOM_NAME:
+        make_atom()
+    else:
+        main()
diff --git a/doc/website-v1/man-1.2.txt b/doc/website-v1/man-1.2.txt
new file mode 100644
index 0000000..3bc4523
--- /dev/null
+++ b/doc/website-v1/man-1.2.txt
@@ -0,0 +1,3437 @@
+:man source:   crm
+:man version:  1.2.6
+:man manual:   crmsh documentation
+
+crm(8)
+======
+
+NOTE: This is the documentation for stable release 1.2.6 of `crmsh`.
+
+
+NAME
+----
+crm - Pacemaker command line interface for configuration and management
+
+
+SYNOPSIS
+--------
+*crm* [-D output_type] [-f file] [-c cib] [-H hist_src] [-hFRDw] [--version] [args]
+
+
+[[topics_Description,Program description]]
+DESCRIPTION
+-----------
+Pacemaker configuration is stored in a CIB file (Cluster
+Information Base). The CIB is a set of instructions coded in XML.
+Editing the CIB is a challenge, not only due to its complexity
+and a wide variety of options, but also because XML is more
+computer than user friendly. The `crm` shell alleviates this
+issue significantly by introducing small and simple configuration
+language. The CIB is translated into this language on the fly.
+
+`crm` is also a management tool. For management tasks it relies
+almost exclusively on other command line tools, such as
+`crm_resource(8)` or `crm_attribute(8)`.  Use of these programs
+is, however, plagued by the notorious weakness common to all UNIX
+tools: a multitude of options, necessary for operation and yet
+very hard to remember. `crm` tries to present a consistent
+interface to the user and to hide the arcane detail.
+
+It may be used either as an interactive shell or for single
+commands directly on the shell's command line. It is also
+possible to feed it a set of commands from standard input or a
+file, thus turning it into a scripting tool. Templates with ready
+made configurations may help newbies learn about the cluster
+configuration or facilitate testing procedures.
+
+The `crm` shell is line oriented: every command must start and
+finish on the same line. It is possible to use a continuation
+character (`\`) to write one command in two or more lines. The
+continuation character is commonly used when displaying
+configurations.
+
+OPTIONS
+-------
+*-f, --file*='FILE'::
+	Load commands from the given file. If the file is `-` then
+    use terminal `stdin`.
+
+*-c, --cib*='CIB'::
+    Start the session with the given shadow CIB file.
+    Equivalent to `cib use`.
+
+*-D, --display=*'OUTPUT_TYPE'::
+	Choose one of the output options: `plain`, `color`, or
+    `uppercase`. The default is `color` if the terminal emulation
+    supports colors. Otherwise, `plain` is used.
+
+*-F, --force*::
+    Make `crm` proceed with doing changes even though it would
+    normally ask user to confirm some of them. Mostly useful in
+    scripts.
+
+*-w, --wait*::
+    Make `crm` wait for the cluster transition to finish (for the
+    changes to take effect) after each processed line.
+
+*-H, --history*='DIR|FILE'::
+    The `history` commands can examine either live cluster
+    (default) or a report generated by `hb_report`. Use this
+    option to specify a directory or file containing the report.
+
+*-h, --help*::
+    Print help page.
+
+*--version*::
+    Print crmsh version and build information (Mercurial Hg
+    changeset hash).
+
+*-R, --regression-tests*::
+    Run in the regression test mode. Used mainly by the
+    regression testing suite.
+
+*-d, --debug*::
+    Print some debug information. Used by developers. [Not yet
+    refined enough to print useful information for other users.]
+
+[[topics_Introduction,Introduction to the user interface]]
+== Introduction to the user interface
+
+Arguably the most important aspect of `crm` is the user
+interface. We begin with an informal introduction so that the
+reader may get acquainted with it and get a general feeling of
+the tool. It is probably best just to give some examples:
+
+1. Command line (one-shot) use:
+
+        # crm resource stop www_app
+
+2. Interactive use:
+
+        # crm
+        crm(live)# resource
+        crm(live)resource# unmanage tetris_1
+        crm(live)resource# end
+        crm(live)# node standby node4
+
+3. Cluster configuration:
+
+    # crm configure<<EOF
+      #
+      # resources
+      #
+      primitive disk0 iscsi \
+        params portal=192.168.2.108:3260 target=iqn.2008-07.com.suse:disk0
+      primitive fs0 Filesystem \
+        params device=/dev/disk/by-label/disk0 directory=/disk0 fstype=ext3
+      primitive internal_ip IPaddr params ip=192.168.1.101
+      primitive apache apache \
+        params configfile=/disk0/etc/apache2/site0.conf
+      primitive apcfence stonith:apcsmart \
+        params ttydev=/dev/ttyS0 hostlist="node1 node2" \
+        op start timeout=60s
+      primitive pingd pingd \
+        params name=pingd dampen=5s multiplier=100 host_list="r1 r2"
+      #
+      # monitor apache and the UPS
+      #
+      monitor apache 60s:30s
+      monitor apcfence 120m:60s
+      #
+      # cluster layout
+      #
+      group internal_www \
+        disk0 fs0 internal_ip apache
+      clone fence apcfence \
+        meta globally-unique=false clone-max=2 clone-node-max=1
+      clone conn pingd \
+        meta globally-unique=false clone-max=2 clone-node-max=1
+      location node_pref internal_www \
+        rule 50: #uname eq node1 \
+        rule pingd: defined pingd
+      #
+      # cluster properties
+      #
+      property stonith-enabled=true
+      commit
+    EOF
+
+If you've ever done a CRM style configuration, you should be able
+to understand the above examples without much difficulties.  The
+shell should provide a means to manage the cluster efficiently or
+put together a configuration in a concise manner.
+
+The `(live)` string in the prompt signifies that the current CIB
+in use is the cluster live configuration. It is also possible to
+work with the so-called shadow CIBs, i.e. configurations which
+are stored in files and aren't active, but may be applied at any
+time to the cluster.
+
+Since the CIB is hierarchical such is the interface too. There
+are several levels and entering each of them enables the user to
+use a certain set of commands.
+
+[[topics_Shadows,Shadow CIB usage]]
+== Shadow CIB usage
+
+Shadow CIB is a normal cluster configuration stored in a file.
+They may be manipulated in the same way like the _live_ CIB, but
+these changes have no effect on the cluster resources. The
+administrator may choose to apply any of them to the cluster,
+thus replacing the running configuration with the one which is in
+the shadow CIB. The `crm` prompt always contains the name of the
+configuration which is currently in use or string _live_ if we
+are using the current cluster configuration.
+
+At the configure level no changes take place before the `commit`
+command. Sometimes though, the administrator may start working
+with the running configuration, but change mind and instead of
+committing the changes to the cluster save them to a shadow CIB.
+This short `configure` session excerpt shows how:
+...............
+    crm(live)configure# cib new test-2
+    INFO: test-2 shadow CIB created
+    crm(test-2)configure# commit
+...............
+
+[[topics_Templates,Configuration templates]]
+== Configuration templates
+
+Configuration templates are ready made configurations created by
+cluster experts. They are designed in such a way so that users
+may generate valid cluster configurations with minimum effort.
+If you are new to Pacemaker, templates may be the best way to
+start.
+
+We will show here how to create a simple yet functional Apache
+configuration:
+...............
+    # crm configure
+    crm(live)configure# template
+    crm(live)configure template# list templates
+    apache       filesystem   virtual-ip   
+    crm(live)configure template# new web <TAB><TAB>
+    apache       filesystem   virtual-ip   
+    crm(live)configure template# new web apache
+    INFO: pulling in template apache
+    INFO: pulling in template virtual-ip
+    crm(live)configure template# list
+    web2-d       web2         vip2         web3         vip          web
+...............
+
+We enter the `template` level from `configure`. Use the `list`
+command to show templates available on the system. The `new`
+command creates a configuration from the `apache` template. You
+can use tab completion to pick templates. Note that the apache
+template depends on a virtual IP address which is automatically
+pulled along. The `list` command shows the just created `web`
+configuration, among other configurations (I hope that you,
+unlike me, will use more sensible and descriptive names).
+
+The `show` command, which displays the resulting configuration,
+may be used to get an idea about the minimum required changes
+which have to be done. All `ERROR` messages show the line numbers
+in which the respective parameters are to be defined:
+...............
+    crm(live)configure template# show
+    ERROR: 23: required parameter ip not set
+    ERROR: 61: required parameter id not set
+    ERROR: 65: required parameter configfile not set
+    crm(live)configure template# edit
+...............
+
+The `edit` command invokes the preferred text editor with the
+`web` configuration. At the top of the file, the user is advised
+how to make changes. A good template should require from the user
+to specify only parameters. For example, the `web` configuration
+we created above has the following required and optional
+parameters (all parameter lines start with `%%`):
+...............
+    $ grep -n ^%% ~/.crmconf/web
+    23:%% ip 
+    31:%% netmask 
+    35:%% lvs_support 
+    61:%% id 
+    65:%% configfile 
+    71:%% options 
+    76:%% envfiles 
+...............
+
+These lines are the only ones that should be modified. Simply
+append the parameter value at the end of the line. For instance,
+after editing this template, the result could look like this (we
+used tabs instead of spaces to make the values stand out):
+...............
+    $ grep -n ^%% ~/.crmconf/web
+    23:%% ip                192.168.1.101
+    31:%% netmask 
+    35:%% lvs_support 
+    61:%% id                websvc
+    65:%% configfile        /etc/apache2/httpd.conf
+    71:%% options 
+    76:%% envfiles 
+...............
+
+As you can see, the parameter line format is very simple:
+...............
+    %% <name> <value>
+...............
+
+After editing the file, use `show` again to display the
+configuration:
+...............
+    crm(live)configure template# show
+    primitive virtual-ip ocf:heartbeat:IPaddr \
+            params ip="192.168.1.101"
+    primitive apache ocf:heartbeat:apache \
+            params configfile="/etc/apache2/httpd.conf"
+    monitor apache 120s:60s
+    group websvc \
+            apache virtual-ip
+...............
+
+The target resource of the apache template is a group which we
+named `websvc` in this sample session.
+
+This configuration looks exactly as you could type it at the
+`configure` level. The point of templates is to save you some
+typing. It is important, however, to understand the configuration
+produced.
+
+Finally, the configuration may be applied to the current
+crm configuration (note how the configuration changed slightly,
+though it is still equivalent, after being digested at the
+`configure` level):
+...............
+    crm(live)configure template# apply 
+    crm(live)configure template# cd ..
+    crm(live)configure# show
+    node xen-b
+    node xen-c
+    primitive apache ocf:heartbeat:apache \
+        params configfile="/etc/apache2/httpd.conf" \
+        op monitor interval="120s" timeout="60s"
+    primitive virtual-ip ocf:heartbeat:IPaddr \
+        params ip="192.168.1.101"
+    group websvc apache virtual-ip
+...............
+
+Note that this still does not commit the configuration to the CIB
+which is used in the shell, either the running one (`live`) or
+some shadow CIB. For that you still need to execute the `commit`
+command.
+
+To complete our example, we should also define the preferred node
+to run the service:
+...............
+    crm(live)configure# location websvc-pref websvc 100: xen-b
+...............
+
+If you are not happy with some resource names which are provided
+by default, you can rename them now:
+...............
+    crm(live)configure# rename virtual-ip intranet-ip
+    crm(live)configure# show
+    node xen-b
+    node xen-c
+    primitive apache ocf:heartbeat:apache \
+            params configfile="/etc/apache2/httpd.conf" \
+            op monitor interval="120s" timeout="60s"
+    primitive intranet-ip ocf:heartbeat:IPaddr \
+            params ip="192.168.1.101"
+    group websvc apache intranet-ip
+    location websvc-pref websvc 100: xen-b
+...............
+
+To summarize, working with templates typically consists of the
+following steps:
+
+- `new`: create a new configuration from templates
+- `edit`: define parameters, at least the required ones
+- `show`: see if the configuration is valid
+- `apply`: apply the configuration to the `configure` level
+
+[[topics_Testing,Resource testing]]
+== Resource testing
+
+The amount of detail in a cluster makes all configurations prone
+to errors. By far the largest number of issues in a cluster is
+due to bad resource configuration. The shell can help quickly
+diagnose such problems. And considerably reduce your keyboard
+wear.
+
+Let's say that we entered the following configuration:
+...............
+    node xen-b
+    node xen-c
+    node xen-d
+    primitive fencer stonith:external/libvirt \
+            params hypervisor_uri="qemu+tcp://10.2.13.1/system" \
+                hostlist="xen-b xen-c xen-d" \
+            op monitor interval="2h"
+    primitive svc ocf:heartbeat:Xinetd \
+            params service="systat" \
+            op monitor interval="30s"
+    primitive intranet-ip ocf:heartbeat:IPaddr2 \
+            params ip="10.2.13.100" \
+            op monitor interval="30s"
+    primitive apache ocf:heartbeat:apache \
+            params configfile="/etc/apache2/httpd.conf" \
+            op monitor interval="120s" timeout="60s"
+    group websvc apache intranet-ip
+    location websvc-pref websvc 100: xen-b
+...............
+
+Before typing `commit` to submit the configuration to the cib we
+can make sure that all resources are usable on all nodes:
+...............
+    crm(live)configure# rsctest websvc svc fencer
+...............
+
+It is important that resources being tested are not running on
+any nodes. Otherwise, the `rsctest` command will refuse to do
+anything. Of course, if the current configuration resides in a
+CIB shadow, then a `commit` is irrelevant. The point being that
+resources are not running on any node.
+
+.Note on stopping all resources
+****************************
+Alternatively to not committing a configuration, it is also
+possible to tell Pacemaker not to start any resources:
+
+...............
+    crm(live)configure# property stop-all-resources="yes"
+...............
+Almost none---resources of class stonith are still started. But
+shell is not as strict when it comes to stonith resources.
+****************************
+
+Order of resources is significant insofar that a resource depends
+on all resources to its left. In most configurations, it's
+probably practical to test resources in several runs, based on
+their dependencies.
+
+Apart from groups, `crm` does not interpret constraints and
+therefore knows nothing about resource dependencies. It also
+doesn't know if a resource can run on a node at all in case of an
+asymmetric cluster. It is up to the user to specify a list of
+eligible nodes if a resource is not meant to run on every node.
+
+[[topics_Completion,Tab completion]]
+== Tab completion
+
+The `crm` makes extensive use of tab completion. The completion
+is both static (i.e. for `crm` commands) and dynamic. The latter
+takes into account the current status of the cluster or
+information from installed resource agents. Sometimes, completion
+may also be used to get short help on resource parameters. Here a
+few examples:
+...............
+    crm(live)# resource
+    crm(live)resource# <TAB><TAB>
+    bye           failcount     move          restart       unmigrate 
+    cd            help          param         show          unmove 
+    cleanup       list          promote       start         up 
+    demote        manage        quit          status utilization 
+    end           meta          refresh       stop          
+    exit          migrate       reprobe       unmanage      
+    crm(live)resource# end
+    crm(live)# configure 
+    crm(live)configure# primitive fence-1 <TAB><TAB>
+    heartbeat:  lsb:        ocf:        stonith:    
+    crm(live)configure# primitive fence-1 stonith:<TAB><TAB>
+    apcmaster                external/ippower9258     fence_legacy 
+    apcmastersnmp            external/kdumpcheck      ibmhmc 
+    apcsmart                 external/libvirt         ipmilan 
+    baytech                  external/nut             meatware 
+    bladehpi                 external/rackpdu         null 
+    cyclades                 external/riloe           nw_rpc100s 
+    drac3                    external/sbd             rcd_serial 
+    external/drac5           external/ssh             rps10 
+    external/dracmc-telnet   external/ssh-bad         ssh 
+    external/hmchttp         external/ssh-slow        suicide 
+    external/ibmrsa          external/vmware          wti_mpc 
+    external/ibmrsa-telnet   external/xen0            wti_nps 
+    external/ipmi            external/xen0-ha         
+    crm(live)configure# primitive fence-1 stonith:ipmilan params <TAB><TAB>
+    auth=      hostname=  ipaddr=    login=     password=  port=      priv=
+    crm(live)configure# primitive fence-1 stonith:ipmilan params auth=<TAB><TAB>
+    auth* (string)
+        The authorization type of the IPMI session ("none", "straight", "md2", or "md5")
+    crm(live)configure# primitive fence-1 stonith:ipmilan params auth=
+...............
+
+[[topics_Checks,Configuration semantic checks]]
+== Configuration semantic checks
+
+Resource definitions may be checked against the meta-data
+provided with the resource agents. These checks are currently
+carried out:
+
+- are required parameters set
+- existence of defined parameters
+- timeout values for operations
+
+The parameter checks are obvious and need no further explanation.
+Failures in these checks are treated as configuration errors.
+
+The timeouts for operations should be at least as long as those
+recommended in the meta-data. Too short timeout values are a
+common mistake in cluster configurations and, even worse, they
+often slip through if cluster testing was not thorough. Though
+operation timeouts issues are treated as warnings, make sure that
+the timeouts are usable in your environment. Note also that the
+values given are just _advisory minimum_---your resources may
+require longer timeouts.
+
+User may tune the frequency of checks and the treatment of errors
+by the <<cmdhelp_options_check-frequency,`check-frequency`>> and
+<<cmdhelp_options_check-mode,`check-mode`>> preferences.
+
+Note that if the `check-frequency` is set to `always` and the
+`check-mode` to `strict`, errors are not tolerated and such
+configuration cannot be saved.
+
+[[topics_Security,Access Control Lists (ACL)]]
+== Access Control Lists (ACL)
+
+By default, the users from the `haclient` group have full access
+to the cluster (or, more precisely, to the CIB). Access control
+lists allow for finer access control to the cluster.
+
+Access control lists consist of an ordered set of access rules.
+Each rule allows read or write access or denies access
+completely. Rules are typically combined to produce a specific
+role. Then, users may be assigned a role.
+
+For instance, this is a role which defines a set of rules
+allowing management of a single resource:
+
+...............
+    role bigdb_admin \
+        write meta:bigdb:target-role \
+        write meta:bigdb:is-managed \
+        write location:bigdb \
+        read ref:bigdb
+...............
+
+The first two rules allow modifying the `target-role` and
+`is-managed` meta attributes which effectively enables users in
+this role to stop/start and manage/unmanage the resource. The
+constraints write access rule allows moving the resource around.
+Finally, the user is granted read access to the resource
+definition.
+
+For proper operation of all Pacemaker programs, it is advisable
+to add the following role to all users:
+
+...............
+    role read_all \
+        read cib
+...............
+
+For finer grained read access try with the rules listed in the
+following role:
+
+...............
+    role basic_read \
+        read node attribute:uname \
+        read node attribute:type \
+        read property \
+        read status
+...............
+
+It is however possible that some Pacemaker programs (e.g.
+`ptest`) may not function correctly if the whole CIB is not
+readable.
+
+Some of the ACL rules in the examples above are expanded by the
+shell to XPath specifications. For instance,
+`meta:bigdb:target-role` is a shortcut for
+`//primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']`.
+You can see the expansion by showing XML:
+
+...............
+    crm(live) configure# show xml bigdb_admin
+    ...
+    <acls>
+      <acl_role id="bigdb_admin">
+          <write id="bigdb_admin-write"
+          xpath="//primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']"/>
+...............
+
+Many different XPath expressions can have equal meaning. For
+instance, the following two are equal, but only the first one is
+going to be recognized as shortcut:
+
+...............
+      //primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']
+      //resources/primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']
+...............
+
+XPath is a powerful language, but you should try to keep your ACL
+xpaths simple and the builtin shortcuts should be used whenever
+possible.
+
+[[topics_Reference,Command reference]]
+== Command reference
+
+We define a small and simple language. Most commands consist of
+just a list of simple tokens. The only complex constructs are
+found at the `configure` level.
+
+The syntax is described in a somewhat informal manner: `<>`
+denotes a string, `[]` means that the construct is optional, the
+ellipsis (`...`) signifies that the previous construct may be
+repeated, `|` means pick one of many, and the rest are literals
+(strings, `:`, `=`).
+
+=== `status`
+
+Show cluster status. The status is displayed by `crm_mon`. Supply
+additional arguments for more information or different format.
+See `crm_mon(8)` for more details.
+
+Usage:
+...............
+        status [<option> ...]
+
+        option :: bynode | inactive | ops | timing | failcounts
+...............
+
+[[cmdhelp_cib,CIB shadow management]]
+=== `cib` (shadow CIBs)
+
+This level is for management of shadow CIBs. It is available both
+at the top level and the `configure` level.
+
+All the commands are implemented using `cib_shadow(8)` and the
+`CIB_shadow` environment variable. The user prompt always
+includes the name of the currently active shadow or the live CIB.
+
+[[cmdhelp_cib_new,create a new shadow CIB]]
+==== `new`
+
+Create a new shadow CIB. The live cluster configuration and
+status is copied to the shadow CIB. Specify `withstatus` if you
+want to edit the status section of the shadow CIB (see the
+<<cmdhelp_cibstatus,cibstatus section>>). Add `force` to force overwriting the
+existing shadow CIB.
+
+To start with an empty configuration that is not copied from the live
+CIB, specify the `empty` keyword. (This also allows a shadow CIB to be
+created in case no cluster is running.)
+
+Usage:
+...............
+        new <cib> [withstatus] [force] [empty]
+...............
+
+[[cmdhelp_cib_delete,delete a shadow CIB]]
+==== `delete`
+
+Delete an existing shadow CIB.
+
+Usage:
+...............
+        delete <cib>
+...............
+
+[[cmdhelp_cib_reset,copy live cib to a shadow CIB]]
+==== `reset`
+
+Copy the current cluster configuration into the shadow CIB.
+
+Usage:
+...............
+        reset <cib>
+...............
+
+[[cmdhelp_cib_commit,copy a shadow CIB to the cluster]]
+==== `commit`
+
+Apply a shadow CIB to the cluster.
+
+Usage:
+...............
+        commit <cib>
+...............
+
+[[cmdhelp_cib_use,change working CIB]]
+==== `use`
+
+Choose a CIB source. If you want to edit the status from the
+shadow CIB specify `withstatus` (see <<cmdhelp_cibstatus,`cibstatus`>>).
+Leave out the CIB name to switch to the running CIB.
+
+Usage:
+...............
+        use [<cib>] [withstatus]
+...............
+
+[[cmdhelp_cib_diff,diff between the shadow CIB and the live CIB]]
+==== `diff`
+
+Print differences between the current cluster configuration and
+the active shadow CIB.
+
+Usage:
+...............
+        diff
+...............
+
+[[cmdhelp_cib_list,list all shadow CIBs]]
+==== `list`
+
+List existing shadow CIBs.
+
+Usage:
+...............
+        list
+...............
+
+[[cmdhelp_cib_import,import a CIB or PE input file to a shadow]]
+==== `import`
+
+At times it may be useful to create a shadow file from the
+existing CIB. The CIB may be specified as file or as a PE input
+file number. The shell will look up files in the local directory
+first and then in the PE directory (typically `/var/lib/pengine`).
+Once the CIB file is found, it is copied to a shadow and this
+shadow is immediately available for use at both `configure` and
+`cibstatus` levels.
+
+If the shadow name is omitted then the target shadow is named
+after the input CIB file.
+
+Note that there are often more than one PE input file, so you may
+need to specify the full name.
+
+Usage:
+...............
+        import {<file>|<number>} [<shadow>]
+...............
+Examples:
+...............
+        import pe-warn-2222
+        import 2289 issue2
+...............
+
+[[cmdhelp_cib_cibstatus,CIB status management and editing]]
+==== `cibstatus`
+
+Enter edit and manage the CIB status section level. See the
+<<cmdhelp_cibstatus,CIB status management section>>.
+
+[[cmdhelp_ra,Resource Agents (RA) lists and documentation]]
+=== `ra`
+
+This level contains commands which show various information about
+the installed resource agents. It is available both at the top
+level and at the `configure` level.
+
+[[cmdhelp_ra_classes,list classes and providers]]
+==== `classes`
+
+Print all resource agents' classes and, where appropriate, a list
+of available providers.
+
+Usage:
+...............
+        classes
+...............
+
+[[cmdhelp_ra_list,list RA for a class (and provider)]]
+==== `list`
+
+List available resource agents for the given class. If the class
+is `ocf`, supply a provider to get agents which are available
+only from that provider.
+
+Usage:
+...............
+        list <class> [<provider>]
+...............
+Example:
+...............
+        list ocf pacemaker
+...............
+
+[[cmdhelp_ra_meta,show meta data for a RA]]
+==== `meta` (`info`)
+
+Show the meta-data of a resource agent type. This is where users
+can find information on how to use a resource agent. It is also
+possible to get information from some programs: `pengine`,
+`crmd`, `cib`, and `stonithd`. Just specify the program name
+instead of an RA.
+
+Usage:
+...............
+        info [<class>:[<provider>:]]<type>
+        info <type> <class> [<provider>] (obsolete)
+...............
+Example:
+...............
+        info apache
+        info ocf:pacemaker:Dummy
+        info stonith:ipmilan
+        info pengine
+...............
+
+[[cmdhelp_ra_providers,show providers for a RA and a class]]
+==== `providers`
+
+List providers for a resource agent type. The class parameter
+defaults to `ocf`.
+
+Usage:
+...............
+        providers <type> [<class>]
+...............
+Example:
+...............
+        providers apache
+...............
+
+[[cmdhelp_resource,Resource management]]
+=== `resource`
+
+At this level resources may be managed.
+
+All (or almost all) commands are implemented with the CRM tools
+such as `crm_resource(8)`.
+
+[[cmdhelp_resource_status,show status of resources]]
+==== `status` (`show`, `list`)
+
+Print resource status. If the resource parameter is left out
+status of all resources is printed.
+
+Usage:
+...............
+        status [<rsc>]
+...............
+
+[[cmdhelp_resource_start,start a resource]]
+==== `start`
+
+Start a resource by setting the `target-role` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `target-role` attributes
+are removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+        start <rsc>
+...............
+
+[[cmdhelp_resource_stop,stop a resource]]
+==== `stop`
+
+Stop a resource using the `target-role` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `target-role` attributes
+are removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+        stop <rsc>
+...............
+
+[[cmdhelp_resource_restart,restart a resource]]
+==== `restart`
+
+Restart a resource. This is essentially a shortcut for resource
+stop followed by a start. The shell is first going to wait for
+the stop to finish, that is for all resources to really stop, and
+only then to order the start action. Due to this command
+entailing a whole set of operations, informational messages are
+printed to let the user see some progress.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+        restart <rsc>
+...............
+Example:
+...............
+        # crm resource restart g_webserver
+        INFO: ordering g_webserver to stop
+        waiting for stop to finish .... done
+        INFO: ordering g_webserver to start
+        # 
+...............
+
+[[cmdhelp_resource_promote,promote a master-slave resource]]
+==== `promote`
+
+Promote a master-slave resource using the `target-role`
+attribute.
+
+Usage:
+...............
+        promote <rsc>
+...............
+
+[[cmdhelp_resource_demote,demote a master-slave resource]]
+==== `demote`
+
+Demote a master-slave resource using the `target-role`
+attribute.
+
+Usage:
+...............
+        demote <rsc>
+...............
+
+[[cmdhelp_resource_manage,put a resource into managed mode]]
+==== `manage`
+
+Manage a resource using the `is-managed` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `is-managed` attributes are
+removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+        manage <rsc>
+...............
+
+[[cmdhelp_resource_unmanage,put a resource into unmanaged mode]]
+==== `unmanage`
+
+Unmanage a resource using the `is-managed` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `is-managed` attributes are
+removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+        unmanage <rsc>
+...............
+
+[[cmdhelp_resource_migrate,migrate a resource to another node]]
+==== `migrate` (`move`)
+
+Migrate a resource to a different node. If node is left out, the
+resource is migrated by creating a constraint which prevents it from
+running on the current node. Additionally, you may specify a
+lifetime for the constraint---once it expires, the location
+constraint will no longer be active.
+
+Usage:
+...............
+        migrate <rsc> [<node>] [<lifetime>] [force]
+...............
+
+[[cmdhelp_resource_unmigrate,unmigrate a resource to another node]]
+==== `unmigrate` (`unmove`)
+
+Remove the constraint generated by the previous migrate command.
+
+Usage:
+...............
+        unmigrate <rsc>
+...............
+
+[[cmdhelp_resource_param,manage a parameter of a resource]]
+==== `param`
+
+Show/edit/delete a parameter of a resource.
+
+Usage:
+...............
+        param <rsc> set <param> <value>
+        param <rsc> delete <param>
+        param <rsc> show <param>
+...............
+Example:
+...............
+        param ip_0 show ip
+...............
+
+[[cmdhelp_resource_secret,manage sensitive parameters]]
+==== `secret`
+
+Sensitive parameters can be kept in local files rather than CIB
+in order to prevent accidental data exposure. Use the `secret`
+command to manage such parameters. `stash` and `unstash` move the
+value from the CIB and back to the CIB respectively. The `set`
+subcommand sets the parameter to the provided value. `delete`
+removes the parameter completely. `show` displays the value of
+the parameter from the local file. Use `check` to verify if the
+local file content is valid.
+
+Usage:
+...............
+        secret <rsc> set <param> <value>
+        secret <rsc> stash <param>
+        secret <rsc> unstash <param>
+        secret <rsc> delete <param>
+        secret <rsc> show <param>
+        secret <rsc> check <param>
+...............
+Example:
+...............
+        secret fence_1 show password
+        secret fence_1 stash password
+        secret fence_1 set password secret_value
+...............
+
+[[cmdhelp_resource_meta,manage a meta attribute]]
+==== `meta`
+
+Show/edit/delete a meta attribute of a resource. Currently, all
+meta attributes of a resource may be managed with other commands
+such as `resource stop`.
+
+Usage:
+...............
+        meta <rsc> set <attr> <value>
+        meta <rsc> delete <attr>
+        meta <rsc> show <attr>
+...............
+Example:
+...............
+        meta ip_0 set target-role stopped
+...............
+
+[[cmdhelp_resource_utilization,manage a utilization attribute]]
+==== `utilization`
+
+Show/edit/delete a utilization attribute of a resource. These
+attributes describe hardware requirements. By setting the
+`placement-strategy` cluster property appropriately, it is
+possible then to distribute resources based on resource
+requirements and node size. See also <<cmdhelp_node_utilization,node utilization attributes>>.
+
+Usage:
+...............
+        utilization <rsc> set <attr> <value>
+        utilization <rsc> delete <attr>
+        utilization <rsc> show <attr>
+...............
+Example:
+...............
+        utilization xen1 set memory 4096
+...............
+
+[[cmdhelp_resource_failcount,manage failcounts]]
+==== `failcount`
+
+Show/edit/delete the failcount of a resource.
+
+Usage:
+...............
+        failcount <rsc> set <node> <value>
+        failcount <rsc> delete <node>
+        failcount <rsc> show <node>
+...............
+Example:
+...............
+        failcount fs_0 delete node2
+...............
+
+[[cmdhelp_resource_cleanup,cleanup resource status]]
+==== `cleanup`
+
+Cleanup resource status. Typically done after the resource has
+temporarily failed. If a node is omitted, cleanup on all nodes.
+If there are many nodes, the command may take a while.
+
+Usage:
+...............
+        cleanup <rsc> [<node>]
+...............
+
+[[cmdhelp_resource_refresh,refresh CIB from the LRM status]]
+==== `refresh`
+
+Refresh CIB from the LRM status.
+
+Usage:
+...............
+        refresh [<node>]
+...............
+
+[[cmdhelp_resource_reprobe,probe for resources not started by the CRM]]
+==== `reprobe`
+
+Probe for resources not started by the CRM.
+
+Usage:
+...............
+        reprobe [<node>]
+...............
+
+[[cmdhelp_resource_trace,start RA tracing]]
+==== `trace`
+
+Start tracing RA for the given operation. The trace files are
+stored in `$HA_VARLIB/trace_ra`. If the operation to be traced is
+monitor, note that the number of trace files can grow very
+quickly.
+
+Usage:
+...............
+        trace <rsc> <op> [<interval>]
+...............
+Example:
+...............
+        trace fs start
+...............
+
+[[cmdhelp_resource_untrace,stop RA tracing]]
+==== `untrace`
+
+Stop tracing RA for the given operation.
+
+Usage:
+...............
+        untrace <rsc> <op> [<interval>]
+...............
+Example:
+...............
+        untrace fs start
+...............
+
+[[cmdhelp_node,Nodes management]]
+=== `node`
+
+Node management and status commands.
+
+[[cmdhelp_node_status,show nodes' status as XML]]
+==== `status`
+
+Show nodes' status as XML. If the node parameter is omitted then
+all nodes are shown.
+
+Usage:
+...............
+        status [<node>]
+...............
+
+[[cmdhelp_node_show,show node]]
+==== `show`
+
+Show a node definition. If the node parameter is omitted then all
+nodes are shown.
+
+Usage:
+...............
+        show [<node>]
+...............
+
+[[cmdhelp_node_standby,put node into standby]]
+==== `standby`
+
+Set a node to standby status. The node parameter defaults to the
+node where the command is run. Additionally, you may specify a
+lifetime for the standby---if set to `reboot`, the node will be
+back online once it reboots. `forever` will keep the node in
+standby after reboot.
+
+Usage:
+...............
+        standby [<node>] [<lifetime>]
+
+        lifetime :: reboot | forever
+...............
+
+[[cmdhelp_node_online,set node online]]
+==== `online`
+
+Set a node to online status. The node parameter
+defaults to the node where the command is run.
+
+Usage:
+...............
+        online [<node>]
+...............
+
+[[cmdhelp_node_maintenance,put node into maintenance mode]]
+==== `maintenance`
+
+Set the node status to maintenance. This is equivalent to the
+cluster-wide `maintenance-mode` property but puts just one node
+into the maintenance mode. The node parameter defaults to the
+node where the command is run.
+
+Usage:
+...............
+        maintenance [<node>]
+...............
+
+[[cmdhelp_node_ready,put node into ready mode]]
+==== `ready`
+
+Set the node's maintenance status to `off`. The node should be
+now again fully operational and capable of running resource
+operations.
+
+Usage:
+...............
+        ready [<node>]
+...............
+
+[[cmdhelp_node_fence,fence node]]
+==== `fence`
+
+Make CRM fence a node. This functionality depends on stonith
+resources capable of fencing the specified node. No such stonith
+resources, no fencing will happen.
+
+Usage:
+...............
+        fence <node>
+...............
+
+[[cmdhelp_node_clearstate,Clear node state]]
+==== `clearnodestate`
+
+Resets and clears the state of the specified node. This node is
+afterwards assumed clean and offline. This command can be used to
+manually confirm that a node has been fenced (e.g., powered off).
+
+Be careful! This can cause data corruption if you confirm that a node is
+down that is, in fact, not cleanly down - the cluster will proceed as if
+the fence had succeeded, possibly starting resources multiple times.
+
+Usage:
+...............
+        clearstate <node>
+...............
+
+[[cmdhelp_node_delete,delete node]]
+==== `delete`
+
+Delete a node. This command will remove the node from the CIB
+and, in case the cluster stack is running, use the appropriate
+program (`crm_node` or `hb_delnode`) to remove the node from the
+membership.
+
+If the node is still listed as active and a member of our
+partition we refuse to remove it. With the global force option
+(`-F`) we will try to delete the node anyway.
+
+Usage:
+...............
+        delete <node>
+...............
+
+[[cmdhelp_node_attribute,manage attributes]]
+==== `attribute`
+
+Edit node attributes. This kind of attribute should refer to
+relatively static properties, such as memory size.
+
+Usage:
+...............
+        attribute <node> set <attr> <value>
+        attribute <node> delete <attr>
+        attribute <node> show <attr>
+...............
+Example:
+...............
+        attribute node_1 set memory_size 4096
+...............
+
+[[cmdhelp_node_utilization,manage utilization attributes]]
+==== `utilization`
+
+Edit node utilization attributes. These attributes describe
+hardware characteristics as integer numbers such as memory size
+or the number of CPUs. By setting the `placement-strategy`
+cluster property appropriately, it is possible then to distribute
+resources based on resource requirements and node size. See also
+<<cmdhelp_resource_utilization,resource utilization attributes>>.
+
+Usage:
+...............
+        utilization <node> set <attr> <value>
+        utilization <node> delete <attr>
+        utilization <node> show <attr>
+...............
+Examples:
+...............
+        utilization node_1 set memory 16384
+        utilization node_1 show cpu
+...............
+
+[[cmdhelp_node_status-attr,manage status attributes]]
+==== `status-attr`
+
+Edit node attributes which are in the CIB status section, i.e.
+attributes which hold properties of a more volatile nature. One
+typical example is attribute generated by the `pingd` utility.
+
+Usage:
+...............
+        status-attr <node> set <attr> <value>
+        status-attr <node> delete <attr>
+        status-attr <node> show <attr>
+...............
+Example:
+...............
+        status-attr node_1 show pingd
+...............
+
+[[cmdhelp_site,site support]]
+=== `site`
+
+A cluster may consist of two or more subclusters in different and
+distant locations. This set of commands supports such setups.
+
+[[cmdhelp_site_ticket,manage site tickets]]
+==== `ticket`
+
+Tickets are cluster-wide attributes. They can be managed at the
+site where this command is executed.
+
+It is then possible to constrain resources depending on the
+ticket availability (see the <<cmdhelp_configure_rsc_ticket,`rsc_ticket`>> command
+for more details).
+
+Usage:
+...............
+        ticket {grant|revoke|standby|activate|show|time|delete} <ticket>
+...............
+Example:
+...............
+        ticket grant ticket1
+...............
+
+[[cmdhelp_options,user preferences]]
+=== `options`
+
+The user may set various options for the crm shell itself.
+
+[[cmdhelp_options_skill-level,set skill level]]
+==== `skill-level`
+
+Based on the skill-level setting, the user is allowed to use only
+a subset of commands. There are three levels: operator,
+administrator, and expert. The operator level allows only
+commands at the `resource` and `node` levels, but not editing
+or deleting resources. The administrator may do that and may also
+configure the cluster at the `configure` level and manage the
+shadow CIBs. The expert may do all.
+
+Usage:
+...............
+        skill-level <level>
+        
+        level :: operator | administrator | expert
+...............
+
+.Note on security
+****************************
+The `skill-level` option is advisory only. There is nothing
+stopping any users change their skill level (see
+<<topics_Security,Access Control Lists (ACL)>> on how to enforce
+access control).
+****************************
+
+[[cmdhelp_options_user,set the cluster user]]
+==== `user`
+
+Sufficient privileges are necessary in order to manage a
+cluster: programs such as `crm_verify` or `crm_resource` and,
+ultimately, `cibadmin` have to be run either as `root` or as the
+CRM owner user (typically `hacluster`). You don't have to worry
+about that if you run `crm` as `root`. A more secure way is to
+run the program with your usual privileges, set this option to
+the appropriate user (such as `hacluster`), and setup the
+`sudoers` file.
+
+Usage:
+...............
+        user system-user
+...............
+Example:
+...............
+        user hacluster
+...............
+
+[[cmdhelp_options_editor,set preferred editor program]]
+==== `editor`
+
+The `edit` command invokes an editor. Use this to specify your
+preferred editor program. If not set, it will default to either
+the value of the `EDITOR` environment variable or to one of the
+standard UNIX editors (`vi`,`emacs`,`nano`).
+
+Usage:
+...............
+        editor program
+...............
+Example:
+...............
+        editor vim
+...............
+
+[[cmdhelp_options_pager,set preferred pager program]]
+==== `pager`
+
+The `view` command displays text through a pager. Use this to
+specify your preferred pager program. If not set, it will default
+to either the value of the `PAGER` environment variable or to one
+of the standard UNIX system pagers (`less`,`more`,`pg`).
+
+[[cmdhelp_options_sort-elements,sort CIB elements]]
+==== `sort-elements`
+
+`crm` by default sorts CIB elements. If you want them appear in
+the order they were created, set this option to `no`.
+
+Usage:
+...............
+        sort-elements {yes|no}
+...............
+Example:
+...............
+        sort-elements no
+...............
+
+[[cmdhelp_options_wait,synchronous operation]]
+==== `wait`
+
+In normal operation, `crm` runs a command and gets back
+immediately to process other commands or get input from the user.
+With this option set to `yes` it will wait for the started
+transition to finish. In interactive mode dots are printed to
+indicate progress.
+
+Usage:
+...............
+        wait {yes|no}
+...............
+Example:
+...............
+        wait yes
+...............
+
+[[cmdhelp_options_output,set output type]]
+==== `output`
+
+`crm` can adorn configurations in two ways: in color (similar to
+for instance the `ls --color` command) and by showing keywords in
+upper case. Possible values are `plain`, `color`, and
+'uppercase'. It is possible to combine the latter two in order to
+get an upper case xmass tree. Just set this option to
+`color,uppercase`.
+
+[[cmdhelp_options_colorscheme,set colors for output]]
+==== `colorscheme`
+
+With `output` set to `color`, a comma separated list of colors
+from this option are used to emphasize:
+
+- keywords
+- object ids
+- attribute names
+- attribute values
+- scores
+- resource references
+
+`crm` can show colors only if there is curses support for python
+installed (usually provided by the `python-curses` package). The
+colors are whatever is available in your terminal. Use `normal`
+if you want to keep the default foreground color.
+
+This user preference defaults to
+`yellow,normal,cyan,red,green,magenta` which is good for
+terminals with dark background. You may want to change the color
+scheme and save it in the preferences file for other color
+setups.
+
+Example:
+...............
+    colorscheme yellow,normal,blue,red,green,magenta
+...............
+
+[[cmdhelp_options_check-frequency,when to perform semantic check]]
+==== `check-frequency`
+
+Semantic check of the CIB or elements modified or created may be
+done on every configuration change (`always`), when verifying
+(`on-verify`) or `never`. It is by default set to `always`.
+Experts may want to change the setting to `on-verify`.
+
+The checks require that resource agents are present. If they are
+not installed at the configuration time set this preference to
+`never`.
+
+See <<topics_Checks,Configuration semantic checks>> for more details.
+
+[[cmdhelp_options_check-mode,how to treat semantic errors]]
+==== `check-mode`
+
+Semantic check of the CIB or elements modified or created may be
+done in the `strict` mode or in the `relaxed` mode. In the former
+certain problems are treated as configuration errors. In the
+`relaxed` mode all are treated as warnings. The default is `strict`.
+
+See <<topics_Checks,Configuration semantic checks>> for more details.
+
+[[cmdhelp_options_add-quotes,add quotes around parameters containing spaces]]
+==== `add-quotes`
+
+The shell (as in `/bin/sh`) parser strips quotes from the command
+line. This may sometimes make it really difficult to type values
+which contain white space. One typical example is the configure
+filter command. The crm shell will supply extra quotes around
+arguments which contain white space. The default is `yes`.
+
+.Note on quotes use
+****************************
+Adding quotes around arguments automatically has been introduced
+with version 1.2.2 and it is technically a regression. Being a
+regression is the only reason the `add-quotes` option exists. If
+you have custom shell scripts which would break, just set the
+`add-quotes` option to `no`.
+
+For instance, with adding quotes enabled, it is possible to do
+the following:
+...............
+    # crm configure primitive d1 ocf:heartbeat:Dummy meta description="some description here"
+    # crm configure filter 'sed "s/hostlist=./&node-c /"' fencing
+...............
+****************************
+
+[[cmdhelp_options_manage-children,how to handle children resource attributes]]
+==== `manage-children`
+
+Some resource management commands, such as `resource stop`, when
+the target resource is a group, may not always produce desired
+result. Each element, group and the primitive members, can have a
+meta attribute and those attributes may end up with conflicting
+values. Consider the following construct:
+...............
+    crm(live)# configure show svc fs virtual-ip
+    primitive fs ocf:heartbeat:Filesystem \
+            params device="/dev/drbd0" directory="/srv/nfs" fstype="ext3" \
+            op monitor interval="10s" \
+            meta target-role="Started"
+    primitive virtual-ip ocf:heartbeat:IPaddr2 \
+            params ip="10.2.13.110" iflabel="1" \
+            op monitor interval="10s" \
+            op start interval="0" \
+            meta target-role="Started"
+    group svc fs virtual-ip \
+            meta target-role="Stopped"
+...............
+
+Even though the element `svc` should be stopped, the group is
+actually running because all its members have the `target-role`
+set to `Started`:
+...............
+    crm(live)# resource show svc
+    resource svc is running on: xen-f 
+...............
+
+Hence, if the user invokes `resource stop svc` the intention is
+not clear. This preference gives the user an opportunity to
+better control what happens if attributes of group members have
+values which are in conflict with the same attribute of the group
+itself.
+
+Possible values are `ask` (the default), `always`, and `never`.
+If set to `always`, the crm shell removes all children attributes
+which have values different from the parent. If set to `never`,
+all children attributes are left intact. Finally, if set to
+`ask`, the user will be asked for each member what is to be done.
+
+[[cmdhelp_options_show,show current user preference]]
+==== `show`
+
+Display all current settings.
+
+[[cmdhelp_options_save,save the user preferences to the rc file]]
+==== `save`
+
+Save current settings to the rc file (`$HOME/.config/crm/rc`). On
+further `crm` runs, the rc file is automatically read and parsed.
+
+[[cmdhelp_options_reset,reset user preferences to factory defaults]]
+==== `reset`
+
+This command resets all user options to the defaults. If used as
+a single-shot command, the rc file (`$HOME/.config/crm/rc`) is
+reset to the defaults too.
+
+[[cmdhelp_configure,CIB configuration]]
+=== `configure`
+
+This level enables all CIB object definition commands.
+
+The configuration may be logically divided into four parts:
+nodes, resources, constraints, and (cluster) properties and
+attributes.  Each of these commands support one or more basic CIB
+objects.
+
+Nodes and attributes describing nodes are managed using the
+`node` command.
+
+Commands for resources are:
+
+- `primitive`
+- `monitor`
+- `group`
+- `clone`
+- `ms`/`master` (master-slave)
+
+In order to streamline large configurations, it is possible to
+define a template which can later be referenced in primitives:
+
+- `rsc_template`
+
+In that case the primitive inherits all attributes defined in the
+template.
+
+There are three types of constraints:
+
+- `location`
+- `colocation`
+- `order`
+
+It is possible to define fencing order (stonith resource
+priorities):
+
+- `fencing_topology`
+
+Finally, there are the cluster properties, resource meta
+attributes defaults, and operations defaults. All are just a set
+of attributes. These attributes are managed by the following
+commands:
+
+- `property`
+- `rsc_defaults`
+- `op_defaults`
+
+In addition to the cluster configuration, the Access Control
+Lists (ACL) can be setup to allow access to parts of the CIB for
+users other than `root` and `hacluster`. The following commands
+manage ACL:
+
+- `user`
+- `role`
+
+The changes are applied to the current CIB only on ending the
+configuration session or using the `commit` command.
+
+Comments start with `#` in the first line. The comments are tied
+to the element which follows. If the element moves, its comments
+will follow.
+
+[[cmdhelp_configure_node,define a cluster node]]
+==== `node`
+
+The node command describes a cluster node. Nodes in the CIB are
+commonly created automatically by the CRM. Hence, you should not
+need to deal with nodes unless you also want to define node
+attributes. Note that it is also possible to manage node
+attributes at the `node` level.
+
+Usage:
+...............
+        node <uname>[:<type>]
+          [attributes <param>=<value> [<param>=<value>...]]
+          [utilization <param>=<value> [<param>=<value>...]]
+
+        type :: normal | member | ping
+...............
+Example:
+...............
+        node node1
+        node big_node attributes memory=64
+...............
+
+[[cmdhelp_configure_primitive,define a resource]]
+==== `primitive`
+
+The primitive command describes a resource. It may be referenced
+only once in group, clone, or master-slave objects. If it's not
+referenced, then it is placed as a single resource in the CIB.
+
+Operations may be specified in three ways. "Anonymous" as a
+simple list of "op" specifications. Use that if you don't want to
+reference the set of operations elsewhere. That's by far the most
+common way to define operations. If reusing operation sets is
+desired, use the "operations" keyword along with the id to give
+the operations set a name and the id-ref to reference another set
+of operations.
+
+Operation's attributes which are not recognized are saved as
+instance attributes of that operation. A typical example is
+`OCF_CHECK_LEVEL`.
+
+For multistate resources, roles are specified as `role=<role>`.
+
+A template may be defined for resources which are of the same
+type and which share most of the configuration. See 
+<<cmdhelp_configure_rsc_template,`rsc_template`>> for more information.
+
+Usage:
+...............
+        primitive <rsc> {[<class>:[<provider>:]]<type>|@<template>}
+          [params attr_list]
+          [meta attr_list]
+          [utilization attr_list]
+          [operations id_spec]
+            [op op_type [<attribute>=<value>...] ...]
+
+        attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+        id_spec :: $id=<id> | $id-ref=<id>
+        op_type :: start | stop | monitor
+...............
+Example:
+...............
+        primitive apcfence stonith:apcsmart \
+          params ttydev=/dev/ttyS0 hostlist="node1 node2" \
+          op start timeout=60s \
+          op monitor interval=30m timeout=60s
+
+        primitive www8 apache \
+          params configfile=/etc/apache/www8.conf \
+          operations $id-ref=apache_ops
+
+        primitive db0 mysql \
+          params config=/etc/mysql/db0.conf \
+          op monitor interval=60s \
+          op monitor interval=300s OCF_CHECK_LEVEL=10
+
+        primitive r0 ocf:linbit:drbd \
+          params drbd_resource=r0 \
+          op monitor role=Master interval=60s \
+          op monitor role=Slave interval=300s
+
+        primitive xen0 @vm_scheme1 \
+          params xmfile=/etc/xen/vm/xen0
+...............
+
+[[cmdhelp_configure_monitor,add monitor operation to a primitive]]
+==== `monitor`
+
+Monitor is by far the most common operation. It is possible to
+add it without editing the whole resource. Also, long primitive
+definitions may be a bit uncluttered. In order to make this
+command as concise as possible, less common operation attributes
+are not available. If you need them, then use the `op` part of
+the `primitive` command.
+
+Usage:
+...............
+        monitor <rsc>[:<role>] <interval>[:<timeout>]
+...............
+Example:
+...............
+        monitor apcfence 60m:60s
+...............
+
+Note that after executing the command, the monitor operation may
+be shown as part of the primitive definition.
+
+[[cmdhelp_configure_group,define a group]]
+==== `group`
+
+The `group` command creates a group of resources. This can be useful
+when resources depend on other resources and require that those
+resources start in order on the same node. A commmon use of resource
+groups is to ensure that a server and a virtual IP are located
+together, and that the virtual IP is started before the server.
+
+Grouped resources are started in the order they appear in the group,
+and stopped in the reverse order. If a resource in the group cannot
+run anywhere, resources following it in the group will not start.
+
+`group` can be passed the "container" meta attribute, to indicate that
+it is to be used to group VM resources monitored using Nagios. The
+resource referred to by the container attribute must be of type
+`ocf:heartbeat:Xen`, `oxf:heartbeat:VirtualDomain` or `ocf:heartbeat:lxc`.
+
+Usage:
+...............
+        group <name> <rsc> [<rsc>...]
+          [meta attr_list]
+          [params attr_list]
+
+        attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+        group internal_www disk0 fs0 internal_ip apache \
+          meta target_role=stopped
+
+        group vm-and-services vm vm-sshd meta container="vm"
+...............
+
+[[cmdhelp_configure_clone,define a clone]]
+==== `clone`
+
+The `clone` command creates a resource clone. It may contain a
+single primitive resource or one group of resources.
+
+Usage:
+...............
+        clone <name> <rsc>
+          [meta attr_list]
+          [params attr_list]
+
+        attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+        clone cl_fence apc_1 \
+          meta clone-node-max=1 globally-unique=false
+...............
+
+[[cmdhelp_configure_ms,define a master-slave resource]]
+==== `ms` (`master`)
+
+The `ms` command creates a master/slave resource type. It may contain a
+single primitive resource or one group of resources.
+
+Usage:
+...............
+        ms <name> <rsc>
+          [meta attr_list]
+          [params attr_list]
+
+        attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+        ms disk1 drbd1 \
+          meta notify=true globally-unique=false
+...............
+
+.Note on `id-ref` usage
+****************************
+Instance or meta attributes (`params` and `meta`) may contain
+a reference to another set of attributes. In that case, no other
+attributes are allowed. Since attribute sets' ids, though they do
+exist, are not shown in the `crm`, it is also possible to
+reference an object instead of an attribute set. `crm` will
+automatically replace such a reference with the right id:
+
+...............
+    crm(live)configure# primitive a2 www-2 meta $id-ref=a1
+    crm(live)configure# show a2
+    primitive a2 ocf:heartbeat:apache \
+        meta $id-ref="a1-meta_attributes"
+        [...]
+...............
+It is advisable to give meaningful names to attribute sets which
+are going to be referenced.
+****************************
+
+[[cmdhelp_configure_rsc_template,define a resource template]]
+==== `rsc_template`
+
+The `rsc_template` command creates a resource template. It may be
+referenced in primitives. It is used to reduce large
+configurations with many similar resources.
+
+Usage:
+...............
+        rsc_template <name> [<class>:[<provider>:]]<type>
+          [params attr_list]
+          [meta attr_list]
+          [utilization attr_list]
+          [operations id_spec]
+            [op op_type [<attribute>=<value>...] ...]
+
+        attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+        id_spec :: $id=<id> | $id-ref=<id>
+        op_type :: start | stop | monitor
+...............
+Example:
+...............
+        rsc_template public_vm ocf:heartbeat:Xen \
+          op start timeout=300s \
+          op stop timeout=300s \
+          op monitor interval=30s timeout=60s \
+          op migrate_from timeout=600s \
+          op migrate_to timeout=600s
+        primitive xen0 @public_vm \
+          params xmfile=/etc/xen/xen0
+        primitive xen1 @public_vm \
+          params xmfile=/etc/xen/xen1
+...............
+
+[[cmdhelp_configure_location,a location preference]]
+==== `location`
+
+`location` defines the preference of nodes for the given
+resource. The location constraints consist of one or more rules
+which specify a score to be awarded if the rule matches.
+
+Usage:
+...............
+        location <id> <rsc> {node_pref|rules}
+
+        node_pref :: <score>: <node>
+
+        rules ::
+          rule [id_spec] [$role=<role>] <score>: <expression>
+          [rule [id_spec] [$role=<role>] <score>: <expression> ...]
+
+        id_spec :: $id=<id> | $id-ref=<id>
+        score :: <number> | <attribute> | [-]inf
+        expression :: <simple_exp> [bool_op <simple_exp> ...]
+        bool_op :: or | and
+        simple_exp :: <attribute> [type:]<binary_op> <value>
+                      | <unary_op> <attribute>
+                      | date <date_expr>
+        type :: string | version | number
+        binary_op :: lt | gt | lte | gte | eq | ne
+        unary_op :: defined | not_defined
+
+        date_expr :: lt <end>
+                     | gt <start>
+                     | in_range start=<start> end=<end>
+                     | in_range start=<start> <duration>
+                     | date_spec <date_spec>
+        duration|date_spec ::
+                     hours=<value>
+                     | monthdays=<value>
+                     | weekdays=<value>
+                     | yearsdays=<value>
+                     | months=<value>
+                     | weeks=<value>
+                     | years=<value>
+                     | weekyears=<value>
+                     | moon=<value>
+...............
+Examples:
+...............
+        location conn_1 internal_www 100: node1
+
+        location conn_1 internal_www \
+          rule 50: #uname eq node1 \
+          rule pingd: defined pingd
+
+        location conn_2 dummy_float \
+          rule -inf: not_defined pingd or pingd number:lte 0
+...............
+
+[[cmdhelp_configure_colocation,colocate resources]]
+==== `colocation` (`collocation`)
+
+This constraint expresses the placement relation between two
+or more resources. If there are more than two resources, then the
+constraint is called a resource set.
+
+The score is used to indicate the priority of the constraint. A
+positive score indicates that the resources should run on the same
+node. A negative score that they should not run on the same
+node. Values of positive or negative `infinity` indicate a mandatory
+constraint.
+
+In the two resource form, the cluster will place `<with-rsc>` first,
+and then decide where to put the `<rsc>` resource.
+
+Collocation resource sets have an extra attribute (`sequential`)
+to allow for sets of resources which don't depend on each other
+in terms of state. The shell syntax for such sets is to put
+resources in parentheses.
+
+Sets cannot be nested.
+
+The optional `node-attribute` references an attribute in nodes'
+instance attributes.
+
+Usage:
+...............
+        colocation <id> <score>: <rsc>[:<role>] <with-rsc>[:<role>]
+          [node-attribute=<node_attr>]
+
+        colocation <id> <score>: <rsc>[:<role>] <rsc>[:<role>] ...
+          [node-attribute=<node_attr>]
+...............
+Example:
+...............
+        colocation never_put_apache_with_dummy -inf: apache dummy
+        colocation c1 inf: A ( B C )
+...............
+
+[[cmdhelp_configure_order,order resources]]
+==== `order`
+
+This constraint expresses the order of actions on two resources
+or more resources. If there are more than two resources, then the
+constraint is called a resource set.
+
+Ordered resource sets have an extra attribute to allow for sets
+of resources whose actions may run in parallel. The shell syntax
+for such sets is to put resources in parentheses.
+
+If the subsequent resource can start or promote after any one of the
+resources in a set has done, enclose the set in brackets (`[` and `]`).
+
+Sets cannot be nested.
+
+Three strings are reserved to specify a kind of order constraint:
+`Mandatory`, `Optional`, and `Serialize`. It is preferred to use
+one of these settings instead of score. Previous versions mapped
+scores `0` and `inf` to keywords `advisory` and `mandatory`.
+That is still valid but deprecated.
+
+.Note on resource sets' XML attributes
+****************************
+The XML attribute `require-all` controls whether all resources in
+a set are, well, required. The bracketed sets actually have this
+attribute as well as `sequential` set to `false`. If you need a
+different combination, for whatever reason, just set one of the
+attributes within the set. Something like this:
+
+...............
+    crm(live)configure# order o1 Mandatory: [ A B sequential=true ] C
+...............
+It is up to you to find out whether such a combination makes
+sense.
+****************************
+
+Usage:
+...............
+        order <id> {kind|<score>}: <rsc>[:<action>] <rsc>[:<action>] ...
+          [symmetrical=<bool>]
+
+        kind :: Mandatory | Optional | Serialize
+...............
+Example:
+...............
+        order c_apache_1 Mandatory: apache:start ip_1
+        order o1 Serialize: A ( B C )
+        order order_2 Mandatory: [ A B ] C
+...............
+
+[[cmdhelp_configure_rsc_ticket,resources ticket dependency]]
+==== `rsc_ticket`
+
+This constraint expresses dependency of resources on cluster-wide
+attributes, also known as tickets. Tickets are mainly used in
+geo-clusters, which consist of multiple sites. A ticket may be
+granted to a site, thus allowing resources to run there.
+
+The `loss-policy` attribute specifies what happens to the
+resource (or resources) if the ticket is revoked. The default is
+either `stop` or `demote` depending on whether a resource is
+multi-state.
+
+See also the <<cmdhelp_site_ticket,`site`>> set of commands.
+
+Usage:
+...............
+        rsc_ticket <id> <ticket_id>: <rsc>[:<role>] [<rsc>[:<role>] ...]
+          [loss-policy=<loss_policy_action>]
+
+        loss_policy_action :: stop | demote | fence | freeze
+...............
+Example:
+...............
+        rsc_ticket ticket-A_public-ip ticket-A: public-ip
+        rsc_ticket ticket-A_bigdb ticket-A: bigdb loss-policy=fence
+        rsc_ticket ticket-B_storage ticket-B: drbd-a:Master drbd-b:Master
+...............
+
+
+[[cmdhelp_configure_property,set a cluster property]]
+==== `property`
+
+Set the cluster (`crm_config`) options.
+
+Usage:
+...............
+        property [$id=<set_id>] <option>=<value> [<option>=<value> ...]
+...............
+Example:
+...............
+        property stonith-enabled=true
+...............
+
+[[cmdhelp_configure_rsc_defaults,set resource defaults]]
+==== `rsc_defaults`
+
+Set defaults for the resource meta attributes.
+
+Usage:
+...............
+        rsc_defaults [$id=<set_id>] <option>=<value> [<option>=<value> ...]
+...............
+Example:
+...............
+        rsc_defaults failure-timeout=3m
+...............
+
+[[cmdhelp_configure_fencing_topology,node fencing order]]
+==== `fencing_topology`
+
+If multiple fencing (stonith) devices are available capable of
+fencing a node, their order may be specified by `fencing_topology`.
+The order is specified per node.
+
+Stonith resources can be separated by `,` in which case all of
+them need to succeed. If they fail, the next stonith resource (or
+set of resources) is used. In other words, use comma to separate
+resources which all need to succeed and whitespace for serial
+order. It is not allowed to use whitespace around comma.
+
+If the node is left out, the order is used for all nodes.
+That should reduce the configuration size in some stonith setups.
+
+Usage:
+...............
+        fencing_topology stonith_resources [stonith_resources ...]
+        fencing_topology fencing_order [fencing_order ...]
+
+        fencing_order :: <node>: stonith_resources [stonith_resources ...]
+
+        stonith_resources :: <rsc>[,<rsc>...]
+...............
+Example:
+...............
+        fencing_topology poison-pill power
+        fencing_topology \
+            node-a: poison-pill power
+            node-b: ipmi serial
+...............
+
+[[cmdhelp_configure_role,define role access rights]]
+==== `role`
+
+An ACL role is a set of rules which describe access rights to
+CIB. Rules consist of an access right `read`, `write`, or `deny`
+and a specification denoting part of the configuration to which
+the access right applies. The specification can be an XPath or a
+combination of tag and id references. If an attribute is
+appended, then the specification applies only to that attribute
+of the matching element.
+
+There is a number of shortcuts for XPath specifications. The
+`meta,` `params`, and `utilization` shortcuts reference resource
+meta attributes, parameters, and utilization respectively. The
+`location` may be used to specify location constraints most of
+the time to allow resource `move` and `unmove` commands. The
+`property` references cluster properties. The `node` allows
+reading node attributes. `nodeattr` and `nodeutil` reference node
+attributes and node capacity (utilization). The `status` shortcut
+references the whole status section of the CIB. Read access to
+status is necessary for various monitoring tools such as
+`crm_mon(8)` (aka `crm status`).
+
+Usage:
+...............
+        role <role-id> rule [rule ...]
+
+        rule :: acl-right cib-spec [attribute:<attribute>]
+
+        acl-right :: read | write | deny
+
+        cib-spec :: xpath-spec | tag-ref-spec
+        xpath-spec :: xpath:<xpath> | shortcut
+        tag-ref-spec :: tag:<tag> | ref:<id> | tag:<tag> ref:<id>
+
+        shortcut :: meta:<rsc>[:<attr>]
+                    params:<rsc>[:<attr>]
+                    utilization:<rsc>
+                    location:<rsc>
+                    property[:<attr>]
+                    node[:<node>]
+                    nodeattr[:<attr>]
+                    nodeutil[:<node>]
+                    status
+...............
+Example:
+...............
+        role app1_admin \
+            write meta:app1:target-role \
+            write meta:app1:is-managed \
+            write location:app1 \
+            read ref:app1
+...............
+
+[[cmdhelp_configure_user,define user access rights]]
+==== `user`
+
+Users which normally cannot view or manage cluster configuration
+can be allowed access to parts of the CIB. The access is defined
+by a set of `read`, `write`, and `deny` rules as in role
+definitions or by referencing roles. The latter is considered
+best practice.
+
+Usage:
+...............
+        user <uid> {roles|rules}
+
+        roles :: role:<role-ref> [role:<role-ref> ...]
+        rules :: rule [rule ...]
+...............
+Example:
+...............
+        user joe \
+            role:app1_admin \
+            role:read_all
+...............
+
+[[cmdhelp_configure_op_defaults,set resource operations defaults]]
+==== `op_defaults`
+
+Set defaults for the operations meta attributes.
+
+Usage:
+...............
+        op_defaults [$id=<set_id>] <option>=<value> [<option>=<value> ...]
+...............
+Example:
+...............
+        op_defaults record-pending=true
+...............
+
+[[cmdhelp_configure_schema,set or display current CIB RNG schema]]
+==== `schema`
+
+CIB's content is validated by a RNG schema. Pacemaker supports
+several, depending on version. Currently supported schemas are
+`pacemaker-1.0`, `pacemaker-1.1`, and `pacemaker-1.2`.
+
+Use this command to display or switch to another RNG schema.
+
+Usage:
+...............
+        schema [<schema>]
+...............
+Example:
+...............
+        schema pacemaker-1.1
+...............
+
+[[cmdhelp_configure_show,display CIB objects]]
+==== `show`
+
+The `show` command displays objects. It may display all objects
+or a set of objects. The user may also choose to see only objects
+which were changed.
+Optionally, the XML code may be displayed instead of the CLI
+representation.
+
+Usage:
+...............
+        show [xml] [<id> ...]
+        show [xml] changed
+...............
+
+[[cmdhelp_configure_edit,edit CIB objects]]
+==== `edit`
+
+This command invokes the editor with the object description. As
+with the `show` command, the user may choose to edit all objects
+or a set of objects.
+
+If the user insists, he or she may edit the XML edition of the
+object. If you do that, don't modify any id attributes.
+
+Usage:
+...............
+        edit [xml] [<id> ...]
+        edit [xml] changed
+...............
+
+.Note on renaming element ids
+****************************
+The edit command sometimes cannot properly handle modifying
+element ids. In particular for elements which belong to group or
+ms resources. Group and ms resources themselves also cannot be
+renamed. Please use the `rename` command instead.
+****************************
+
+[[cmdhelp_configure_filter,filter CIB objects]]
+==== `filter`
+
+This command filters the given CIB elements through an external
+program. The program should accept input on `stdin` and send
+output to `stdout` (the standard UNIX filter conventions). As
+with the `show` command, the user may choose to filter all or
+just a subset of elements.
+
+It is possible to filter the XML representation of objects, but
+probably not as useful as the configuration language. The
+presentation is somewhat different from what would be displayed
+by the `show` command---each element is shown on a single line,
+i.e. there are no backslashes and no other embelishments.
+
+Don't forget to put quotes around the filter if it contains
+spaces.
+
+Usage:
+...............
+        filter <prog> [xml] [<id> ...]
+        filter <prog> [xml] changed
+...............
+Examples:
+...............
+        filter "sed '/^primitive/s/target-role=[^ ]*//'"
+        # crm configure filter "sed '/^primitive/s/target-role=[^ ]*//'"
+...............
+
+[[cmdhelp_configure_delete,delete CIB objects]]
+==== `delete`
+
+Delete one or more objects. If an object to be deleted belongs to
+a container object, such as a group, and it is the only resource
+in that container, then the container is deleted as well. Any
+related constraints are removed as well.
+
+Usage:
+...............
+        delete <id> [<id>...]
+...............
+
+[[cmdhelp_configure_default-timeouts,set timeouts for operations to minimums from the meta-data]]
+==== `default-timeouts`
+
+This command takes the timeouts from the actions section of the
+resource agent meta-data and sets them for the operations of the
+primitive.
+
+Usage:
+...............
+        default-timeouts <id> [<id>...]
+...............
+
+.Note on `default-timeouts`
+****************************
+You may be happy using this, but your applications may not. And
+it will tell you so at the worst possible moment. You have been
+warned.
+****************************
+
+[[cmdhelp_configure_rename,rename a CIB object]]
+==== `rename`
+
+Rename an object. It is recommended to use this command to rename
+a resource, because it will take care of updating all related
+constraints and a parent resource. Changing ids with the edit
+command won't have the same effect.
+
+If you want to rename a resource, it must be in the stopped state.
+
+Usage:
+...............
+        rename <old_id> <new_id>
+...............
+
+[[cmdhelp_configure_modgroup,modify group]]
+==== `modgroup`
+
+Add or remove primitives in a group. The `add` subcommand appends
+the new group member by default. Should it go elsewhere, there
+are `after` and `before` clauses.
+
+Usage:
+...............
+        modgroup <id> add <id> [after <id>|before <id>]
+        modgroup <id> remove <id>
+...............
+Examples:
+...............
+        modgroup share1 add storage2 before share1-fs
+...............
+
+[[cmdhelp_configure_refresh,refresh from CIB]]
+==== `refresh`
+
+Refresh the internal structures from the CIB. All changes made
+during this session are lost.
+
+Usage:
+...............
+        refresh
+...............
+
+[[cmdhelp_configure_erase,erase the CIB]]
+==== `erase`
+
+The `erase` clears all configuration. Apart from nodes. To remove
+nodes, you have to specify an additional keyword `nodes`.
+
+Note that removing nodes from the live cluster may have some
+strange/interesting/unwelcome effects.
+
+Usage:
+...............
+        erase [nodes]
+...............
+
+[[cmdhelp_configure_ptest,show cluster actions if changes were committed]]
+==== `ptest` (`simulate`)
+
+Show PE (Policy Engine) motions using `ptest(8)` or
+`crm_simulate(8)`.
+
+A CIB is constructed using the current user edited configuration
+and the status from the running CIB. The resulting CIB is run
+through `ptest` (or `crm_simulate`) to show changes which would
+happen if the configuration is committed.
+
+The status section may be loaded from another source and modified
+using the <<cmdhelp_cibstatus,`cibstatus`>> level commands. In that case, the
+`ptest` command will issue a message informing the user that the
+Policy Engine graph is not calculated based on the current status
+section and therefore won't show what would happen to the
+running but some imaginary cluster.
+
+If you have graphviz installed and X11 session, `dotty(1)` is run
+to display the changes graphically.
+
+Add a string of `v` characters to increase verbosity. `ptest`
+can also show allocation scores. `utilization` turns on
+information about the remaining capacity of nodes. With the
+`actions` option, `ptest` will print all resource actions.
+
+The `ptest` program has been replaced by `crm_simulate` in newer
+Pacemaker versions. In some installations both could be
+installed. Use `simulate` to enfore using `crm_simulate`.
+
+Usage:
+...............
+        ptest [nograph] [v...] [scores] [actions] [utilization]
+...............
+Examples:
+...............
+        ptest scores
+        ptest vvvvv
+        simulate actions
+...............
+
+[[cmdhelp_configure_rsctest,test resources as currently configured]]
+==== `rsctest`
+
+Test resources with current resource configuration. If no nodes
+are specified, tests are run on all known nodes.
+
+The order of resources is significant: it is assumed that later
+resources depend on earlier ones.
+
+If a resource is multi-state, it is assumed that the role on
+which later resources depend is master.
+
+Tests are run sequentially to prevent running the same resource
+on two or more nodes. Tests are carried out only if none of the
+specified nodes currently run any of the specified resources.
+However, it won't verify whether resources run on the other
+nodes.
+
+Superuser privileges are obviously required: either run this as
+root or setup the `sudoers` file appropriately.
+
+Note that resource testing may take some time.
+
+Usage:
+...............
+        rsctest <rsc_id> [<rsc_id> ...] [<node_id> ...]
+...............
+Examples:
+...............
+        rsctest my_ip websvc
+        rsctest websvc nodeB
+...............
+
+[[cmdhelp_configure_cib,CIB shadow management]]
+=== `cib` (shadow CIBs)
+
+This level is for management of shadow CIBs. It is available at
+the `configure` level to enable saving intermediate changes to a
+shadow CIB instead of to the live cluster. This short excerpt
+shows how:
+...............
+    crm(live)configure# cib new test-2
+    INFO: test-2 shadow CIB created
+    crm(test-2)configure# commit
+...............
+Note how the current CIB in the prompt changed from `live` to
+`test-2` after issuing the `cib new` command. See also the
+<<cmdhelp_cib,CIB shadow management>> for more information.
+
+[[cmdhelp_configure_cibstatus,CIB status management and editing]]
+==== `cibstatus`
+
+Enter edit and manage the CIB status section level. See the
+<<cmdhelp_cibstatus,CIB status management section>>.
+
+[[cmdhelp_configure_template,edit and import a configuration from a template]]
+==== `template`
+
+The specified template is loaded into the editor. It's up to the
+user to make a good CRM configuration out of it. See also the
+<<cmdhelp_template,template section>>.
+
+Usage:
+...............
+        template [xml] url
+...............
+Example:
+...............
+        template two-apaches.txt
+...............
+
+[[cmdhelp_configure_commit,commit the changes to the CIB]]
+==== `commit`
+
+Commit the current configuration to the CIB in use. As noted
+elsewhere, commands in a configure session don't have immediate
+effect on the CIB. All changes are applied at one point in time,
+either using `commit` or when the user leaves the configure
+level. In case the CIB in use changed in the meantime, presumably
+by somebody else, the crm shell will refuse to apply the changes.
+If you know that it's fine to still apply them add `force`.
+
+Usage:
+...............
+        commit [force]
+...............
+
+[[cmdhelp_configure_verify,verify the CIB with crm_verify]]
+==== `verify`
+
+Verify the contents of the CIB which would be committed.
+
+Usage:
+...............
+        verify
+...............
+
+[[cmdhelp_configure_upgrade,upgrade the CIB to version 1.0]]
+==== `upgrade`
+
+If you get the `CIB not supported` error, which typically means
+that the current CIB version is coming from the older release,
+you may try to upgrade it to the latest revision. The command
+to perform the upgrade is:
+...............
+    # cibadmin --upgrade --force
+...............
+
+If we don't recognize the current CIB as the old one, but you're
+sure that it is, you may force the command.
+
+Usage:
+...............
+        upgrade [force]
+...............
+
+[[cmdhelp_configure_save,save the CIB to a file]]
+==== `save`
+
+Save the current configuration to a file. Optionally, as XML. Use
+`-` instead of file name to write the output to `stdout`.
+
+Usage:
+...............
+        save [xml] <file>
+...............
+Example:
+...............
+        save myfirstcib.txt
+...............
+
+[[cmdhelp_configure_load,import the CIB from a file]]
+==== `load`
+
+Load a part of configuration (or all of it) from a local file or
+a network URL. The `replace` method replaces the current
+configuration with the one from the source. The `update` tries to
+import the contents into the current configuration.
+The file may be a CLI file or an XML file.
+
+Usage:
+...............
+        load [xml] <method> URL
+
+        method :: replace | update
+...............
+Example:
+...............
+        load xml update myfirstcib.xml
+        load xml replace http://storage.big.com/cibs/bigcib.xml
+...............
+
+[[cmdhelp_configure_graph,generate a directed graph]]
+==== `graph`
+
+Create a graphviz graphical layout from the current cluster
+configuration.
+
+Currently, only `dot` (directed graph) is supported. It is
+essentially a visualization of resource ordering.
+
+The graph may be saved to a file which can be used as source for
+various graphviz tools (by default it is displayed in the user's
+X11 session). Optionally, by specifying the format, one can also
+produce an image instead.
+
+For more or different graphviz attributes, it is possible to save
+the default set of attributes to an ini file. If this file exists
+it will always override the builtin settings. The `exportsettings`
+subcommand also prints the location of the ini file.
+
+Usage:
+...............
+        graph [<gtype> [<file> [<img_format>]]]
+        graph exportsettings
+
+        gtype :: dot
+        img_format :: `dot` output format (see the `-T` option)
+...............
+Example:
+...............
+        graph dot
+        graph dot clu1.conf.dot
+        graph dot clu1.conf.svg svg
+...............
+
+[[cmdhelp_configure_xml,raw xml]]
+==== `xml`
+
+Even though we promissed no xml, it may happen, but hopefully
+very very seldom, that an element from the CIB cannot be rendered
+in the configuration language. In that case, the element will be
+shown as raw xml, prefixed by this command. That element can then
+be edited like any other. If the shell finds out that after the
+change it can digest it, then it is going to be converted into
+the normal configuration language. Otherwise, there is no need to
+use `xml` for configuration.
+
+Usage:
+...............
+        xml <xml>
+...............
+
+[[cmdhelp_template,edit and import a configuration from a template]]
+=== `template`
+
+User may be assisted in the cluster configuration by templates
+prepared in advance. Templates consist of a typical ready
+configuration which may be edited to suit particular user needs.
+
+This command enters a template level where additional commands
+for configuration/template management are available.
+
+[[cmdhelp_template_new,create a new configuration from templates]]
+==== `new`
+
+Create a new configuration from one or more templates. Note that
+configurations and templates are kept in different places, so it
+is possible to have a configuration name equal a template name.
+
+If you already know which parameters are required, you can set
+them directly on the command line.
+
+The parameter name `id` is set by default to the name of the
+configuration.
+
+Usage:
+...............
+        new <config> <template> [<template> ...] [params name=value ...]"
+...............
+Examples:
+...............
+        new vip virtual-ip
+        new bigfs ocfs2 params device=/dev/sdx8 directory=/bigfs
+...............
+
+[[cmdhelp_template_load,load a configuration]]
+==== `load`
+
+Load an existing configuration. Further `edit`, `show`, and
+`apply` commands will refer to this configuration.
+
+Usage:
+...............
+        load <config>
+...............
+
+[[cmdhelp_template_edit,edit a configuration]]
+==== `edit`
+
+Edit current or given configuration using your favourite editor.
+
+Usage:
+...............
+        edit [<config>]
+...............
+
+[[cmdhelp_template_delete,delete a configuration]]
+==== `delete`
+
+Remove a configuration. The loaded (active) configuration may be
+removed by force.
+
+Usage:
+...............
+        delete <config> [force]
+...............
+
+[[cmdhelp_template_list,list configurations/templates]]
+==== `list`
+
+List existing configurations or templates.
+
+Usage:
+...............
+        list [templates]
+...............
+
+[[cmdhelp_template_apply,process and apply the current configuration to the current CIB]]
+==== `apply`
+
+Copy the current or given configuration to the current CIB. By
+default, the CIB is replaced, unless the method is set to
+"update".
+
+Usage:
+...............
+        apply [<method>] [<config>]
+
+        method :: replace | update
+...............
+
+[[cmdhelp_template_show,show the processed configuration]]
+==== `show`
+
+Process the current or given configuration and display the result.
+
+Usage:
+...............
+        show [<config>]
+...............
+
+[[cmdhelp_cibstatus,CIB status management and editing]]
+=== `cibstatus`
+
+The `status` section of the CIB keeps the current status of nodes
+and resources. It is modified _only_ on events, i.e. when some
+resource operation is run or node status changes. For obvious
+reasons, the CRM has no user interface with which it is possible
+to affect the status section. From the user's point of view, the
+status section is essentially a read-only part of the CIB. The
+current status is never even written to disk, though it is
+available in the PE (Policy Engine) input files which represent
+the history of cluster motions. The current status may be read
+using the `cibadmin -Q` command.
+
+It may sometimes be of interest to see how status changes would
+affect the Policy Engine. The set of `cibstatus` level commands
+allow the user to load status sections from various sources and
+then insert or modify resource operations or change nodes' state.
+
+The effect of those changes may then be observed by running the
+<<cmdhelp_configure_ptest,`ptest`>> command at the `configure` level
+or `simulate` and `run` commands at this level. The `ptest`
+runs with the user edited CIB whereas the latter two commands
+run with the CIB which was loaded along with the status section.
+
+The `simulate` and `run` commands as well as all status
+modification commands are implemented using `crm_simulate(8)`.
+
+[[cmdhelp_cibstatus_load,load the CIB status section]]
+==== `load`
+
+Load a status section from a file, a shadow CIB, or the running
+cluster. By default, the current (`live`) status section is
+modified. Note that if the `live` status section is modified it
+is not going to be updated if the cluster status changes, because
+that would overwrite the user changes. To make `crm` drop changes
+and resume use of the running cluster status, run `load live`.
+
+All CIB shadow configurations contain the status section which is
+a snapshot of the status section taken at the time the shadow was
+created. Obviously, this status section doesn't have much to do
+with the running cluster status, unless the shadow CIB has just
+been created. Therefore, the `ptest` command by default uses the
+running cluster status section.
+
+Usage:
+...............
+        load {<file>|shadow:<cib>|live}
+...............
+Example:
+...............
+        load bug-12299.xml
+        load shadow:test1
+...............
+
+[[cmdhelp_cibstatus_save,save the CIB status section]]
+==== `save`
+
+The current internal status section with whatever modifications
+were performed can be saved to a file or shadow CIB.
+
+If the file exists and contains a complete CIB, only the status
+section is going to be replaced and the rest of the CIB will
+remain intact. Otherwise, the current user edited configuration
+is saved along with the status section.
+
+Note that all modifications are saved in the source file as soon
+as they are run.
+
+Usage:
+...............
+        save [<file>|shadow:<cib>]
+...............
+Example:
+...............
+        save bug-12299.xml
+...............
+
+[[cmdhelp_cibstatus_origin,display origin of the CIB status section]]
+==== `origin`
+
+Show the origin of the status section currently in use. This
+essentially shows the latest `load` argument.
+
+Usage:
+...............
+        origin
+...............
+
+[[cmdhelp_cibstatus_show,show CIB status section]]
+==== `show`
+
+Show the current status section in the XML format. Brace yourself
+for some unreadable output. Add `changed` option to get a human
+readable output of all changes.
+
+Usage:
+...............
+        show [changed]
+...............
+
+[[cmdhelp_cibstatus_node,change node status]]
+==== `node`
+
+Change the node status. It is possible to throw a node out of
+the cluster, make it a member, or set its state to unclean.
+
+`online`:: Set the `node_state` `crmd` attribute to `online`
+and the `expected` and `join` attributes to `member`. The effect
+is that the node becomes a cluster member.
+
+`offline`:: Set the `node_state` `crmd` attribute to `offline`
+and the `expected` attribute to empty. This makes the node
+cleanly removed from the cluster.
+
+`unclean`:: Set the `node_state` `crmd` attribute to `offline`
+and the `expected` attribute to `member`. In this case the node
+has unexpectedly disappeared.
+
+Usage:
+...............
+        node <node> {online|offline|unclean}
+...............
+Example:
+...............
+        node xen-b unclean
+...............
+
+[[cmdhelp_cibstatus_op,edit outcome of a resource operation]]
+==== `op`
+
+Edit the outcome of a resource operation. This way you can
+tell CRM that it ran an operation and that the resource agent
+returned certain exit code. It is also possible to change the
+operation's status. In case the operation status is set to
+something other than `done`, the exit code is effectively
+ignored.
+
+Usage:
+...............
+        op <operation> <resource> <exit_code> [<op_status>] [<node>]
+
+        operation :: probe | monitor[:<n>] | start | stop |
+           promote | demote | notify | migrate_to | migrate_from
+        exit_code :: <rc> | success | generic | args | 
+           unimplemented | perm | installed | configured | not_running | 
+           master | failed_master
+        op_status :: pending | done | cancelled | timeout | notsupported | error
+
+        n :: the monitor interval in seconds; if omitted, the first
+           recurring operation is referenced
+        rc :: numeric exit code in range 0..9
+...............
+Example:
+...............
+        op start d1 xen-b generic
+        op start d1 xen-b 1
+        op monitor d1 xen-b not_running
+        op stop d1 xen-b 0 timeout
+...............
+
+[[cmdhelp_cibstatus_quorum,set the quorum]]
+==== `quorum`
+
+Set the quorum value.
+
+Usage:
+...............
+        quorum <bool>
+...............
+Example:
+...............
+        quorum false
+...............
+
+[[cmdhelp_cibstatus_ticket,manage tickets]]
+==== `ticket`
+
+Modify the ticket status. Tickets can be granted and revoked.
+Granted tickets could be activated or put in standby.
+
+Usage:
+...............
+        ticket <ticket> {grant|revoke|activate|standby}
+...............
+Example:
+...............
+        ticket ticketA grant
+...............
+
+[[cmdhelp_cibstatus_run,run policy engine]]
+==== `run`
+
+Run the policy engine with the edited status section.
+
+Add a string of `v` characters to increase verbosity. Specify
+`scores` to see allocation scores also. `utilization` turns on
+information about the remaining capacity of nodes.
+
+If you have graphviz installed and X11 session, `dotty(1)` is run
+to display the changes graphically.
+
+Usage:
+...............
+        run [nograph] [v...] [scores] [utilization]
+...............
+Example:
+...............
+        run
+...............
+
+[[cmdhelp_cibstatus_simulate,simulate cluster transition]]
+==== `simulate`
+
+Run the policy engine with the edited status section and simulate
+the transition.
+
+Add a string of `v` characters to increase verbosity. Specify
+`scores` to see allocation scores also. `utilization` turns on
+information about the remaining capacity of nodes.
+
+If you have graphviz installed and X11 session, `dotty(1)` is run
+to display the changes graphically.
+
+Usage:
+...............
+        simulate [nograph] [v...] [scores] [utilization]
+...............
+Example:
+...............
+        simulate
+...............
+
+[[cmdhelp_history,cluster history]]
+=== `history`
+
+Examining Pacemaker's history is a particularly involved task.
+The number of subsystems to be considered, the complexity of the
+configuration, and the set of various information sources, most
+of which are not exactly human readable, keep analyzing resource
+or node problems accessible to only the most knowledgeable. Or,
+depending on the point of view, to the most persistent. The
+following set of commands has been devised in hope to make
+cluster history more accessible.
+
+Of course, looking at _all_ history could be time consuming
+regardless of how good tools at hand are. Therefore, one should
+first say which period he or she wants to analyze. If not
+otherwise specified, the last hour is considered. Logs and other
+relevant information is collected using `hb_report`. Since this
+process takes some time and we always need fresh logs,
+information is refreshed in a much faster way using `pssh(1)`. If
+`python-pssh` is not found on the system, examining live cluster
+is still possible though not as comfortable.
+
+Apart from examining live cluster, events may be retrieved from a
+report generated by `hb_report` (see also the `-H` option). In
+that case we assume that the period stretching the whole report
+needs to be investigated. Of course, it is still possible to
+further reduce the time range.
+
+If you think you may have found a bug or just need clarification
+from developers or your support, the `session pack` command can
+help create a report. This is an example:
+...............
+    crm(live)history# timeframe "Jul 18 12:00" "Jul 18 12:30"
+    crm(live)history# session save strange_restart
+    crm(live)history# session pack
+    Report saved in .../strange_restart.tar.bz2
+    crm(live)history# 
+...............
+In order to reduce report size and allow developers to
+concentrate on the issue, you should beforehand limit the time
+frame. Giving a meaningful session name helps too.
+
+==== `info`
+
+The `info` command shows most important information about the
+cluster.
+
+Usage:
+...............
+        info
+...............
+Example:
+...............
+        info
+...............
+
+[[cmdhelp_history_latest,show latest news from the cluster]]
+==== `latest`
+
+The `latest` command shows a bit of recent history, more
+precisely whatever happened since the last cluster change (the
+latest transition). If the transition is running, the shell will
+first wait until it finishes.
+
+Usage:
+...............
+        latest
+...............
+Example:
+...............
+        latest
+...............
+
+[[cmdhelp_history_limit,limit timeframe to be examined]]
+==== `limit` (`timeframe`)
+
+All history commands look at events within certain period. It
+defaults to the last hour for the live cluster source. There is
+no limit for the `hb_report` source. Use this command to set the
+timeframe.
+
+The time period is parsed by the dateutil python module. It
+covers wide range of date formats. For instance:
+
+- 3:00          (today at 3am)
+- 15:00         (today at 3pm)
+- 2010/9/1 2pm  (September 1st 2010 at 2pm)
+
+We won't bother to give definition of the time specification in
+usage below. Either use common sense or read the
+http://labix.org/python-dateutil[dateutil] documentation.
+
+If dateutil is not available, then the time is parsed using
+strptime and only the kind as printed by `date(1)` is allowed:
+
+- Tue Sep 15 20:46:27 CEST 2010
+
+Usage:
+...............
+        limit [<from_time> [<to_time>]]
+...............
+Examples:
+...............
+        limit 10:15
+        limit 15h22m 16h
+        limit "Sun 5 20:46" "Sun 5 22:00"
+...............
+
+[[cmdhelp_history_source,set source to be examined]]
+==== `source`
+
+Events to be examined can come from the current cluster or from a
+`hb_report` report. This command sets the source. `source live`
+sets source to the running cluster and system logs. If no source
+is specified, the current source information is printed.
+
+In case a report source is specified as a file reference, the file
+is going to be unpacked in place where it resides. This directory
+is not removed on exit.
+
+Usage:
+...............
+        source [<dir>|<file>|live]
+...............
+Examples:
+...............
+        source live
+        source /tmp/customer_case_22.tar.bz2
+        source /tmp/customer_case_22
+        source
+...............
+
+[[cmdhelp_history_refresh,refresh live report]]
+==== `refresh`
+
+This command makes sense only for the `live` source and makes
+`crm` collect the latest logs and other relevant information from
+the logs. If you want to make a completely new report, specify
+`force`.
+
+Usage:
+...............
+        refresh [force]
+...............
+
+[[cmdhelp_history_detail,set the level of detail shown]]
+==== `detail`
+
+How much detail to show from the logs.
+
+Usage:
+...............
+        detail <detail_level>
+
+        detail_level :: small integer (defaults to 0)
+...............
+Example:
+...............
+        detail 1
+...............
+
+[[cmdhelp_history_setnodes,set the list of cluster nodes]]
+==== `setnodes`
+
+In case the host this program runs on is not part of the cluster,
+it is necessary to set the list of nodes.
+
+Usage:
+...............
+        setnodes node <node> [<node> ...]
+...............
+Example:
+...............
+        setnodes node_a node_b
+...............
+
+[[cmdhelp_history_resource,resource events]]
+==== `resource`
+
+Show actions and any failures that happened on all specified
+resources on all nodes. Normally, one gives resource names as
+arguments, but it is also possible to use extended regular
+expressions. Note that neither groups nor clones or master/slave
+names are ever logged. The resource command is going to expand
+all of these appropriately, so that clone instances or resources
+which are part of a group are shown.
+
+Usage:
+...............
+        resource <rsc> [<rsc> ...]
+...............
+Example:
+...............
+        resource bigdb public_ip
+        resource my_.*_db2
+        resource ping_clone
+...............
+
+[[cmdhelp_history_node,node events]]
+==== `node`
+
+Show important events that happened on a node. Important events
+are node lost and join, standby and online, and fence. Use either
+node names or extended regular expressions.
+
+Usage:
+...............
+        node <node> [<node> ...]
+...............
+Example:
+...............
+        node node1
+...............
+
+[[cmdhelp_history_log,log content]]
+==== `log`
+
+Show messages logged on one or more nodes. Leaving out a node
+name produces combined logs of all nodes. Messages are sorted by
+time and, if the terminal emulations supports it, displayed in
+different colours depending on the node to allow for easier
+reading.
+
+The sorting key is the timestamp as written by syslog which
+normally has the maximum resolution of one second. Obviously,
+messages generated by events which share the same timestamp may
+not be sorted in the same way as they happened. Such close events
+may actually happen fairly often.
+
+Usage:
+...............
+        log [<node>]
+...............
+Example:
+...............
+        log node-a
+...............
+
+[[cmdhelp_history_exclude,exclude log messages]]
+==== `exclude`
+
+If a log is infested with irrelevant messages, those messages may
+be excluded by specifying a regular expression. The regular
+expressions used are Python extended. This command is additive.
+To drop all regular expressions, use `exclude clear`. Run
+`exclude` only to see the current list of regular expressions.
+Excludes are saved along with the history sessions.
+
+Usage:
+...............
+        exclude [<regex>|clear]
+...............
+Example:
+...............
+        exclude kernel.*ocfs2
+...............
+
+[[cmdhelp_history_peinputs,list or get PE input files]]
+==== `peinputs`
+
+Every event in the cluster results in generating one or more
+Policy Engine (PE) files. These files describe future motions of
+resources. The files are listed as full paths in the current
+report directory. Add `v` to also see the creation time stamps.
+
+Usage:
+...............
+        peinputs [{<range>|<number>} ...] [v]
+
+        range :: <n1>:<n2>
+...............
+Example:
+...............
+        peinputs
+        peinputs 440:444 446
+        peinputs v
+...............
+
+[[cmdhelp_history_transition,show transition]]
+==== `transition`
+
+This command will print actions planned by the PE and run
+graphviz (`dotty`) to display a graphical representation of the
+transition. Of course, for the latter an X11 session is required.
+This command invokes `ptest(8)` in background.
+
+The `showdot` subcommand runs graphviz (`dotty`) to display a
+graphical representation of the `.dot` file which has been
+included in the report. Essentially, it shows the calculation
+produced by `pengine` which is installed on the node where the
+report was produced. In optimal case this output should not
+differ from the one produced by the locally installed `pengine`.
+
+The `log` subcommand shows the full log for the duration of the
+transition.
+
+A transition can also be saved to a CIB shadow for further
+analysis or use with `cib` or `configure` commands (use the
+`save` subcommand). The shadow file name defaults to the name of
+the PE input file.
+
+If the PE input file number is not provided, it defaults to the
+last one, i.e. the last transition. The last transition can also
+be referenced with number 0. If the number is negative, then the
+corresponding transition relative to the last one is chosen.
+
+If there are warning and error PE input files or different nodes
+were the DC in the observed timeframe, it may happen that PE
+input file numbers collide. In that case provide some unique part
+of the path to the file.
+
+After the `ptest` output, logs about events that happened during
+the transition are printed.
+
+Usage:
+...............
+        transition [<number>|<index>|<file>] [nograph] [v...] [scores] [actions] [utilization]
+        transition showdot [<number>|<index>|<file>]
+        transition log [<number>|<index>|<file>]
+        transition save [<number>|<index>|<file> [name]]
+...............
+Examples:
+...............
+        transition
+        transition 444
+        transition -1
+        transition pe-error-3.bz2
+        transition node-a/pengine/pe-input-2.bz2
+        transition showdot 444
+        transition log
+        transition save 0 enigma-22
+...............
+
+[[cmdhelp_history_show,show status or configuration of the PE input file]]
+==== `show`
+
+Every transition is saved as a PE file. Use this command to
+render that PE file either as configuration or status. The
+configuration output is the same as `crm configure show`.
+
+Usage:
+...............
+        show <pe> [status]
+
+        pe :: <number>|<index>|<file>|live
+...............
+Examples:
+...............
+        show 2066
+        show pe-input-2080.bz2 status
+...............
+
+[[cmdhelp_history_graph,generate a directed graph from the PE file]]
+==== `graph`
+
+Create a graphviz graphical layout from the PE file (the
+transition). Every transition contains the cluster configuration
+which was active at the time. See also <<cmdhelp_configure_graph,generate a directed graph
+from configuration>>.
+
+Usage:
+...............
+        graph <pe> [<gtype> [<file> [<img_format>]]]
+
+        gtype :: dot
+        img_format :: `dot` output format (see the `-T` option)
+...............
+Example:
+...............
+        graph -1
+        graph 322 dot clu1.conf.dot
+        graph 322 dot clu1.conf.svg svg
+...............
+
+[[cmdhelp_history_diff,cluster states/transitions difference]]
+==== `diff`
+
+A transition represents a change in cluster configuration or
+state. Use `diff` to see what has changed between two
+transitions.
+
+If you want to specify the current cluster configuration and
+status, use the string `live`.
+
+Normally, the first transition specified should be the one which
+is older, but we are not going to enforce that.
+
+Note that a single configuration update may result in more than
+one transition.
+
+Usage:
+...............
+        diff <pe> <pe> [status] [html]
+
+        pe :: <number>|<index>|<file>|live
+...............
+Examples:
+...............
+        diff 2066 2067
+        diff pe-input-2080.bz2 live status
+...............
+
+[[cmdhelp_history_session,manage history sessions]]
+==== `session`
+
+Sometimes you may want to get back to examining a particular
+history period or bug report. In order to make that easier, the
+current settings can be saved and later retrieved.
+
+If the current history being examined is coming from a live
+cluster the logs, PE inputs, and other files are saved too,
+because they may disappear from nodes. For the existing reports
+coming from `hb_report`, only the directory location is saved
+(not to waste space).
+
+A history session may also be packed into a tarball which can
+then be sent to support.
+
+Leave out subcommand to see the current session.
+
+Usage:
+...............
+        session [{save|load|delete} <name> | pack [<name>] | update | list]
+...............
+Examples:
+...............
+        session save bnc966622
+        session load rsclost-2
+        session list
+...............
+
+=== `end` (`cd`, `up`)
+
+The `end` command ends the current level and the user moves to
+the parent level. This command is available everywhere.
+
+Usage:
+...............
+        end
+...............
+
+=== `help`
+
+The `help` command prints help for the current level or for the
+specified topic (command). This command is available everywhere.
+
+Usage:
+...............
+        help [<topic>]
+...............
+
+=== `quit` (`exit`, `bye`)
+
+Leave the program.
+
+BUGS
+----
+Even though all sensible configurations (and most of those that
+are not) are going to be supported by the crm shell, I suspect
+that it may still happen that certain XML constructs may confuse
+the tool. When that happens, please file a bug report.
+
+The crm shell will not try to update the objects it does not
+understand. Of course, it is always possible to edit such objects
+in the XML format.
+
+AUTHOR
+------
+Dejan Muhamedagic, <dejan at suse.de>
+and many OTHERS
+
+SEE ALSO
+--------
+crm_resource(8), crm_attribute(8), crm_mon(8), cib_shadow(8),
+ptest(8), dotty(1), crm_simulate(8), cibadmin(8)
+
+
+COPYING
+-------
+Copyright \(C) 2008-2011 Dejan Muhamedagic. Free use of this
+software is granted under the terms of the GNU General Public License (GPL).
+
+//////////////////////
+ vim:ts=4:sw=4:expandtab:
+//////////////////////
diff --git a/doc/website-v1/man-2.0.txt b/doc/website-v1/man-2.0.txt
new file mode 100644
index 0000000..a59e6d0
--- /dev/null
+++ b/doc/website-v1/man-2.0.txt
@@ -0,0 +1,4319 @@
+:man source:   crm
+:man version:  2.1.0
+:man manual:   crmsh documentation
+
+crm(8)
+======
+
+NAME
+----
+crm - Pacemaker command line interface for configuration and management
+
+
+SYNOPSIS
+--------
+*crm* [OPTIONS] [SUBCOMMAND ARGS...]
+
+
+[[topics_Description,Program description]]
+DESCRIPTION
+-----------
+The `crm` shell is a command-line based cluster configuration and
+management tool. Its goal is to assist as much as possible with the
+configuration and maintenance of Pacemaker-based High Availability
+clusters.
+
+`crm` works both as a command-line tool to be called directly from the
+system shell, and as an interactive shell with extensive tab
+completion and help.
+
+The primary focus of the `crm` shell is to provide a simplified and
+consistent interface to Pacemaker, but it also provides tools for
+managing the creation and configuration of High Availability clusters
+from scratch. To learn more about this aspect of `crm`, see the
+`cluster` section below.
+
+The Pacemaker configuration is stored in something called a CIB file,
+where CIB stands for Cluster Information Base. The CIB is a set of
+instructions coded in XML which is synchronized across the cluster.
+
+Editing the CIB is a challenge, not only due to its complexity and
+wide variety of options, but also because XML is more computer than
+user friendly. To help with this task, the `crm` shell provides a
+small and simple line-oriented configuration language consistent with
+the other commands available in the shell. For more information about
+this language and how to use it, see the `configure` section below.
+
+`crm` provides a consistent and well-documented interface to most of
+the management tools included in Pacemaker, for example
+`crm_resource(8)` or `crm_attribute(8)`. Instead of having to remember
+the various flags and options available for each tool, `crm` hides all
+of the arcane detail.
+
+`crm` can also function as a cluster scripting tool, and can be fed
+multi-line sets of commands either directly from standard input or via
+a file. Templates with ready made configurations may help newbies
+learn about the cluster configuration or facilitate testing
+procedures.
+
+The `crm` shell is line oriented: every command must start and finish
+on the same line. It is possible to use a continuation character (+\+)
+to write one command in two or more lines. The continuation character
+is commonly used when displaying configurations.
+
+[[topics_CommandLine,Command line options]]
+OPTIONS
+-------
+*-f, --file*='FILE'::
+    Load commands from the given file. If a dash +-+ is used in place
+    of a file name, `crm` will read commands from the shell standard
+    input (`stdin`).
+
+*-c, --cib*='CIB'::
+    Start the session using the given shadow CIB file.
+    Equivalent to +cib use <CIB>+.
+
+*-D, --display=*'OUTPUT_TYPE'::
+    Choose one of the output options: +plain+, +color+, or
+    +uppercase+. The default is +color+ if the terminal emulation
+    supports colors. Otherwise, +plain+ is used.
+
+*-F, --force*::
+    Make `crm` proceed with applying changes where it would normally
+    ask the user to confirm before proceeding. This option is mainly
+    useful in scripts, and should be used with care.
+
+*-w, --wait*::
+    Make `crm` wait for the cluster transition to finish (for the
+    changes to take effect) after each processed line.
+
+*-H, --history*='DIR|FILE'::
+    The `history` commands can either work directly on the live
+    cluster (default), or on a report generated by the `report`
+    command. Use this option to specify a directory or file containing
+    the previously generated report.
+
+*-h, --help*::
+    Print help page.
+
+*--version*::
+    Print crmsh version and build information (Mercurial Hg changeset
+    hash).
+
+*-d, --debug*::
+    Print verbose debugging information.
+
+*-R, --regression-tests*::
+    Enables extra verbose trace logging used by the regression
+    tests. Logs all external calls made by crmsh.
+
+*--scriptdir*='DIR'::
+    Extra directory where crm looks for cluster scripts. Can be a semi-colon
+    separated list of directories.
+
+[[topics_Introduction,Introduction]]
+== Introduction
+
+This section of the user guide covers general topics about the user
+interface and describes some of the features of `crmsh` in detail.
+
+[[topics_Introduction_Interface,User interface]]
+=== User interface
+
+The main purpose of `crmsh` is to provide a simple yet powerful
+interface to the cluster stack. There are two main modes of operation
+with the user interface of `crmsh`:
+
+* Command line (single-shot) use - Use `crm` as a regular UNIX command
+  from your usual shell. `crm` has full bash completion built in, so
+  using it in this manner should be as comfortable and familiar as
+  using any other command-line tool.
+
+* Interactive mode - By calling `crm` without arguments, or by calling
+  it with only a sublevel as argument, `crm` enters the interactive
+  mode. In this mode, it acts as its own command shell, which
+  remembers which sublevel you are currently in and allows for rapid
+  and convenient execution of multiple commands within the same
+  sublevel. This mode also has full tab completion, as well as
+  built-in interactive help and syntax highlighting.
+
+Here are a few examples of using `crm` both as a command-line tool and
+as an interactive shell:
+
+.Command line (one-shot) use:
+........
+# crm resource stop www_app
+........
+
+.Interactive use:
+........
+# crm
+crm(live)# resource
+crm(live)resource# unmanage tetris_1
+crm(live)resource# up
+crm(live)# node standby node4
+........
+
+.Cluster configuration:
+........
+# crm configure<<EOF
+  #
+  # resources
+  #
+  primitive disk0 iscsi \
+    params portal=192.168.2.108:3260 target=iqn.2008-07.com.suse:disk0
+  primitive fs0 Filesystem \
+    params device=/dev/disk/by-label/disk0 directory=/disk0 fstype=ext3
+  primitive internal_ip IPaddr params ip=192.168.1.101
+  primitive apache apache \
+    params configfile=/disk0/etc/apache2/site0.conf
+  primitive apcfence stonith:apcsmart \
+    params ttydev=/dev/ttyS0 hostlist="node1 node2" \
+    op start timeout=60s
+  primitive pingd pingd \
+    params name=pingd dampen=5s multiplier=100 host_list="r1 r2"
+  #
+  # monitor apache and the UPS
+  #
+  monitor apache 60s:30s
+  monitor apcfence 120m:60s
+  #
+  # cluster layout
+  #
+  group internal_www \
+    disk0 fs0 internal_ip apache
+  clone fence apcfence \
+    meta globally-unique=false clone-max=2 clone-node-max=1
+  clone conn pingd \
+    meta globally-unique=false clone-max=2 clone-node-max=1
+  location node_pref internal_www \
+    rule 50: #uname eq node1 \
+    rule pingd: defined pingd
+  #
+  # cluster properties
+  #
+  property stonith-enabled=true
+  commit
+EOF
+........
+
+The `crm` interface is hierarchical, with commands organized into
+separate levels by functionality. To list the available levels and
+commands, either execute +help <level>+, or, if at the top level of
+the shell, simply typing `help` will provide an overview of all
+available levels and commands.
+
+The +(live)+ string in the `crm` prompt signifies that the current CIB
+in use is the cluster live configuration. It is also possible to
+work with so-called <<topics_Features_Shadows,shadow CIBs>>. These are separate, inactive
+configurations stored in files, that can be applied and thereby
+replace the live configuration at any time.
+
+[[topics_Introcution_Completion,Tab completion]]
+=== Tab completion
+
+The `crm` makes extensive use of tab completion. The completion
+is both static (i.e. for `crm` commands) and dynamic. The latter
+takes into account the current status of the cluster or
+information from installed resource agents. Sometimes, completion
+may also be used to get short help on resource parameters. Here
+are a few examples:
+
+...............
+crm(live)resource# <TAB><TAB>
+bye           failcount     move          restart       unmigrate
+cd            help          param         show          unmove
+cleanup       list          promote       start         up
+demote        manage        quit          status        utilization
+end           meta          refresh       stop
+exit          migrate       reprobe       unmanage
+
+crm(live)configure# primitive fence-1 <TAB><TAB>
+heartbeat:  lsb:    ocf:    stonith:
+
+crm(live)configure# primitive fence-1 stonith:<TAB><TAB>
+apcmaster                external/ippower9258     fence_legacy
+apcmastersnmp            external/kdumpcheck      ibmhmc
+apcsmart                 external/libvirt         ipmilan
+
+crm(live)configure# primitive fence-1 stonith:ipmilan params <TAB><TAB>
+auth=      hostname=  ipaddr=    login=     password=  port=      priv=
+
+crm(live)configure# primitive fence-1 stonith:ipmilan params auth=<TAB><TAB>
+auth* (string)
+    The authorization type of the IPMI session ("none", "straight", "md2", or "md5")
+...............
+
+`crmsh` also comes with bash completion usable directly from the
+system shell. This should be installed automatically with the command
+itself.
+
+[[topics_Features,Features]]
+== Features
+
+The feature set of crmsh covers a wide range of functionality, and
+understanding how and when to use the various features of the shell
+can be difficult. This section of the guide describes some of the
+features and use cases of `crmsh` in more depth. The intention is to
+provide a deeper understanding of these features, but also to serve as
+a guide to using them.
+
+[[topics_Features_Shadows,Shadow CIB usage]]
+=== Shadow CIB usage
+
+A Shadow CIB is a normal cluster configuration stored in a file.
+They may be manipulated in much the same way as the _live_ CIB, with
+the key difference that changes to a shadow CIB have no effect on the
+actual cluster resources. An administrator may choose to apply any of
+them to the cluster, thus replacing the running configuration with the
+one found in the shadow CIB.
+
+The `crm` prompt always contains the name of the configuration which
+is currently in use, or the string _live_ if using the live cluster
+configuration.
+
+When editing the configuration in the `configure` level, no changes
+are actually applied until the `commit` command is executed. It is
+possible to start editing a configuration as usual, but instead of
+committing the changes to the active CIB, save them to a shadow CIB.
+
+The following example `configure` session demonstrates how this can be
+done:
+...............
+crm(live)configure# cib new test-2
+INFO: test-2 shadow CIB created
+crm(test-2)configure# commit
+...............
+
+[[topics_Features_Checks,Configuration semantic checks]]
+=== Configuration semantic checks
+
+Resource definitions may be checked against the meta-data
+provided with the resource agents. These checks are currently
+carried out:
+
+- are required parameters set
+- existence of defined parameters
+- timeout values for operations
+
+The parameter checks are obvious and need no further explanation.
+Failures in these checks are treated as configuration errors.
+
+The timeouts for operations should be at least as long as those
+recommended in the meta-data. Too short timeout values are a
+common mistake in cluster configurations and, even worse, they
+often slip through if cluster testing was not thorough. Though
+operation timeouts issues are treated as warnings, make sure that
+the timeouts are usable in your environment. Note also that the
+values given are just _advisory minimum_---your resources may
+require longer timeouts.
+
+User may tune the frequency of checks and the treatment of errors
+by the <<cmdhelp_options_check-frequency,`check-frequency`>> and
+<<cmdhelp_options_check-mode,`check-mode`>> preferences.
+
+Note that if the +check-frequency+ is set to +always+ and the
++check-mode+ to +strict+, errors are not tolerated and such
+configuration cannot be saved.
+
+[[topics_Features_Templates,Configuration templates]]
+=== Configuration templates
+
+Configuration templates are ready made configurations created by
+cluster experts. They are designed in such a way so that users
+may generate valid cluster configurations with minimum effort.
+If you are new to Pacemaker, templates may be the best way to
+start.
+
+We will show here how to create a simple yet functional Apache
+configuration:
+...............
+# crm configure
+crm(live)configure# template
+crm(live)configure template# list templates
+apache       filesystem   virtual-ip
+crm(live)configure template# new web <TAB><TAB>
+apache       filesystem   virtual-ip
+crm(live)configure template# new web apache
+INFO: pulling in template apache
+INFO: pulling in template virtual-ip
+crm(live)configure template# list
+web2-d       web2     vip2     web3     vip      web
+...............
+
+We enter the `template` level from `configure`. Use the `list`
+command to show templates available on the system. The `new`
+command creates a configuration from the +apache+ template. You
+can use tab completion to pick templates. Note that the apache
+template depends on a virtual IP address which is automatically
+pulled along. The `list` command shows the just created +web+
+configuration, among other configurations (I hope that you,
+unlike me, will use more sensible and descriptive names).
+
+The `show` command, which displays the resulting configuration,
+may be used to get an idea about the minimum required changes
+which have to be done. All +ERROR+ messages show the line numbers
+in which the respective parameters are to be defined:
+...............
+crm(live)configure template# show
+ERROR: 23: required parameter ip not set
+ERROR: 61: required parameter id not set
+ERROR: 65: required parameter configfile not set
+crm(live)configure template# edit
+...............
+
+The `edit` command invokes the preferred text editor with the
++web+ configuration. At the top of the file, the user is advised
+how to make changes. A good template should require from the user
+to specify only parameters. For example, the +web+ configuration
+we created above has the following required and optional
+parameters (all parameter lines start with +%%+):
+...............
+$ grep -n ^%% ~/.crmconf/web
+23:%% ip
+31:%% netmask
+35:%% lvs_support
+61:%% id
+65:%% configfile
+71:%% options
+76:%% envfiles
+...............
+
+These lines are the only ones that should be modified. Simply
+append the parameter value at the end of the line. For instance,
+after editing this template, the result could look like this (we
+used tabs instead of spaces to make the values stand out):
+...............
+$ grep -n ^%% ~/.crmconf/web
+23:%% ip        192.168.1.101
+31:%% netmask
+35:%% lvs_support
+61:%% id        websvc
+65:%% configfile    /etc/apache2/httpd.conf
+71:%% options
+76:%% envfiles
+...............
+
+As you can see, the parameter line format is very simple:
+...............
+%% <name> <value>
+...............
+
+After editing the file, use `show` again to display the
+configuration:
+...............
+crm(live)configure template# show
+primitive virtual-ip IPaddr \
+    params ip=192.168.1.101
+primitive apache apache \
+    params configfile="/etc/apache2/httpd.conf"
+monitor apache 120s:60s
+group websvc \
+    apache virtual-ip
+...............
+
+The target resource of the apache template is a group which we
+named +websvc+ in this sample session.
+
+This configuration looks exactly as you could type it at the
+`configure` level. The point of templates is to save you some
+typing. It is important, however, to understand the configuration
+produced.
+
+Finally, the configuration may be applied to the current
+crm configuration (note how the configuration changed slightly,
+though it is still equivalent, after being digested at the
+`configure` level):
+...............
+crm(live)configure template# apply
+crm(live)configure template# cd ..
+crm(live)configure# show
+node xen-b
+node xen-c
+primitive apache apache \
+    params configfile="/etc/apache2/httpd.conf" \
+    op monitor interval=120s timeout=60s
+primitive virtual-ip IPaddr \
+    params ip=192.168.1.101
+group websvc apache virtual-ip
+...............
+
+Note that this still does not commit the configuration to the CIB
+which is used in the shell, either the running one (+live+) or
+some shadow CIB. For that you still need to execute the `commit`
+command.
+
+To complete our example, we should also define the preferred node
+to run the service:
+...............
+crm(live)configure# location websvc-pref websvc 100: xen-b
+...............
+
+If you are not happy with some resource names which are provided
+by default, you can rename them now:
+...............
+crm(live)configure# rename virtual-ip intranet-ip
+crm(live)configure# show
+node xen-b
+node xen-c
+primitive apache apache \
+    params configfile="/etc/apache2/httpd.conf" \
+    op monitor interval=120s timeout=60s
+primitive intranet-ip IPaddr \
+    params ip=192.168.1.101
+group websvc apache intranet-ip
+location websvc-pref websvc 100: xen-b
+...............
+
+To summarize, working with templates typically consists of the
+following steps:
+
+- `new`: create a new configuration from templates
+- `edit`: define parameters, at least the required ones
+- `show`: see if the configuration is valid
+- `apply`: apply the configuration to the `configure` level
+
+[[topics_Features_Testing,Resource testing]]
+=== Resource testing
+
+The amount of detail in a cluster makes all configurations prone
+to errors. By far the largest number of issues in a cluster is
+due to bad resource configuration. The shell can help quickly
+diagnose such problems. And considerably reduce your keyboard
+wear.
+
+Let's say that we entered the following configuration:
+...............
+node xen-b
+node xen-c
+node xen-d
+primitive fencer stonith:external/libvirt \
+    params hypervisor_uri="qemu+tcp://10.2.13.1/system" \
+        hostlist="xen-b xen-c xen-d" \
+    op monitor interval=2h
+primitive svc Xinetd \
+    params service=systat \
+    op monitor interval=30s
+primitive intranet-ip IPaddr2 \
+    params ip=10.2.13.100 \
+    op monitor interval=30s
+primitive apache apache \
+    params configfile="/etc/apache2/httpd.conf" \
+    op monitor interval=120s timeout=60s
+group websvc apache intranet-ip
+location websvc-pref websvc 100: xen-b
+...............
+
+Before typing `commit` to submit the configuration to the cib we
+can make sure that all resources are usable on all nodes:
+...............
+crm(live)configure# rsctest websvc svc fencer
+...............
+
+It is important that resources being tested are not running on
+any nodes. Otherwise, the `rsctest` command will refuse to do
+anything. Of course, if the current configuration resides in a
+CIB shadow, then a `commit` is irrelevant. The point being that
+resources are not running on any node.
+
+.Note on stopping all resources
+****************************
+Alternatively to not committing a configuration, it is also
+possible to tell Pacemaker not to start any resources:
+
+...............
+crm(live)configure# property stop-all-resources=yes
+...............
+Almost none---resources of class stonith are still started. But
+shell is not as strict when it comes to stonith resources.
+****************************
+
+Order of resources is significant insofar that a resource depends
+on all resources to its left. In most configurations, it's
+probably practical to test resources in several runs, based on
+their dependencies.
+
+Apart from groups, `crm` does not interpret constraints and
+therefore knows nothing about resource dependencies. It also
+doesn't know if a resource can run on a node at all in case of an
+asymmetric cluster. It is up to the user to specify a list of
+eligible nodes if a resource is not meant to run on every node.
+
+[[topics_Features_Security,Access Control Lists (ACL)]]
+=== Access Control Lists (ACL)
+
+.Note on ACLs in Pacemaker 1.1.12
+****************************
+The support for ACLs has been revised in Pacemaker version 1.1.12 and
+up. Depending on which version you are using, the information in this
+section may no longer be accurate. Look for the `acl_target` and
+`acl_group` configuration elements for more details on the new
+syntax.
+****************************
+
+By default, the users from the +haclient+ group have full access
+to the cluster (or, more precisely, to the CIB). Access control
+lists allow for finer access control to the cluster.
+
+Access control lists consist of an ordered set of access rules.
+Each rule allows read or write access or denies access
+completely. Rules are typically combined to produce a specific
+role. Then, users may be assigned a role.
+
+For instance, this is a role which defines a set of rules
+allowing management of a single resource:
+
+...............
+role bigdb_admin \
+    write meta:bigdb:target-role \
+    write meta:bigdb:is-managed \
+    write location:bigdb \
+    read ref:bigdb
+...............
+
+The first two rules allow modifying the +target-role+ and
++is-managed+ meta attributes which effectively enables users in
+this role to stop/start and manage/unmanage the resource. The
+constraints write access rule allows moving the resource around.
+Finally, the user is granted read access to the resource
+definition.
+
+For proper operation of all Pacemaker programs, it is advisable
+to add the following role to all users:
+
+...............
+role read_all \
+    read cib
+...............
+
+For finer grained read access try with the rules listed in the
+following role:
+
+...............
+role basic_read \
+    read node attribute:uname \
+    read node attribute:type \
+    read property \
+    read status
+...............
+
+It is however possible that some Pacemaker programs (e.g.
+`ptest`) may not function correctly if the whole CIB is not
+readable.
+
+Some of the ACL rules in the examples above are expanded by the
+shell to XPath specifications. For instance, 
++meta:bigdb:target-role+ expands to:
+
+........
+//primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']
+........
+
+You can see the expansion by showing XML:
+
+...............
+crm(live) configure# show xml bigdb_admin
+...
+<acls>
+  <acl_role id="bigdb_admin">
+      <write id="bigdb_admin-write"
+      xpath="//primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']"/>
+...............
+
+Many different XPath expressions can have equal meaning. For
+instance, the following two are equal, but only the first one is
+going to be recognized as shortcut:
+
+...............
+//primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']
+//resources/primitive[@id='bigdb']/meta_attributes/nvpair[@name='target-role']
+...............
+
+XPath is a powerful language, but you should try to keep your ACL
+xpaths simple and the builtin shortcuts should be used whenever
+possible.
+
+[[topics_Features_Resourcesets,Syntax: Resource sets]]
+=== Syntax: Resource sets
+
+Using resource sets can be a bit confusing unless one knows the
+details of the implementation in Pacemaker as well as how to interpret
+the syntax provided by `crmsh`.
+
+Three different types of resource sets are provided by `crmsh`, and
+each one implies different values for the two resource set attributes,
++sequential+ and +require-all+.
+
++sequential+::
+  If true, the resources in the set do not depend on each other
+  internally. Setting +sequential+ to +true+ implies a strict order of
+  dependency within the set.
+
++require-all+::
+  If false, only one resource in the set is required to fulfil the
+  requirements of the set. The set of A, B and C with +require-all+
+  set to +false+ is be read as "A OR B OR C" when its dependencies
+  are resolved.
+
+The three types of resource sets modify the attributes in the
+following way:
+
+1. Implicit sets (no brackets). +sequential=true+, +require-all=true+
+2. Parenthesis set (+(+ ... +)+). +sequential=false+, +require-all=true+
+3. Bracket set (+[+ ... +]+). +sequential=false+, +require-all=false+
+
+To create a set with the properties +sequential=true+ and
++require-all=false+, explicitly set +sequential+ in a bracketed set,
++[ A B C sequential=true ]+.
+
+To create multiple sets with both +sequential+ and +require-all+ set to
+true, explicitly set +sequential+ in a parenthesis set:
++A B ( C D sequential=true )+.
+
+[[topics_Features_AttributeListReferences,Syntax: Attribute list references]]
+=== Syntax: Attribute list references
+
+Attribute lists are used to set attributes and parameters for
+resources, constraints and property definitions. For example, to set
+the virtual IP used by an +IPAddr+ resource the attribute +ip+ can be
+set in an attribute list for that resource.
+
+Attribute lists can have identifiers that name them, and other
+resources can reuse the same attribute list by referring to that name
+using an +$id-ref+. For example, the following statement defines a
+simple dummy resource with an attribute list which sets the parameter
++state+ to the value 1 and sets the identifier for the attribute list
+to +on-state+:
+
+..............
+primitive dummy-1 Dummy params $id=on-state state=1
+..............
+
+To refer to this attribute list from a different resource, refer to
+the +on-state+ name using an id-ref:
+
+..............
+primitive dummy-2 Dummy params $id-ref=on-state
+..............
+
+The resource +dummy-2+ will now also have the parameter +state+ set to the value 1.
+
+[[topics_Features_AttributeReferences,Syntax: Attribute references]]
+=== Syntax: Attribute references
+
+In some cases, referencing complete attribute lists is too
+coarse-grained, for example if two different parameters with different
+names should have the same value set. Instead of having to copy the
+value in multiple places, it is possible to create references to
+individual attributes in attribute lists.
+
+To name an attribute in order to be able to refer to it later, prefix
+the attribute name with a +$+ character (as seen above with the
+special names +$id+ and +$id-ref+:
+
+............
+primitive dummy-1 Dummy params $state=1
+............
+
+The identifier +state+ can now be used to refer to this attribute from other
+primitives, using the +@<id>+ syntax:
+
+............
+primitive dummy-2 Dummy params @state
+............
+
+In some cases, using the attribute name as the identifier doesn't work
+due to name clashes. In this case, the syntax +$<id>:<name>=<value>+
+can be used to give the attribute a different identifier:
+
+............
+primitive dummy-1 params $dummy-state-on:state=1
+primitive dummy-2 params @dummy-state-on
+............
+
+There is also the possibility that two resources both use the same
+attribute value but with different names. For example, a web server
+may have a parameter +server_ip+ for setting the IP address where it
+listens for incoming requests, and a virtual IP resource may have a
+parameter called +ip+ which sets the IP address it creates. To
+configure these two resources with an IP without repeating the value,
+the reference can be given a name using the syntax +@<id>:<name>+.
+
+Example:
+............
+primitive virtual-ip IPaddr2 params $vip:ip=192.168.1.100
+primitive webserver apache params @vip:server_ip
+............
+
+[[topics_Reference,Command reference]]
+== Command reference
+
+We define a small and simple language. Most commands consist of
+just a list of simple tokens. The only complex constructs are
+found at the `configure` level.
+
+The syntax is described in a somewhat informal manner: `<>`
+denotes a string, `[]` means that the construct is optional, the
+ellipsis (`...`) signifies that the previous construct may be
+repeated, `|` means pick one of many, and the rest are literals
+(strings, `:`, `=`).
+
+[[cmdhelp_root_status,Cluster status]]
+=== `status`
+
+Show cluster status. The status is displayed by `crm_mon`. Supply
+additional arguments for more information or different format.
+See `crm_mon(8)` for more details.
+
+Usage:
+...............
+status [<option> ...]
+
+option :: bynode | inactive | ops | timing | failcounts
+...............
+
+[[cmdhelp_cluster,Cluster setup and management]]
+=== `cluster`
+
+Whole-cluster configuration management with High Availability
+awareness.
+
+The commands on the cluster level allows configuration and
+modification of the underlying cluster infrastructure, and also
+supplies tools to do whole-cluster systems management.
+
+These commands enable easy installation and maintenance of a HA
+cluster, by providing support for package installation, configuration
+of the cluster messaging layer, file system setup and more.
+
+[[cmdhelp_cluster_start,Start cluster services]]
+==== `start`
+
+Starts the cluster-related system services on this node.
+
+Usage:
+.........
+start
+.........
+
+[[cmdhelp_cluster_stop,Stop cluster services]]
+==== `stop`
+
+Stops the cluster-related system services on this node.
+
+Usage:
+.........
+stop
+.........
+
+[[cmdhelp_cluster_init,Initializes a new HA cluster]]
+==== `init`
+
+Installs and configures a basic HA cluster on a set of nodes.
+
+Usage:
+........
+init node1 node2 node3
+init --dry-run node1 node2 node3
+........
+
+[[cmdhelp_cluster_add,Add a new node to the cluster]]
+==== `add`
+
+This command simplifies the process of adding a new node to a running
+cluster. The new node will be installed and configured with the
+packages and configuration files needed to run the cluster
+resources. If a cluster file system is used, the new node will be set
+up to host the file system.
+
+This command should be executed from a node already in the cluster.
+
+Usage:
+...............
+add <node>
+...............
+
+[[cmdhelp_cluster_remove,Remove a node from the cluster]]
+==== `remove`
+
+This command simplifies the process of removing a node from the
+cluster, moving any resources hosted by that node to other nodes.
+
+Usage:
+...............
+remove <node>
+...............
+
+[[cmdhelp_cluster_status,Cluster status check]]
+==== `status`
+
+Reports the status for the cluster messaging layer on the local
+node.
+
+Usage:
+...............
+status
+...............
+
+[[cmdhelp_cluster_health,Cluster health check]]
+==== `health`
+
+Runs a larger set of tests and queries on all nodes in the cluster to
+verify the general system health and detect potential problems.
+
+Usage:
+...............
+health
+...............
+
+[[cmdhelp_cluster_wait_for_startup,Wait for cluster to start]]
+==== `wait_for_startup`
+
+Mostly useful in scripts or automated workflows, this command will
+attempt to connect to the local cluster node repeatedly. The command
+will keep trying until the cluster node responds, or the `timeout`
+elapses. The timeout can be changed by supplying a value in seconds as
+an argument.
+
+Usage:
+........
+wait_for_startup
+........
+
+[[cmdhelp_cluster_run,Execute an arbitrary command on all nodes]]
+==== `run`
+
+This command takes a shell statement as argument, executes that
+statement on all nodes in the cluster, and reports the result.
+
+Usage:
+...............
+run <command>
+...............
+
+Example:
+...............
+run "cat /proc/uptime"
+...............
+
+[[cmdhelp_script,Cluster script management]]
+=== `script`
+
+Cluster scripts can perform cluster-wide configuration,
+validation and management. See the `list` command for
+an overview of available scripts.
+
+[[cmdhelp_script_list,List available scripts]]
+==== `list`
+
+Lists the available cluster scripts.
+
+Usage:
+............
+list
+............
+
+[[cmdhelp_script_verify,Verify the cluster script]]
+==== `verify`
+
+Mainly useful when creating new scripts, this command
+verifies that the script definition has all necessary
+fields and that the referenced actions exist.
+
+Usage:
+............
+verify <script>
+............
+
+[[cmdhelp_script_describe,Describe the cluster script]]
+==== `describe`
+
+Prints a description and short summary of the cluster script, with
+descriptions of all parameters, both required and optional.
+
+Usage:
+............
+describe <script>
+............
+
+[[cmdhelp_script_steps,List steps in cluster script]]
+==== `steps`
+
+List the names of all steps in the cluster script.
+
+This command is intended for use by automated tools
+and the web frontend.
+
+Usage:
+............
+steps <script>
+............
+
+
+[[cmdhelp_script_run,Execute the cluster script]]
+==== `run`
+
+Runs a cluster script. Can optionally take at least two arguments:
+* `nodes=<nodes>`: List of nodes that the script runs over
+* `dry_run=yes|no`: If set, the script will not perform any modifications.
+
+Additional arguments may be available depending on the cluster
+script. Use the `describe` command to see what arguments are
+provided.
+
+Usage:
+.............
+run <script> [args...]
+.............
+
+Example:
+.............
+run health dry_run=yes verbose=yes
+run init nodes="node-1 node-2 node-3"
+.............
+
+[[cmdhelp_corosync,Corosync management]]
+=== `corosync`
+
+Corosync is the underlying messaging layer for most HA clusters.
+This level provides commands for editing and managing the corosync
+configuration.
+
+[[cmdhelp_corosync_status,Display the corosync status]]
+==== `status`
+
+Displays the status of Corosync, including the votequorum state.
+
+Usage:
+.........
+status
+.........
+
+[[cmdhelp_corosync_show,Display the corosync configuration]]
+==== `show`
+
+Displays the corosync configuration on the current node.
+
+.........
+show
+.........
+
+[[cmdhelp_corosync_edit,Edit the corosync configuration]]
+==== `edit`
+
+Opens the Corosync configuration file in an editor.
+
+Usage:
+.........
+edit
+.........
+
+[[cmdhelp_corosync_log,Show the corosync log file]]
+==== `log`
+
+Opens the log file specified in the corosync configuration file. If no
+log file is configured, this command returns an error.
+
+The pager used can be configured either using the PAGER
+environment variable or in `crm.conf`.
+
+Usage:
+.........
+log
+.........
+
+[[cmdhelp_corosync_reload,Reload the corosync configuration]]
+==== `reload`
+
+Tells all instances of corosync in this cluster to reload
+`corosync.conf`.
+
+After pushing a new configuration to all cluster nodes, call this
+command to make corosync use the new configuration.
+
+Usage:
+.........
+reload
+.........
+
+[[cmdhelp_corosync_push,Push the corosync configuration]]
+==== `push`
+
+Pushes the corosync configuration file on this node to
+the list of nodes provided. If no target nodes are given,
+the configuration is pushed to all other nodes in the cluster.
+
+It is recommended to use `csync2` to distribute the cluster
+configuration files rather than relying on this command.
+
+Usage:
+.........
+push [node] ...
+.........
+
+Example:
+.........
+push node-2 node-3
+.........
+
+[[cmdhelp_corosync_pull,Pulls the corosync configuration]]
+==== `pull`
+
+Gets the corosync configuration from another node and copies
+it to this node.
+
+Usage:
+.........
+pull <node>
+.........
+
+[[cmdhelp_corosync_diff,Diffs the corosync configuration]]
+==== `diff`
+
+Diffs the corosync configurations on different nodes. If no nodes are
+given as arguments, the corosync configurations on all nodes in the
+cluster are compared.
+
+`diff` takes an option argument `--checksum`, to force checksum mode.
+
+If the number of nodes to compare are greater than two, `diff`
+automatically switches to checksum mode.
+
+Usage:
+.........
+diff [--checksum] [node...]
+.........
+
+[[cmdhelp_corosync_add-node,Add a corosync node]]
+==== `add-node`
+
+Adds a node to the corosync configuration. This is used with the `udpu`
+type configuration in corosync.
+
+A nodeid for the added node is generated automatically.
+
+Note that this command assumes that only a single ring is used, and
+sets only the address for ring0.
+
+Usage:
+.........
+add-node <addr>
+.........
+
+[[cmdhelp_corosync_del-node,Remove a corosync node]]
+==== `del-node`
+
+Removes a node from the corosync configuration. The argument given is
+the `ring0_addr` address set in the configuration file.
+
+Usage:
+.........
+del-node <addr>
+.........
+
+[[cmdhelp_corosync_get,Get a corosync configuration value]]
+==== `get`
+
+Returns the value configured in `corosync.conf`, which is not
+necessarily the value used in the running configuration. See `reload`
+for telling corosync about configuration changes.
+
+The argument is the complete dot-separated path to the value.
+
+If there are multiple values configured with the same path, the
+command returns all values for that path. For example, to get all
+configured `ring0_addr` values, use this command:
+
+Example:
+........
+get nodelist.node.ring0_addr
+........
+
+[[cmdhelp_corosync_set,Set a corosync configuration value]]
+==== `set`
+
+Sets the value identified by the given path. If the value does not
+exist in the configuration file, it will be added. However, if the
+section containing the value does not exist, the command will fail.
+
+Usage:
+.........
+set quorum.expected_votes 2
+.........
+
+[[cmdhelp_cib,CIB shadow management]]
+=== `cib` (shadow CIBs)
+
+This level is for management of shadow CIBs. It is available both
+at the top level and the `configure` level.
+
+All the commands are implemented using `cib_shadow(8)` and the
+`CIB_shadow` environment variable. The user prompt always
+includes the name of the currently active shadow or the live CIB.
+
+[[cmdhelp_cib_new,create a new shadow CIB]]
+==== `new`
+
+Create a new shadow CIB. The live cluster configuration and
+status is copied to the shadow CIB.
+
+If the name of the shadow is omitted, we create a temporary CIB
+shadow. It is useful if multiple level sessions are desired
+without affecting the cluster. A temporary CIB shadow is short
+lived and will be removed either on `commit` or on program exit.
+Note that if the temporary shadow is not committed all changes in
+the temporary shadow are lost.
+
+Specify `withstatus` if you want to edit the status section of
+the shadow CIB (see the <<cmdhelp_cibstatus,cibstatus section>>).
+Add `force` to force overwriting the existing shadow CIB.
+
+To start with an empty configuration that is not copied from the live
+CIB, specify the `empty` keyword. (This also allows a shadow CIB to be
+created in case no cluster is running.)
+
+Usage:
+...............
+new [<cib>] [withstatus] [force] [empty]
+...............
+
+[[cmdhelp_cib_delete,delete a shadow CIB]]
+==== `delete`
+
+Delete an existing shadow CIB.
+
+Usage:
+...............
+delete <cib>
+...............
+
+[[cmdhelp_cib_reset,copy live cib to a shadow CIB]]
+==== `reset`
+
+Copy the current cluster configuration into the shadow CIB.
+
+Usage:
+...............
+reset <cib>
+...............
+
+[[cmdhelp_cib_commit,copy a shadow CIB to the cluster]]
+==== `commit`
+
+Apply a shadow CIB to the cluster. If the shadow name is omitted
+then the current shadow CIB is applied.
+
+Temporary shadow CIBs are removed automatically on commit.
+
+Usage:
+...............
+commit [<cib>]
+...............
+
+[[cmdhelp_cib_use,change working CIB]]
+==== `use`
+
+Choose a CIB source. If you want to edit the status from the
+shadow CIB specify `withstatus` (see <<cmdhelp_cibstatus,`cibstatus`>>).
+Leave out the CIB name to switch to the running CIB.
+
+Usage:
+...............
+use [<cib>] [withstatus]
+...............
+
+[[cmdhelp_cib_diff,diff between the shadow CIB and the live CIB]]
+==== `diff`
+
+Print differences between the current cluster configuration and
+the active shadow CIB.
+
+Usage:
+...............
+diff
+...............
+
+[[cmdhelp_cib_list,list all shadow CIBs]]
+==== `list`
+
+List existing shadow CIBs.
+
+Usage:
+...............
+list
+...............
+
+[[cmdhelp_cib_import,import a CIB or PE input file to a shadow]]
+==== `import`
+
+At times it may be useful to create a shadow file from the
+existing CIB. The CIB may be specified as file or as a PE input
+file number. The shell will look up files in the local directory
+first and then in the PE directory (typically `/var/lib/pengine`).
+Once the CIB file is found, it is copied to a shadow and this
+shadow is immediately available for use at both `configure` and
+`cibstatus` levels.
+
+If the shadow name is omitted then the target shadow is named
+after the input CIB file.
+
+Note that there are often more than one PE input file, so you may
+need to specify the full name.
+
+Usage:
+...............
+import {<file>|<number>} [<shadow>]
+...............
+Examples:
+...............
+import pe-warn-2222
+import 2289 issue2
+...............
+
+[[cmdhelp_cib_cibstatus,CIB status management and editing]]
+==== `cibstatus`
+
+Enter edit and manage the CIB status section level. See the
+<<cmdhelp_cibstatus,CIB status management section>>.
+
+[[cmdhelp_ra,Resource Agents (RA) lists and documentation]]
+=== `ra`
+
+This level contains commands which show various information about
+the installed resource agents. It is available both at the top
+level and at the `configure` level.
+
+[[cmdhelp_ra_classes,list classes and providers]]
+==== `classes`
+
+Print all resource agents' classes and, where appropriate, a list
+of available providers.
+
+Usage:
+...............
+classes
+...............
+
+[[cmdhelp_ra_list,list RA for a class (and provider)]]
+==== `list`
+
+List available resource agents for the given class. If the class
+is `ocf`, supply a provider to get agents which are available
+only from that provider.
+
+Usage:
+...............
+list <class> [<provider>]
+...............
+Example:
+...............
+list ocf pacemaker
+...............
+
+[[cmdhelp_ra_info,show meta data for a RA]]
+==== `info` (`meta`)
+
+Show the meta-data of a resource agent type. This is where users
+can find information on how to use a resource agent. It is also
+possible to get information from some programs: `pengine`,
+`crmd`, `cib`, and `stonithd`. Just specify the program name
+instead of an RA.
+
+Usage:
+...............
+info [<class>:[<provider>:]]<type>
+info <type> <class> [<provider>] (obsolete)
+...............
+Example:
+...............
+info apache
+info ocf:pacemaker:Dummy
+info stonith:ipmilan
+info pengine
+...............
+
+[[cmdhelp_ra_providers,show providers for a RA and a class]]
+==== `providers`
+
+List providers for a resource agent type. The class parameter
+defaults to `ocf`.
+
+Usage:
+...............
+providers <type> [<class>]
+...............
+Example:
+...............
+providers apache
+...............
+
+[[cmdhelp_resource,Resource management]]
+=== `resource`
+
+At this level resources may be managed.
+
+All (or almost all) commands are implemented with the CRM tools
+such as `crm_resource(8)`.
+
+[[cmdhelp_resource_status,show status of resources]]
+==== `status` (`show`, `list`)
+
+Print resource status. If the resource parameter is left out
+status of all resources is printed.
+
+Usage:
+...............
+status [<rsc>]
+...............
+
+[[cmdhelp_resource_start,start a resource]]
+==== `start`
+
+Start a resource by setting the `target-role` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `target-role` attributes
+are removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+start <rsc>
+...............
+
+[[cmdhelp_resource_stop,stop a resource]]
+==== `stop`
+
+Stop a resource using the `target-role` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `target-role` attributes
+are removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+stop <rsc>
+...............
+
+[[cmdhelp_resource_restart,restart a resource]]
+==== `restart`
+
+Restart a resource. This is essentially a shortcut for resource
+stop followed by a start. The shell is first going to wait for
+the stop to finish, that is for all resources to really stop, and
+only then to order the start action. Due to this command
+entailing a whole set of operations, informational messages are
+printed to let the user see some progress.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+restart <rsc>
+...............
+Example:
+...............
+# crm resource restart g_webserver
+INFO: ordering g_webserver to stop
+waiting for stop to finish .... done
+INFO: ordering g_webserver to start
+#
+...............
+
+[[cmdhelp_resource_promote,promote a master-slave resource]]
+==== `promote`
+
+Promote a master-slave resource using the `target-role`
+attribute.
+
+Usage:
+...............
+promote <rsc>
+...............
+
+[[cmdhelp_resource_demote,demote a master-slave resource]]
+==== `demote`
+
+Demote a master-slave resource using the `target-role`
+attribute.
+
+Usage:
+...............
+demote <rsc>
+...............
+
+[[cmdhelp_resource_manage,put a resource into managed mode]]
+==== `manage`
+
+Manage a resource using the `is-managed` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `is-managed` attributes are
+removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+manage <rsc>
+...............
+
+[[cmdhelp_resource_unmanage,put a resource into unmanaged mode]]
+==== `unmanage`
+
+Unmanage a resource using the `is-managed` attribute. If there
+are multiple meta attributes sets, the attribute is set in all of
+them. If the resource is a clone, all `is-managed` attributes are
+removed from the children resources.
+
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+
+Usage:
+...............
+unmanage <rsc>
+...............
+
+[[cmdhelp_resource_migrate,migrate a resource to another node]]
+==== `migrate` (`move`)
+
+Migrate a resource to a different node. If node is left out, the
+resource is migrated by creating a constraint which prevents it from
+running on the current node. Additionally, you may specify a
+lifetime for the constraint---once it expires, the location
+constraint will no longer be active.
+
+Usage:
+...............
+migrate <rsc> [<node>] [<lifetime>] [force]
+...............
+
+[[cmdhelp_resource_unmigrate,unmigrate a resource to another node]]
+==== `unmigrate` (`unmove`)
+
+Remove the constraint generated by the previous migrate command.
+
+Usage:
+...............
+unmigrate <rsc>
+...............
+
+[[cmdhelp_resource_maintenance,Enable/disable per-resource maintenance mode]]
+==== `maintenance`
+
+Enables or disables the per-resource maintenance mode. When this mode
+is enabled, no monitor operations will be triggered for the resource.
+
+Usage:
+..................
+maintenance <resource> [on|off|true|false]
+..................
+
+Example:
+..................
+maintenance rsc1
+maintenance rsc2 off
+..................
+
+[[cmdhelp_resource_param,manage a parameter of a resource]]
+==== `param`
+
+Show/edit/delete a parameter of a resource.
+
+Usage:
+...............
+param <rsc> set <param> <value>
+param <rsc> delete <param>
+param <rsc> show <param>
+...............
+Example:
+...............
+param ip_0 show ip
+...............
+
+[[cmdhelp_resource_secret,manage sensitive parameters]]
+==== `secret`
+
+Sensitive parameters can be kept in local files rather than CIB
+in order to prevent accidental data exposure. Use the `secret`
+command to manage such parameters. `stash` and `unstash` move the
+value from the CIB and back to the CIB respectively. The `set`
+subcommand sets the parameter to the provided value. `delete`
+removes the parameter completely. `show` displays the value of
+the parameter from the local file. Use `check` to verify if the
+local file content is valid.
+
+Usage:
+...............
+secret <rsc> set <param> <value>
+secret <rsc> stash <param>
+secret <rsc> unstash <param>
+secret <rsc> delete <param>
+secret <rsc> show <param>
+secret <rsc> check <param>
+...............
+Example:
+...............
+secret fence_1 show password
+secret fence_1 stash password
+secret fence_1 set password secret_value
+...............
+
+[[cmdhelp_resource_meta,manage a meta attribute]]
+==== `meta`
+
+Show/edit/delete a meta attribute of a resource. Currently, all
+meta attributes of a resource may be managed with other commands
+such as `resource stop`.
+
+Usage:
+...............
+meta <rsc> set <attr> <value>
+meta <rsc> delete <attr>
+meta <rsc> show <attr>
+...............
+Example:
+...............
+meta ip_0 set target-role stopped
+...............
+
+[[cmdhelp_resource_utilization,manage a utilization attribute]]
+==== `utilization`
+
+Show/edit/delete a utilization attribute of a resource. These
+attributes describe hardware requirements. By setting the
+`placement-strategy` cluster property appropriately, it is
+possible then to distribute resources based on resource
+requirements and node size. See also <<cmdhelp_node_utilization,node utilization attributes>>.
+
+Usage:
+...............
+utilization <rsc> set <attr> <value>
+utilization <rsc> delete <attr>
+utilization <rsc> show <attr>
+...............
+Example:
+...............
+utilization xen1 set memory 4096
+...............
+
+[[cmdhelp_resource_failcount,manage failcounts]]
+==== `failcount`
+
+Show/edit/delete the failcount of a resource.
+
+Usage:
+...............
+failcount <rsc> set <node> <value>
+failcount <rsc> delete <node>
+failcount <rsc> show <node>
+...............
+Example:
+...............
+failcount fs_0 delete node2
+...............
+
+[[cmdhelp_resource_cleanup,cleanup resource status]]
+==== `cleanup`
+
+Cleanup resource status. Typically done after the resource has
+temporarily failed. If a node is omitted, cleanup on all nodes.
+If there are many nodes, the command may take a while.
+
+Usage:
+...............
+cleanup <rsc> [<node>]
+...............
+
+[[cmdhelp_resource_refresh,refresh CIB from the LRM status]]
+==== `refresh`
+
+Refresh CIB from the LRM status.
+
+Usage:
+...............
+refresh [<node>]
+...............
+
+[[cmdhelp_resource_reprobe,probe for resources not started by the CRM]]
+==== `reprobe`
+
+Probe for resources not started by the CRM.
+
+Usage:
+...............
+reprobe [<node>]
+...............
+
+[[cmdhelp_resource_trace,start RA tracing]]
+==== `trace`
+
+Start tracing RA for the given operation. The trace files are
+stored in `$HA_VARLIB/trace_ra`. If the operation to be traced is
+monitor, note that the number of trace files can grow very
+quickly.
+
+If no operation name is given, crmsh will attempt to trace all
+operations for the RA. This includes any configured operations, start
+and stop as well as promote/demote for multistate resources.
+
+Usage:
+...............
+trace <rsc> [<op> [<interval>] ]
+...............
+Example:
+...............
+trace fs start
+trace webserver
+...............
+
+[[cmdhelp_resource_untrace,stop RA tracing]]
+==== `untrace`
+
+Stop tracing RA for the given operation. If no operation name is
+given, crmsh will attempt to stop tracing all operations in resource.
+
+Usage:
+...............
+untrace <rsc> [<op> [<interval>] ]
+...............
+Example:
+...............
+untrace fs start
+untrace webserver
+...............
+
+[[cmdhelp_resource_scores,Display resource scores]]
+=== `scores`
+
+Display the allocation scores for all resources.
+
+Usage:
+................
+scores
+................
+
+[[cmdhelp_node,Nodes management]]
+=== `node`
+
+Node management and status commands.
+
+[[cmdhelp_node_status,show nodes' status as XML]]
+==== `status`
+
+Show nodes' status as XML. If the node parameter is omitted then
+all nodes are shown.
+
+Usage:
+...............
+status [<node>]
+...............
+
+[[cmdhelp_node_show,show node]]
+==== `show`
+
+Show a node definition. If the node parameter is omitted then all
+nodes are shown.
+
+Usage:
+...............
+show [<node>]
+...............
+
+[[cmdhelp_node_standby,put node into standby]]
+==== `standby`
+
+Set a node to standby status. The node parameter defaults to the
+node where the command is run. Additionally, you may specify a
+lifetime for the standby---if set to `reboot`, the node will be
+back online once it reboots. `forever` will keep the node in
+standby after reboot.
+
+Usage:
+...............
+standby [<node>] [<lifetime>]
+
+lifetime :: reboot | forever
+...............
+
+[[cmdhelp_node_online,set node online]]
+==== `online`
+
+Set a node to online status. The node parameter
+defaults to the node where the command is run.
+
+Usage:
+...............
+online [<node>]
+...............
+
+[[cmdhelp_node_maintenance,put node into maintenance mode]]
+==== `maintenance`
+
+Set the node status to maintenance. This is equivalent to the
+cluster-wide `maintenance-mode` property but puts just one node
+into the maintenance mode. The node parameter defaults to the
+node where the command is run.
+
+Usage:
+...............
+maintenance [<node>]
+...............
+
+[[cmdhelp_node_ready,put node into ready mode]]
+==== `ready`
+
+Set the node's maintenance status to `off`. The node should be
+now again fully operational and capable of running resource
+operations.
+
+Usage:
+...............
+ready [<node>]
+...............
+
+[[cmdhelp_node_fence,fence node]]
+==== `fence`
+
+Make CRM fence a node. This functionality depends on stonith
+resources capable of fencing the specified node. No such stonith
+resources, no fencing will happen.
+
+Usage:
+...............
+fence <node>
+...............
+
+[[cmdhelp_node_clearstate,Clear node state]]
+==== `clearnodestate`
+
+Resets and clears the state of the specified node. This node is
+afterwards assumed clean and offline. This command can be used to
+manually confirm that a node has been fenced (e.g., powered off).
+
+Be careful! This can cause data corruption if you confirm that a node is
+down that is, in fact, not cleanly down - the cluster will proceed as if
+the fence had succeeded, possibly starting resources multiple times.
+
+Usage:
+...............
+clearstate <node>
+...............
+
+[[cmdhelp_node_delete,delete node]]
+==== `delete`
+
+Delete a node. This command will remove the node from the CIB
+and, in case the cluster stack is running, use the appropriate
+program (`crm_node` or `hb_delnode`) to remove the node from the
+membership.
+
+If the node is still listed as active and a member of our
+partition we refuse to remove it. With the global force option
+(`-F`) we will try to delete the node anyway.
+
+Usage:
+...............
+delete <node>
+...............
+
+[[cmdhelp_node_attribute,manage attributes]]
+==== `attribute`
+
+Edit node attributes. This kind of attribute should refer to
+relatively static properties, such as memory size.
+
+Usage:
+...............
+attribute <node> set <attr> <value>
+attribute <node> delete <attr>
+attribute <node> show <attr>
+...............
+Example:
+...............
+attribute node_1 set memory_size 4096
+...............
+
+[[cmdhelp_node_utilization,manage utilization attributes]]
+==== `utilization`
+
+Edit node utilization attributes. These attributes describe
+hardware characteristics as integer numbers such as memory size
+or the number of CPUs. By setting the `placement-strategy`
+cluster property appropriately, it is possible then to distribute
+resources based on resource requirements and node size. See also
+<<cmdhelp_resource_utilization,resource utilization attributes>>.
+
+Usage:
+...............
+utilization <node> set <attr> <value>
+utilization <node> delete <attr>
+utilization <node> show <attr>
+...............
+Examples:
+...............
+utilization node_1 set memory 16384
+utilization node_1 show cpu
+...............
+
+[[cmdhelp_node_status-attr,manage status attributes]]
+==== `status-attr`
+
+Edit node attributes which are in the CIB status section, i.e.
+attributes which hold properties of a more volatile nature. One
+typical example is attribute generated by the `pingd` utility.
+
+Usage:
+...............
+status-attr <node> set <attr> <value>
+status-attr <node> delete <attr>
+status-attr <node> show <attr>
+...............
+Example:
+...............
+status-attr node_1 show pingd
+...............
+
+[[cmdhelp_site,site support]]
+=== `site`
+
+A cluster may consist of two or more subclusters in different and
+distant locations. This set of commands supports such setups.
+
+[[cmdhelp_site_ticket,manage site tickets]]
+==== `ticket`
+
+Tickets are cluster-wide attributes. They can be managed at the
+site where this command is executed.
+
+It is then possible to constrain resources depending on the
+ticket availability (see the <<cmdhelp_configure_rsc_ticket,`rsc_ticket`>> command
+for more details).
+
+Usage:
+...............
+ticket {grant|revoke|standby|activate|show|time|delete} <ticket>
+...............
+Example:
+...............
+ticket grant ticket1
+...............
+
+[[cmdhelp_options,user preferences]]
+=== `options`
+
+The user may set various options for the crm shell itself.
+
+[[cmdhelp_options_skill-level,set skill level]]
+==== `skill-level`
+
+Based on the skill-level setting, the user is allowed to use only
+a subset of commands. There are three levels: operator,
+administrator, and expert. The operator level allows only
+commands at the `resource` and `node` levels, but not editing
+or deleting resources. The administrator may do that and may also
+configure the cluster at the `configure` level and manage the
+shadow CIBs. The expert may do all.
+
+Usage:
+...............
+skill-level <level>
+
+level :: operator | administrator | expert
+...............
+
+.Note on security
+****************************
+The `skill-level` option is advisory only. There is nothing
+stopping any users change their skill level (see
+<<topics_Features_Security,Access Control Lists (ACL)>> on how to enforce
+access control).
+****************************
+
+[[cmdhelp_options_user,set the cluster user]]
+==== `user`
+
+Sufficient privileges are necessary in order to manage a
+cluster: programs such as `crm_verify` or `crm_resource` and,
+ultimately, `cibadmin` have to be run either as `root` or as the
+CRM owner user (typically `hacluster`). You don't have to worry
+about that if you run `crm` as `root`. A more secure way is to
+run the program with your usual privileges, set this option to
+the appropriate user (such as `hacluster`), and setup the
+`sudoers` file.
+
+Usage:
+...............
+user system-user
+...............
+Example:
+...............
+user hacluster
+...............
+
+[[cmdhelp_options_editor,set preferred editor program]]
+==== `editor`
+
+The `edit` command invokes an editor. Use this to specify your
+preferred editor program. If not set, it will default to either
+the value of the `EDITOR` environment variable or to one of the
+standard UNIX editors (`vi`,`emacs`,`nano`).
+
+Usage:
+...............
+editor program
+...............
+Example:
+...............
+editor vim
+...............
+
+[[cmdhelp_options_pager,set preferred pager program]]
+==== `pager`
+
+The `view` command displays text through a pager. Use this to
+specify your preferred pager program. If not set, it will default
+to either the value of the `PAGER` environment variable or to one
+of the standard UNIX system pagers (`less`,`more`,`pg`).
+
+[[cmdhelp_options_sort-elements,sort CIB elements]]
+==== `sort-elements`
+
+`crm` by default sorts CIB elements. If you want them appear in
+the order they were created, set this option to `no`.
+
+Usage:
+...............
+sort-elements {yes|no}
+...............
+Example:
+...............
+sort-elements no
+...............
+
+[[cmdhelp_options_wait,synchronous operation]]
+==== `wait`
+
+In normal operation, `crm` runs a command and gets back
+immediately to process other commands or get input from the user.
+With this option set to `yes` it will wait for the started
+transition to finish. In interactive mode dots are printed to
+indicate progress.
+
+Usage:
+...............
+wait {yes|no}
+...............
+Example:
+...............
+wait yes
+...............
+
+[[cmdhelp_options_output,set output type]]
+==== `output`
+
+`crm` can adorn configurations in two ways: in color (similar to
+for instance the `ls --color` command) and by showing keywords in
+upper case. Possible values are `plain`, `color`, and
+'uppercase'. It is possible to combine the latter two in order to
+get an upper case xmass tree. Just set this option to
+`color,uppercase`.
+
+[[cmdhelp_options_colorscheme,set colors for output]]
+==== `colorscheme`
+
+With `output` set to `color`, a comma separated list of colors
+from this option are used to emphasize:
+
+- keywords
+- object ids
+- attribute names
+- attribute values
+- scores
+- resource references
+
+`crm` can show colors only if there is curses support for python
+installed (usually provided by the `python-curses` package). The
+colors are whatever is available in your terminal. Use `normal`
+if you want to keep the default foreground color.
+
+This user preference defaults to
+`yellow,normal,cyan,red,green,magenta` which is good for
+terminals with dark background. You may want to change the color
+scheme and save it in the preferences file for other color
+setups.
+
+Example:
+...............
+colorscheme yellow,normal,blue,red,green,magenta
+...............
+
+[[cmdhelp_options_check-frequency,when to perform semantic check]]
+==== `check-frequency`
+
+Semantic check of the CIB or elements modified or created may be
+done on every configuration change (`always`), when verifying
+(`on-verify`) or `never`. It is by default set to `always`.
+Experts may want to change the setting to `on-verify`.
+
+The checks require that resource agents are present. If they are
+not installed at the configuration time set this preference to
+`never`.
+
+See <<topics_Features_Checks,Configuration semantic checks>> for more details.
+
+[[cmdhelp_options_check-mode,how to treat semantic errors]]
+==== `check-mode`
+
+Semantic check of the CIB or elements modified or created may be
+done in the `strict` mode or in the `relaxed` mode. In the former
+certain problems are treated as configuration errors. In the
+`relaxed` mode all are treated as warnings. The default is `strict`.
+
+See <<topics_Features_Checks,Configuration semantic checks>> for more details.
+
+[[cmdhelp_options_add-quotes,add quotes around parameters containing spaces]]
+==== `add-quotes`
+
+The shell (as in `/bin/sh`) parser strips quotes from the command
+line. This may sometimes make it really difficult to type values
+which contain white space. One typical example is the configure
+filter command. The crm shell will supply extra quotes around
+arguments which contain white space. The default is `yes`.
+
+.Note on quotes use
+****************************
+Adding quotes around arguments automatically has been introduced
+with version 1.2.2 and it is technically a regression. Being a
+regression is the only reason the `add-quotes` option exists. If
+you have custom shell scripts which would break, just set the
+`add-quotes` option to `no`.
+
+For instance, with adding quotes enabled, it is possible to do
+the following:
+...............
+# crm configure primitive d1 Dummy \
+    meta description="some description here"
+# crm configure filter 'sed "s/hostlist=./&node-c /"' fencing
+...............
+****************************
+
+[[cmdhelp_options_manage-children,how to handle children resource attributes]]
+==== `manage-children`
+
+Some resource management commands, such as `resource stop`, when
+the target resource is a group, may not always produce desired
+result. Each element, group and the primitive members, can have a
+meta attribute and those attributes may end up with conflicting
+values. Consider the following construct:
+...............
+crm(live)# configure show svc fs virtual-ip
+primitive fs Filesystem \
+    params device="/dev/drbd0" directory="/srv/nfs" fstype=ext3 \
+    op monitor interval=10s \
+    meta target-role=Started
+primitive virtual-ip IPaddr2 \
+    params ip=10.2.13.110 iflabel=1 \
+    op monitor interval=10s \
+    op start interval=0 \
+    meta target-role=Started
+group svc fs virtual-ip \
+    meta target-role=Stopped
+...............
+
+Even though the element +svc+ should be stopped, the group is
+actually running because all its members have the +target-role+
+set to +Started+:
+...............
+crm(live)# resource show svc
+resource svc is running on: xen-f
+...............
+
+Hence, if the user invokes +resource stop svc+ the intention is
+not clear. This preference gives the user an opportunity to
+better control what happens if attributes of group members have
+values which are in conflict with the same attribute of the group
+itself.
+
+Possible values are +ask+ (the default), +always+, and +never+.
+If set to +always+, the crm shell removes all children attributes
+which have values different from the parent. If set to +never+,
+all children attributes are left intact. Finally, if set to
++ask+, the user will be asked for each member what is to be done.
+
+[[cmdhelp_options_show,show current user preference]]
+==== `show`
+
+Display all current settings.
+
+Given an option name as argument, `show` will display only the value
+of that argument.
+
+Given +all+ as argument, `show` displays all available user options.
+
+Usage:
+........
+show [all|<option>]
+........
+
+Example:
+........
+show
+show skill-level
+show all
+........
+
+[[cmdhelp_options_set,Set the value of a given option]]
+==== `set`
+
+Sets the value of an option. Takes the fully qualified
+name of the option as argument, as displayed by +show all+.
+
+The modified option value is stored in the user-local
+configuration file, usually found in +~/.config/crm/crm.conf+.
+
+Usage:
+........
+set <option> <value>
+........
+
+Example:
+........
+set color.warn "magenta bold"
+set editor nano
+........
+
+[[cmdhelp_options_save,save the user preferences to the rc file]]
+==== `save`
+
+Save current settings to the rc file (+$HOME/.config/crm/rc+). On
+further `crm` runs, the rc file is automatically read and parsed.
+
+[[cmdhelp_options_reset,reset user preferences to factory defaults]]
+==== `reset`
+
+This command resets all user options to the defaults. If used as
+a single-shot command, the rc file (+$HOME/.config/crm/rc+) is
+reset to the defaults too.
+
+[[cmdhelp_configure,CIB configuration]]
+=== `configure`
+
+This level enables all CIB object definition commands.
+
+The configuration may be logically divided into four parts:
+nodes, resources, constraints, and (cluster) properties and
+attributes.  Each of these commands support one or more basic CIB
+objects.
+
+Nodes and attributes describing nodes are managed using the
+`node` command.
+
+Commands for resources are:
+
+- `primitive`
+- `monitor`
+- `group`
+- `clone`
+- `ms`/`master` (master-slave)
+
+In order to streamline large configurations, it is possible to
+define a template which can later be referenced in primitives:
+
+- `rsc_template`
+
+In that case the primitive inherits all attributes defined in the
+template.
+
+There are three types of constraints:
+
+- `location`
+- `colocation`
+- `order`
+
+It is possible to define fencing order (stonith resource
+priorities):
+
+- `fencing_topology`
+
+Finally, there are the cluster properties, resource meta
+attributes defaults, and operations defaults. All are just a set
+of attributes. These attributes are managed by the following
+commands:
+
+- `property`
+- `rsc_defaults`
+- `op_defaults`
+
+In addition to the cluster configuration, the Access Control
+Lists (ACL) can be setup to allow access to parts of the CIB for
+users other than +root+ and +hacluster+. The following commands
+manage ACL:
+
+- `user`
+- `role`
+
+In Pacemaker 1.1.12 and up, these commands replace the `user` command
+for handling ACLs:
+
+- `acl_target`
+- `acl_group`
+
+The changes are applied to the current CIB only on ending the
+configuration session or using the `commit` command.
+
+Comments start with +#+ in the first line. The comments are tied
+to the element which follows. If the element moves, its comments
+will follow.
+
+[[cmdhelp_configure_node,define a cluster node]]
+==== `node`
+
+The node command describes a cluster node. Nodes in the CIB are
+commonly created automatically by the CRM. Hence, you should not
+need to deal with nodes unless you also want to define node
+attributes. Note that it is also possible to manage node
+attributes at the `node` level.
+
+Usage:
+...............
+node [$id=<id>] <uname>[:<type>]
+  [description=<description>]
+  [attributes [$id=<id>] [<score>:] [rule...]
+    <param>=<value> [<param>=<value>...]] | $id-ref=<ref>
+  [utilization [$id=<id>] [<score>:] [rule...]
+    <param>=<value> [<param>=<value>...]] | $id-ref=<ref>
+
+type :: normal | member | ping | remote
+...............
+Example:
+...............
+node node1
+node big_node attributes memory=64
+...............
+
+[[cmdhelp_configure_primitive,define a resource]]
+==== `primitive`
+
+The primitive command describes a resource. It may be referenced
+only once in group, clone, or master-slave objects. If it's not
+referenced, then it is placed as a single resource in the CIB.
+
+Operations may be specified anonymously, as a group or by reference:
+
+* "Anonymous", as a list of +op+ specifications. Use this
+  method if you don't need to reference the set of operations
+  elsewhere. This is the most common way to define operations.
+
+* If reusing operation sets is desired, use the +operations+ keyword
+  along with an id to give the operations set a name. Use the
+  +operations+ keyword and an id-ref value set to the id of another
+  operations set, to apply the same set of operations to this
+  primitive.
+
+Operation attributes which are not recognized are saved as
+instance attributes of that operation. A typical example is
++OCF_CHECK_LEVEL+.
+
+For multistate resources, roles are specified as +role=<role>+.
+
+A template may be defined for resources which are of the same
+type and which share most of the configuration. See
+<<cmdhelp_configure_rsc_template,`rsc_template`>> for more information.
+
+Attributes containing time values, such as the +interval+ attribute on
+operations, are configured either as a plain number, which is
+interpreted as a time in seconds, or using one of the following
+suffixes:
+
+* +s+, +sec+ - time in seconds (same as no suffix)
+* +ms+, +msec+ - time in milliseconds
+* +us+, +usec+ - time in microseconds
+* +m+, +min+ - time in minutes
+* +h+, +hr+ - time in hours
+
+Usage:
+...............
+primitive <rsc> {[<class>:[<provider>:]]<type>|@<template>}
+  [description=<description>]
+  [params attr_list]
+  [meta attr_list]
+  [utilization attr_list]
+  [operations id_spec]
+    [op op_type [<attribute>=<value>...] ...]
+
+attr_list :: [$id=<id>] [<score>:] [rule...]
+             <attr>=<val> [<attr>=<val>...]] | $id-ref=<id>
+id_spec :: $id=<id> | $id-ref=<id>
+op_type :: start | stop | monitor
+...............
+Example:
+...............
+primitive apcfence stonith:apcsmart \
+  params ttydev=/dev/ttyS0 hostlist="node1 node2" \
+  op start timeout=60s \
+  op monitor interval=30m timeout=60s
+
+primitive www8 apache \
+  params configfile=/etc/apache/www8.conf \
+  operations $id-ref=apache_ops
+
+primitive db0 mysql \
+  params config=/etc/mysql/db0.conf \
+  op monitor interval=60s \
+  op monitor interval=300s OCF_CHECK_LEVEL=10
+
+primitive r0 ocf:linbit:drbd \
+  params drbd_resource=r0 \
+  op monitor role=Master interval=60s \
+  op monitor role=Slave interval=300s
+
+primitive xen0 @vm_scheme1 \
+  params xmfile=/etc/xen/vm/xen0
+
+primitive mySpecialRsc Special \
+  params 3: rule #uname eq node1 interface=eth1 \
+  params 2: rule #uname eq node2 interface=eth2 port=8888 \
+  params 1: interface=eth0 port=9999
+
+...............
+
+[[cmdhelp_configure_monitor,add monitor operation to a primitive]]
+==== `monitor`
+
+Monitor is by far the most common operation. It is possible to
+add it without editing the whole resource. Also, long primitive
+definitions may be a bit uncluttered. In order to make this
+command as concise as possible, less common operation attributes
+are not available. If you need them, then use the `op` part of
+the `primitive` command.
+
+Usage:
+...............
+monitor <rsc>[:<role>] <interval>[:<timeout>]
+...............
+Example:
+...............
+monitor apcfence 60m:60s
+...............
+
+Note that after executing the command, the monitor operation may
+be shown as part of the primitive definition.
+
+[[cmdhelp_configure_group,define a group]]
+==== `group`
+
+The `group` command creates a group of resources. This can be useful
+when resources depend on other resources and require that those
+resources start in order on the same node. A commmon use of resource
+groups is to ensure that a server and a virtual IP are located
+together, and that the virtual IP is started before the server.
+
+Grouped resources are started in the order they appear in the group,
+and stopped in the reverse order. If a resource in the group cannot
+run anywhere, resources following it in the group will not start.
+
+`group` can be passed the "container" meta attribute, to indicate that
+it is to be used to group VM resources monitored using Nagios. The
+resource referred to by the container attribute must be of type
+`ocf:heartbeat:Xen`, `oxf:heartbeat:VirtualDomain` or `ocf:heartbeat:lxc`.
+
+Usage:
+...............
+group <name> <rsc> [<rsc>...]
+  [description=<description>]
+  [meta attr_list]
+  [params attr_list]
+
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+group internal_www disk0 fs0 internal_ip apache \
+  meta target_role=stopped
+
+group vm-and-services vm vm-sshd meta container="vm"
+...............
+
+[[cmdhelp_configure_clone,define a clone]]
+==== `clone`
+
+The `clone` command creates a resource clone. It may contain a
+single primitive resource or one group of resources.
+
+Usage:
+...............
+clone <name> <rsc>
+  [description=<description>]
+  [meta attr_list]
+  [params attr_list]
+
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+clone cl_fence apc_1 \
+  meta clone-node-max=1 globally-unique=false
+...............
+
+[[cmdhelp_configure_ms,define a master-slave resource]]
+==== `ms` (`master`)
+
+The `ms` command creates a master/slave resource type. It may contain a
+single primitive resource or one group of resources.
+
+Usage:
+...............
+ms <name> <rsc>
+  [description=<description>]
+  [meta attr_list]
+  [params attr_list]
+
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+ms disk1 drbd1 \
+  meta notify=true globally-unique=false
+...............
+
+.Note on `id-ref` usage
+****************************
+Instance or meta attributes (`params` and `meta`) may contain
+a reference to another set of attributes. In that case, no other
+attributes are allowed. Since attribute sets' ids, though they do
+exist, are not shown in the `crm`, it is also possible to
+reference an object instead of an attribute set. `crm` will
+automatically replace such a reference with the right id:
+
+...............
+crm(live)configure# primitive a2 www-2 meta $id-ref=a1
+crm(live)configure# show a2
+primitive a2 apache \
+    meta $id-ref=a1-meta_attributes
+    [...]
+...............
+It is advisable to give meaningful names to attribute sets which
+are going to be referenced.
+****************************
+
+[[cmdhelp_configure_rsc_template,define a resource template]]
+==== `rsc_template`
+
+The `rsc_template` command creates a resource template. It may be
+referenced in primitives. It is used to reduce large
+configurations with many similar resources.
+
+Usage:
+...............
+rsc_template <name> [<class>:[<provider>:]]<type>
+  [description=<description>]
+  [params attr_list]
+  [meta attr_list]
+  [utilization attr_list]
+  [operations id_spec]
+    [op op_type [<attribute>=<value>...] ...]
+
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+id_spec :: $id=<id> | $id-ref=<id>
+op_type :: start | stop | monitor
+...............
+Example:
+...............
+rsc_template public_vm Xen \
+  op start timeout=300s \
+  op stop timeout=300s \
+  op monitor interval=30s timeout=60s \
+  op migrate_from timeout=600s \
+  op migrate_to timeout=600s
+primitive xen0 @public_vm \
+  params xmfile=/etc/xen/xen0
+primitive xen1 @public_vm \
+  params xmfile=/etc/xen/xen1
+...............
+
+[[cmdhelp_configure_location,a location preference]]
+==== `location`
+
+`location` defines the preference of nodes for the given
+resource. The location constraints consist of one or more rules
+which specify a score to be awarded if the rule matches.
+
+The resource referenced by the location constraint can be one of the
+following:
+
+* Plain resource reference: +location loc1 webserver 100: node1+
+* Resource set in curly brackets: +location loc1 { virtual-ip webserver } 100: node1+
+* Tag containing resource ids: +location loc1 tag1 100: node1+
+* Resource pattern: +location loc1 /web.*/ 100: node1+
+
+The syntax for resource sets is described in detail for <<cmdhelp_configure_colocation,`colocation`>>.
+
+For more details on how to configure resource sets, see
+<<topics_Features_Resourcesets,`Syntax: Resource sets`>>.
+
+Usage:
+...............
+location <id> rsc [role=<role>] {node_pref|rules}
+
+rsc :: /<rsc-pattern>/
+        | { resource_sets }
+        | <rsc>
+
+node_pref :: <score>: <node>
+
+rules ::
+  rule [id_spec] [$role=<role>] <score>: <expression>
+  [rule [id_spec] [$role=<role>] <score>: <expression> ...]
+
+id_spec :: $id=<id> | $id-ref=<id>
+score :: <number> | <attribute> | [-]inf
+expression :: <simple_exp> [bool_op <simple_exp> ...]
+bool_op :: or | and
+simple_exp :: <attribute> [type:]<binary_op> <value>
+          | <unary_op> <attribute>
+          | date <date_expr>
+type :: string | version | number
+binary_op :: lt | gt | lte | gte | eq | ne
+unary_op :: defined | not_defined
+
+date_expr :: lt <end>
+         | gt <start>
+         | in start=<start> end=<end>
+         | in start=<start> <duration>
+         | spec <date_spec>
+duration|date_spec ::
+         hours=<value>
+         | monthdays=<value>
+         | weekdays=<value>
+         | yearsdays=<value>
+         | months=<value>
+         | weeks=<value>
+         | years=<value>
+         | weekyears=<value>
+         | moon=<value>
+...............
+Examples:
+...............
+location conn_1 internal_www 100: node1
+
+location conn_1 internal_www \
+  rule 50: #uname eq node1 \
+  rule pingd: defined pingd
+
+location conn_2 dummy_float \
+  rule -inf: not_defined pingd or pingd number:lte 0
+...............
+
+[[cmdhelp_configure_colocation,colocate resources]]
+==== `colocation` (`collocation`)
+
+This constraint expresses the placement relation between two
+or more resources. If there are more than two resources, then the
+constraint is called a resource set.
+
+The score is used to indicate the priority of the constraint. A
+positive score indicates that the resources should run on the same
+node. A negative score that they should not run on the same
+node. Values of positive or negative +infinity+ indicate a mandatory
+constraint.
+
+In the two resource form, the cluster will place +<with-rsc>+ first,
+and then decide where to put the +<rsc>+ resource.
+
+Collocation resource sets have an extra attribute (+sequential+)
+to allow for sets of resources which don't depend on each other
+in terms of state. The shell syntax for such sets is to put
+resources in parentheses.
+
+Sets cannot be nested.
+
+The optional +node-attribute+ references an attribute in nodes'
+instance attributes.
+
+For more details on how to configure resource sets, see
+<<topics_Features_Resourcesets,`Syntax: Resource sets`>>.
+
+Usage:
+...............
+colocation <id> <score>: <rsc>[:<role>] <with-rsc>[:<role>]
+  [node-attribute=<node_attr>]
+
+colocation <id> <score>: resource_sets
+  [node-attribute=<node_attr>]
+
+resource_sets :: resource_set [resource_set ...]
+
+resource_set :: ["("|"["] <rsc>[:<role>] [<rsc>[:<role>] ...] \
+                [attributes]  [")"|"]"]
+
+attributes :: [require-all=(true|false)] [sequential=(true|false)]
+
+...............
+Example:
+...............
+colocation never_put_apache_with_dummy -inf: apache dummy
+colocation c1 inf: A ( B C )
+...............
+
+[[cmdhelp_configure_order,order resources]]
+==== `order`
+
+This constraint expresses the order of actions on two resources
+or more resources. If there are more than two resources, then the
+constraint is called a resource set.
+
+Ordered resource sets have an extra attribute to allow for sets
+of resources whose actions may run in parallel. The shell syntax
+for such sets is to put resources in parentheses.
+
+If the subsequent resource can start or promote after any one of the
+resources in a set has done, enclose the set in brackets (+[+ and +]+).
+
+Sets cannot be nested.
+
+Three strings are reserved to specify a kind of order constraint:
++Mandatory+, +Optional+, and +Serialize+. It is preferred to use
+one of these settings instead of score. Previous versions mapped
+scores +0+ and +inf+ to keywords +advisory+ and +mandatory+.
+That is still valid but deprecated.
+
+For more details on how to configure resource sets, see
+<<topics_Features_Resourcesets,`Syntax: Resource sets`>>.
+
+Usage:
+...............
+order <id> [{kind|<score>}:] first then [symmetrical=<bool>]
+
+order <id> [{kind|<score>}:] resource_sets [symmetrical=<bool>]
+
+kind :: Mandatory | Optional | Serialize
+
+first :: <rsc>[:<action>]
+
+then ::  <rsc>[:<action>]
+
+resource_sets :: resource_set [resource_set ...]
+
+resource_set :: ["["|"("] <rsc>[:<action>] [<rsc>[:<action>] ...] \
+                [attributes] ["]"|")"]
+
+attributes :: [require-all=(true|false)] [sequential=(true|false)]
+
+...............
+Example:
+...............
+order o-1 Mandatory: apache:start ip_1
+order o-2 Serialize: A ( B C )
+order o-3 inf: [ A B ] C
+order o-4 first-resource then-resource
+...............
+
+[[cmdhelp_configure_rsc_ticket,resources ticket dependency]]
+==== `rsc_ticket`
+
+This constraint expresses dependency of resources on cluster-wide
+attributes, also known as tickets. Tickets are mainly used in
+geo-clusters, which consist of multiple sites. A ticket may be
+granted to a site, thus allowing resources to run there.
+
+The +loss-policy+ attribute specifies what happens to the
+resource (or resources) if the ticket is revoked. The default is
+either +stop+ or +demote+ depending on whether a resource is
+multi-state.
+
+See also the <<cmdhelp_site_ticket,`site`>> set of commands.
+
+Usage:
+...............
+rsc_ticket <id> <ticket_id>: <rsc>[:<role>] [<rsc>[:<role>] ...]
+  [loss-policy=<loss_policy_action>]
+
+loss_policy_action :: stop | demote | fence | freeze
+...............
+Example:
+...............
+rsc_ticket ticket-A_public-ip ticket-A: public-ip
+rsc_ticket ticket-A_bigdb ticket-A: bigdb loss-policy=fence
+rsc_ticket ticket-B_storage ticket-B: drbd-a:Master drbd-b:Master
+...............
+
+
+[[cmdhelp_configure_property,set a cluster property]]
+==== `property`
+
+Set the cluster (+crm_config+) options.
+
+Usage:
+...............
+property [$id=<set_id>] [rule ...] <option>=<value> [<option>=<value> ...]
+...............
+Example:
+...............
+property stonith-enabled=true
+property rule date spec years=2014 stonith-enabled=false
+...............
+
+[[cmdhelp_configure_rsc_defaults,set resource defaults]]
+==== `rsc_defaults`
+
+Set defaults for the resource meta attributes.
+
+Usage:
+...............
+rsc_defaults [$id=<set_id>] [rule ...] <option>=<value> [<option>=<value> ...]
+...............
+Example:
+...............
+rsc_defaults failure-timeout=3m
+...............
+
+[[cmdhelp_configure_fencing_topology,node fencing order]]
+==== `fencing_topology`
+
+If multiple fencing (stonith) devices are available capable of
+fencing a node, their order may be specified by +fencing_topology+.
+The order is specified per node.
+
+Stonith resources can be separated by +,+ in which case all of
+them need to succeed. If they fail, the next stonith resource (or
+set of resources) is used. In other words, use comma to separate
+resources which all need to succeed and whitespace for serial
+order. It is not allowed to use whitespace around comma.
+
+If the node is left out, the order is used for all nodes.
+That should reduce the configuration size in some stonith setups.
+
+Usage:
+...............
+fencing_topology stonith_resources [stonith_resources ...]
+fencing_topology fencing_order [fencing_order ...]
+
+fencing_order :: <node>: stonith_resources [stonith_resources ...]
+
+stonith_resources :: <rsc>[,<rsc>...]
+...............
+Example:
+...............
+fencing_topology poison-pill power
+fencing_topology \
+    node-a: poison-pill power
+    node-b: ipmi serial
+...............
+
+[[cmdhelp_configure_role,define role access rights]]
+==== `role`
+
+An ACL role is a set of rules which describe access rights to
+CIB. Rules consist of an access right +read+, +write+, or +deny+
+and a specification denoting part of the configuration to which
+the access right applies. The specification can be an XPath or a
+combination of tag and id references. If an attribute is
+appended, then the specification applies only to that attribute
+of the matching element.
+
+There is a number of shortcuts for XPath specifications. The
++meta+, +params+, and +utilization+ shortcuts reference resource
+meta attributes, parameters, and utilization respectively. The
+`location` may be used to specify location constraints most of
+the time to allow resource `move` and `unmove` commands. The
+`property` references cluster properties. The `node` allows
+reading node attributes. +nodeattr+ and +nodeutil+ reference node
+attributes and node capacity (utilization). The `status` shortcut
+references the whole status section of the CIB. Read access to
+status is necessary for various monitoring tools such as
+`crm_mon(8)` (aka `crm status`).
+
+Usage:
+...............
+role <role-id> rule [rule ...]
+
+rule :: acl-right cib-spec [attribute:<attribute>]
+
+acl-right :: read | write | deny
+
+cib-spec :: xpath-spec | tag-ref-spec
+xpath-spec :: xpath:<xpath> | shortcut
+tag-ref-spec :: tag:<tag> | ref:<id> | tag:<tag> ref:<id>
+
+shortcut :: meta:<rsc>[:<attr>]
+        params:<rsc>[:<attr>]
+        utilization:<rsc>
+        location:<rsc>
+        property[:<attr>]
+        node[:<node>]
+        nodeattr[:<attr>]
+        nodeutil[:<node>]
+        status
+...............
+Example:
+...............
+role app1_admin \
+    write meta:app1:target-role \
+    write meta:app1:is-managed \
+    write location:app1 \
+    read ref:app1
+...............
+
+[[cmdhelp_configure_user,define user access rights]]
+==== `user`
+
+Users which normally cannot view or manage cluster configuration
+can be allowed access to parts of the CIB. The access is defined
+by a set of +read+, +write+, and +deny+ rules as in role
+definitions or by referencing roles. The latter is considered
+best practice.
+
+Usage:
+...............
+user <uid> {roles|rules}
+
+roles :: role:<role-ref> [role:<role-ref> ...]
+rules :: rule [rule ...]
+...............
+Example:
+...............
+user joe \
+    role:app1_admin \
+    role:read_all
+...............
+
+[[cmdhelp_configure_acl_target,Define target access rights]]
+==== `acl_target`
+
+Defines an ACL target.
+
+Usage:
+................
+acl_target <tid> [<role> ...]
+................
+Example:
+................
+acl_target joe resource_admin constraint_editor
+................
+
+[[cmdhelp_configure_acl_group,Define group access rights]]
+==== `acl_group`
+
+Defines an ACL group.
+
+Usage:
+................
+acl_group <gid> [<role> ...]
+................
+Example:
+................
+acl_group hacluster operator
+................
+
+
+[[cmdhelp_configure_op_defaults,set resource operations defaults]]
+==== `op_defaults`
+
+Set defaults for the operations meta attributes.
+
+Usage:
+...............
+op_defaults [$id=<set_id>] [rule ...] <option>=<value> [<option>=<value> ...]
+...............
+Example:
+...............
+op_defaults record-pending=true
+...............
+
+[[cmdhelp_configure_tag,Define resource tags]]
+==== `tag`
+
+Define a resource tag. A tag is an id referring to one or more
+resources, without implying any constraints between the tagged
+resources. This can be useful for grouping conceptually related
+resources.
+
+Usage:
+...............
+tag <tag-name>: <rsc> [<rsc> ...]
+...............
+Example:
+...............
+tag web: p-webserver p-vip
+...............
+
+
+[[cmdhelp_configure_schema,set or display current CIB RNG schema]]
+==== `schema`
+
+CIB's content is validated by a RNG schema. Pacemaker supports
+several, depending on version. At least the following schemas are
+accepted by `crmsh`:
+
+* +pacemaker-1.0+
+* +pacemaker-1.1+
+* +pacemaker-1.2+
+* +pacemaker-1.3+
+* +pacemaker-2.0+
+
+Use this command to display or switch to another RNG schema.
+
+Usage:
+...............
+schema [<schema>]
+...............
+Example:
+...............
+schema pacemaker-1.1
+...............
+
+[[cmdhelp_configure_show,display CIB objects]]
+==== `show`
+
+The `show` command displays objects. It may display all objects
+or a set of objects. The user may also choose to see only objects
+which were changed.
+
+Optionally, the XML code may be displayed instead of the CLI
+representation by passing `xml` as the first argument.
+
+To show all objects of a certain type, use the +type:+ prefix.
+
+Usage:
+...............
+show [xml] [<id> ...]
+show [xml] changed
+...............
+
+Example:
+...............
+show webapp
+show type:primitive
+show xml type:node
+...............
+
+[[cmdhelp_configure_edit,edit CIB objects]]
+==== `edit`
+
+This command invokes the editor with the object description. As
+with the `show` command, the user may choose to edit all objects
+or a set of objects.
+
+If the user insists, he or she may edit the XML edition of the
+object. If you do that, don't modify any id attributes.
+
+Usage:
+...............
+edit [xml] [<id> ...]
+edit [xml] changed
+...............
+
+.Note on renaming element ids
+****************************
+The edit command sometimes cannot properly handle modifying
+element ids. In particular for elements which belong to group or
+ms resources. Group and ms resources themselves also cannot be
+renamed. Please use the `rename` command instead.
+****************************
+
+[[cmdhelp_configure_filter,filter CIB objects]]
+==== `filter`
+
+This command filters the given CIB elements through an external
+program. The program should accept input on `stdin` and send
+output to `stdout` (the standard UNIX filter conventions). As
+with the `show` command, the user may choose to filter all or
+just a subset of elements.
+
+It is possible to filter the XML representation of objects, but
+probably not as useful as the configuration language. The
+presentation is somewhat different from what would be displayed
+by the `show` command---each element is shown on a single line,
+i.e. there are no backslashes and no other embelishments.
+
+Don't forget to put quotes around the filter if it contains
+spaces.
+
+Usage:
+...............
+filter <prog> [xml] [<id> ...]
+filter <prog> [xml] changed
+...............
+Examples:
+...............
+filter "sed '/^primitive/s/target-role=[^ ]*//'"
+# crm configure filter "sed '/^primitive/s/target-role=[^ ]*//'"
+crm configure <<END
+  filter "sed '/threshold=\"1\"/s/=\"1\"/=\"0\"/g'"
+END
+...............
+
+.Note on quotation marks
+**************************
+Filter commands which feature a blend of quotation marks can be
+difficult to get right, especially when used directly from bash, since
+bash does its own quotation parsing. In these cases, it can be easier
+to supply the filter command as standard input. See the last example
+above.
+**************************
+
+[[cmdhelp_configure_delete,delete CIB objects]]
+==== `delete`
+
+Delete one or more objects. If an object to be deleted belongs to
+a container object, such as a group, and it is the only resource
+in that container, then the container is deleted as well. Any
+related constraints are removed as well.
+
+If the object is a started resource, it will not be deleted unless the
++--force+ flag is passed to the command, or the +force+ option is set.
+
+Usage:
+...............
+delete [--force] <id> [<id>...]
+...............
+
+[[cmdhelp_configure_default-timeouts,set timeouts for operations to minimums from the meta-data]]
+==== `default-timeouts`
+
+This command takes the timeouts from the actions section of the
+resource agent meta-data and sets them for the operations of the
+primitive.
+
+Usage:
+...............
+default-timeouts <id> [<id>...]
+...............
+
+.Note on `default-timeouts`
+****************************
+You may be happy using this, but your applications may not. And
+it will tell you so at the worst possible moment. You have been
+warned.
+****************************
+
+[[cmdhelp_configure_rename,rename a CIB object]]
+==== `rename`
+
+Rename an object. It is recommended to use this command to rename
+a resource, because it will take care of updating all related
+constraints and a parent resource. Changing ids with the edit
+command won't have the same effect.
+
+If you want to rename a resource, it must be in the stopped state.
+
+Usage:
+...............
+rename <old_id> <new_id>
+...............
+
+[[cmdhelp_configure_modgroup,modify group]]
+==== `modgroup`
+
+Add or remove primitives in a group. The `add` subcommand appends
+the new group member by default. Should it go elsewhere, there
+are `after` and `before` clauses.
+
+Usage:
+...............
+modgroup <id> add <id> [after <id>|before <id>]
+modgroup <id> remove <id>
+...............
+Examples:
+...............
+modgroup share1 add storage2 before share1-fs
+...............
+
+[[cmdhelp_configure_refresh,refresh from CIB]]
+==== `refresh`
+
+Refresh the internal structures from the CIB. All changes made
+during this session are lost.
+
+Usage:
+...............
+refresh
+...............
+
+[[cmdhelp_configure_erase,erase the CIB]]
+==== `erase`
+
+The `erase` clears all configuration. Apart from nodes. To remove
+nodes, you have to specify an additional keyword `nodes`.
+
+Note that removing nodes from the live cluster may have some
+strange/interesting/unwelcome effects.
+
+Usage:
+...............
+erase [nodes]
+...............
+
+[[cmdhelp_configure_ptest,show cluster actions if changes were committed]]
+==== `ptest` (`simulate`)
+
+Show PE (Policy Engine) motions using `ptest(8)` or
+`crm_simulate(8)`.
+
+A CIB is constructed using the current user edited configuration
+and the status from the running CIB. The resulting CIB is run
+through `ptest` (or `crm_simulate`) to show changes which would
+happen if the configuration is committed.
+
+The status section may be loaded from another source and modified
+using the <<cmdhelp_cibstatus,`cibstatus`>> level commands. In that case, the
+`ptest` command will issue a message informing the user that the
+Policy Engine graph is not calculated based on the current status
+section and therefore won't show what would happen to the
+running but some imaginary cluster.
+
+If you have graphviz installed and X11 session, `dotty(1)` is run
+to display the changes graphically.
+
+Add a string of +v+ characters to increase verbosity. `ptest`
+can also show allocation scores. +utilization+ turns on
+information about the remaining capacity of nodes. With the
++actions+ option, `ptest` will print all resource actions.
+
+The `ptest` program has been replaced by `crm_simulate` in newer
+Pacemaker versions. In some installations both could be
+installed. Use `simulate` to enfore using `crm_simulate`.
+
+Usage:
+...............
+ptest [nograph] [v...] [scores] [actions] [utilization]
+...............
+Examples:
+...............
+ptest scores
+ptest vvvvv
+simulate actions
+...............
+
+[[cmdhelp_configure_rsctest,test resources as currently configured]]
+==== `rsctest`
+
+Test resources with current resource configuration. If no nodes
+are specified, tests are run on all known nodes.
+
+The order of resources is significant: it is assumed that later
+resources depend on earlier ones.
+
+If a resource is multi-state, it is assumed that the role on
+which later resources depend is master.
+
+Tests are run sequentially to prevent running the same resource
+on two or more nodes. Tests are carried out only if none of the
+specified nodes currently run any of the specified resources.
+However, it won't verify whether resources run on the other
+nodes.
+
+Superuser privileges are obviously required: either run this as
+root or setup the `sudoers` file appropriately.
+
+Note that resource testing may take some time.
+
+Usage:
+...............
+rsctest <rsc_id> [<rsc_id> ...] [<node_id> ...]
+...............
+Examples:
+...............
+rsctest my_ip websvc
+rsctest websvc nodeB
+...............
+
+[[cmdhelp_configure_cib,CIB shadow management]]
+=== `cib` (shadow CIBs)
+
+This level is for management of shadow CIBs. It is available at
+the `configure` level to enable saving intermediate changes to a
+shadow CIB instead of to the live cluster. This short excerpt
+shows how:
+...............
+crm(live)configure# cib new test-2
+INFO: test-2 shadow CIB created
+crm(test-2)configure# commit
+...............
+Note how the current CIB in the prompt changed from +live+ to
++test-2+ after issuing the `cib new` command. See also the
+<<cmdhelp_cib,CIB shadow management>> for more information.
+
+[[cmdhelp_configure_cibstatus,CIB status management and editing]]
+==== `cibstatus`
+
+Enter edit and manage the CIB status section level. See the
+<<cmdhelp_cibstatus,CIB status management section>>.
+
+[[cmdhelp_configure_template,edit and import a configuration from a template]]
+==== `template`
+
+The specified template is loaded into the editor. It's up to the
+user to make a good CRM configuration out of it. See also the
+<<cmdhelp_template,template section>>.
+
+Usage:
+...............
+template [xml] url
+...............
+Example:
+...............
+template two-apaches.txt
+...............
+
+[[cmdhelp_configure_commit,commit the changes to the CIB]]
+==== `commit`
+
+Commit the current configuration to the CIB in use. As noted
+elsewhere, commands in a configure session don't have immediate
+effect on the CIB. All changes are applied at one point in time,
+either using `commit` or when the user leaves the configure
+level. In case the CIB in use changed in the meantime, presumably
+by somebody else, the crm shell will refuse to apply the changes.
+
+If you know that it's fine to still apply them, add +force+ to the
+command line.
+
+Usage:
+...............
+commit [force]
+...............
+
+[[cmdhelp_configure_verify,verify the CIB with crm_verify]]
+==== `verify`
+
+Verify the contents of the CIB which would be committed.
+
+Usage:
+...............
+verify
+...............
+
+[[cmdhelp_configure_upgrade,upgrade the CIB to version 1.0]]
+==== `upgrade`
+
+If you get the `CIB not supported` error, which typically means
+that the current CIB version is coming from the older release,
+you may try to upgrade it to the latest revision. The command
+to perform the upgrade is:
+...............
+# cibadmin --upgrade --force
+...............
+
+If we don't recognize the current CIB as the old one, but you're
+sure that it is, you may force the command.
+
+Usage:
+...............
+upgrade [force]
+...............
+
+[[cmdhelp_configure_save,save the CIB to a file]]
+==== `save`
+
+Save the current configuration to a file. Optionally, as XML. Use
++-+ instead of file name to write the output to `stdout`.
+
+Usage:
+...............
+save [xml] <file>
+...............
+Example:
+...............
+save myfirstcib.txt
+...............
+
+[[cmdhelp_configure_load,import the CIB from a file]]
+==== `load`
+
+Load a part of configuration (or all of it) from a local file or
+a network URL. The +replace+ method replaces the current
+configuration with the one from the source. The +update+ tries to
+import the contents into the current configuration.
+The file may be a CLI file or an XML file.
+
+Usage:
+...............
+load [xml] <method> URL
+
+method :: replace | update
+...............
+Example:
+...............
+load xml update myfirstcib.xml
+load xml replace http://storage.big.com/cibs/bigcib.xml
+...............
+
+[[cmdhelp_configure_graph,generate a directed graph]]
+==== `graph`
+
+Create a graphviz graphical layout from the current cluster
+configuration.
+
+Currently, only `dot` (directed graph) is supported. It is
+essentially a visualization of resource ordering.
+
+The graph may be saved to a file which can be used as source for
+various graphviz tools (by default it is displayed in the user's
+X11 session). Optionally, by specifying the format, one can also
+produce an image instead.
+
+For more or different graphviz attributes, it is possible to save
+the default set of attributes to an ini file. If this file exists
+it will always override the builtin settings. The +exportsettings+
+subcommand also prints the location of the ini file.
+
+Usage:
+...............
+graph [<gtype> [<file> [<img_format>]]]
+graph exportsettings
+
+gtype :: dot
+img_format :: `dot` output format (see the +-T+ option)
+...............
+Example:
+...............
+graph dot
+graph dot clu1.conf.dot
+graph dot clu1.conf.svg svg
+...............
+
+[[cmdhelp_configure_xml,raw xml]]
+==== `xml`
+
+Even though we promissed no xml, it may happen, but hopefully
+very very seldom, that an element from the CIB cannot be rendered
+in the configuration language. In that case, the element will be
+shown as raw xml, prefixed by this command. That element can then
+be edited like any other. If the shell finds out that after the
+change it can digest it, then it is going to be converted into
+the normal configuration language. Otherwise, there is no need to
+use `xml` for configuration.
+
+Usage:
+...............
+xml <xml>
+...............
+
+[[cmdhelp_template,edit and import a configuration from a template]]
+=== `template`
+
+User may be assisted in the cluster configuration by templates
+prepared in advance. Templates consist of a typical ready
+configuration which may be edited to suit particular user needs.
+
+This command enters a template level where additional commands
+for configuration/template management are available.
+
+[[cmdhelp_template_new,create a new configuration from templates]]
+==== `new`
+
+Create a new configuration from one or more templates. Note that
+configurations and templates are kept in different places, so it
+is possible to have a configuration name equal a template name.
+
+If you already know which parameters are required, you can set
+them directly on the command line.
+
+The parameter name +id+ is set by default to the name of the
+configuration.
+
+Usage:
+...............
+new <config> <template> [<template> ...] [params name=value ...]
+...............
+
+Example:
+...............
+new vip virtual-ip
+new bigfs ocfs2 params device=/dev/sdx8 directory=/bigfs
+...............
+
+[[cmdhelp_template_load,load a configuration]]
+==== `load`
+
+Load an existing configuration. Further `edit`, `show`, and
+`apply` commands will refer to this configuration.
+
+Usage:
+...............
+load <config>
+...............
+
+[[cmdhelp_template_edit,edit a configuration]]
+==== `edit`
+
+Edit current or given configuration using your favourite editor.
+
+Usage:
+...............
+edit [<config>]
+...............
+
+[[cmdhelp_template_delete,delete a configuration]]
+==== `delete`
+
+Remove a configuration. The loaded (active) configuration may be
+removed by force.
+
+Usage:
+...............
+delete <config> [force]
+...............
+
+[[cmdhelp_template_list,list configurations/templates]]
+==== `list`
+
+List existing configurations or templates.
+
+Usage:
+...............
+list [templates]
+...............
+
+[[cmdhelp_template_apply,process and apply the current configuration to the current CIB]]
+==== `apply`
+
+Copy the current or given configuration to the current CIB. By
+default, the CIB is replaced, unless the method is set to
+"update".
+
+Usage:
+...............
+apply [<method>] [<config>]
+
+method :: replace | update
+...............
+
+[[cmdhelp_template_show,show the processed configuration]]
+==== `show`
+
+Process the current or given configuration and display the result.
+
+Usage:
+...............
+show [<config>]
+...............
+
+[[cmdhelp_cibstatus,CIB status management and editing]]
+=== `cibstatus`
+
+The `status` section of the CIB keeps the current status of nodes
+and resources. It is modified _only_ on events, i.e. when some
+resource operation is run or node status changes. For obvious
+reasons, the CRM has no user interface with which it is possible
+to affect the status section. From the user's point of view, the
+status section is essentially a read-only part of the CIB. The
+current status is never even written to disk, though it is
+available in the PE (Policy Engine) input files which represent
+the history of cluster motions. The current status may be read
+using the +cibadmin -Q+ command.
+
+It may sometimes be of interest to see how status changes would
+affect the Policy Engine. The set of `cibstatus` level commands
+allow the user to load status sections from various sources and
+then insert or modify resource operations or change nodes' state.
+
+The effect of those changes may then be observed by running the
+<<cmdhelp_configure_ptest,`ptest`>> command at the `configure` level
+or `simulate` and `run` commands at this level. The `ptest`
+runs with the user edited CIB whereas the latter two commands
+run with the CIB which was loaded along with the status section.
+
+The `simulate` and `run` commands as well as all status
+modification commands are implemented using `crm_simulate(8)`.
+
+[[cmdhelp_cibstatus_load,load the CIB status section]]
+==== `load`
+
+Load a status section from a file, a shadow CIB, or the running
+cluster. By default, the current (+live+) status section is
+modified. Note that if the +live+ status section is modified it
+is not going to be updated if the cluster status changes, because
+that would overwrite the user changes. To make `crm` drop changes
+and resume use of the running cluster status, run +load live+.
+
+All CIB shadow configurations contain the status section which is
+a snapshot of the status section taken at the time the shadow was
+created. Obviously, this status section doesn't have much to do
+with the running cluster status, unless the shadow CIB has just
+been created. Therefore, the `ptest` command by default uses the
+running cluster status section.
+
+Usage:
+...............
+load {<file>|shadow:<cib>|live}
+...............
+Example:
+...............
+load bug-12299.xml
+load shadow:test1
+...............
+
+[[cmdhelp_cibstatus_save,save the CIB status section]]
+==== `save`
+
+The current internal status section with whatever modifications
+were performed can be saved to a file or shadow CIB.
+
+If the file exists and contains a complete CIB, only the status
+section is going to be replaced and the rest of the CIB will
+remain intact. Otherwise, the current user edited configuration
+is saved along with the status section.
+
+Note that all modifications are saved in the source file as soon
+as they are run.
+
+Usage:
+...............
+save [<file>|shadow:<cib>]
+...............
+Example:
+...............
+save bug-12299.xml
+...............
+
+[[cmdhelp_cibstatus_origin,display origin of the CIB status section]]
+==== `origin`
+
+Show the origin of the status section currently in use. This
+essentially shows the latest `load` argument.
+
+Usage:
+...............
+origin
+...............
+
+[[cmdhelp_cibstatus_show,show CIB status section]]
+==== `show`
+
+Show the current status section in the XML format. Brace yourself
+for some unreadable output. Add +changed+ option to get a human
+readable output of all changes.
+
+Usage:
+...............
+show [changed]
+...............
+
+[[cmdhelp_cibstatus_node,change node status]]
+==== `node`
+
+Change the node status. It is possible to throw a node out of
+the cluster, make it a member, or set its state to unclean.
+
++online+:: Set the +node_state+ `crmd` attribute to +online+
+and the +expected+ and +join+ attributes to +member+. The effect
+is that the node becomes a cluster member.
+
++offline+:: Set the +node_state+ `crmd` attribute to +offline+
+and the +expected+ attribute to empty. This makes the node
+cleanly removed from the cluster.
+
++unclean+:: Set the +node_state+ `crmd` attribute to +offline+
+and the +expected+ attribute to +member+. In this case the node
+has unexpectedly disappeared.
+
+Usage:
+...............
+node <node> {online|offline|unclean}
+...............
+Example:
+...............
+node xen-b unclean
+...............
+
+[[cmdhelp_cibstatus_op,edit outcome of a resource operation]]
+==== `op`
+
+Edit the outcome of a resource operation. This way you can
+tell CRM that it ran an operation and that the resource agent
+returned certain exit code. It is also possible to change the
+operation's status. In case the operation status is set to
+something other than +done+, the exit code is effectively
+ignored.
+
+Usage:
+...............
+op <operation> <resource> <exit_code> [<op_status>] [<node>]
+
+operation :: probe | monitor[:<n>] | start | stop |
+   promote | demote | notify | migrate_to | migrate_from
+exit_code :: <rc> | success | generic | args |
+   unimplemented | perm | installed | configured | not_running |
+   master | failed_master
+op_status :: pending | done | cancelled | timeout | notsupported | error
+
+n :: the monitor interval in seconds; if omitted, the first
+   recurring operation is referenced
+rc :: numeric exit code in range 0..9
+...............
+Example:
+...............
+op start d1 xen-b generic
+op start d1 xen-b 1
+op monitor d1 xen-b not_running
+op stop d1 xen-b 0 timeout
+...............
+
+[[cmdhelp_cibstatus_quorum,set the quorum]]
+==== `quorum`
+
+Set the quorum value.
+
+Usage:
+...............
+quorum <bool>
+...............
+Example:
+...............
+quorum false
+...............
+
+[[cmdhelp_cibstatus_ticket,manage tickets]]
+==== `ticket`
+
+Modify the ticket status. Tickets can be granted and revoked.
+Granted tickets could be activated or put in standby.
+
+Usage:
+...............
+ticket <ticket> {grant|revoke|activate|standby}
+...............
+Example:
+...............
+ticket ticketA grant
+...............
+
+[[cmdhelp_cibstatus_run,run policy engine]]
+==== `run`
+
+Run the policy engine with the edited status section.
+
+Add a string of +v+ characters to increase verbosity. Specify
++scores+ to see allocation scores also. +utilization+ turns on
+information about the remaining capacity of nodes.
+
+If you have graphviz installed and X11 session, `dotty(1)` is run
+to display the changes graphically.
+
+Usage:
+...............
+run [nograph] [v...] [scores] [utilization]
+...............
+Example:
+...............
+run
+...............
+
+[[cmdhelp_cibstatus_simulate,simulate cluster transition]]
+==== `simulate`
+
+Run the policy engine with the edited status section and simulate
+the transition.
+
+Add a string of +v+ characters to increase verbosity. Specify
++scores+ to see allocation scores also. +utilization+ turns on
+information about the remaining capacity of nodes.
+
+If you have graphviz installed and X11 session, `dotty(1)` is run
+to display the changes graphically.
+
+Usage:
+...............
+simulate [nograph] [v...] [scores] [utilization]
+...............
+Example:
+...............
+simulate
+...............
+
+[[cmdhelp_assist,Configuration assistant]]
+=== `assist`
+
+The `assist` sublevel is a collection of helper
+commands that create or modify resources and
+constraints, to simplify the creation of certain
+configurations.
+
+For more information on individual commands, see
+the help text for those commands.
+
+[[cmdhelp_assist_weak-bond,Create a weak bond between resources]]
+==== `weak-bond`
+
+A colocation between a group of resources says that the resources
+should be located together, but it also means that those resources are
+dependent on each other. If one of the resources fails, the others
+will be restarted.
+
+If this is not desired, it is possible to circumvent: By placing the
+resources in a non-sequential set and colocating the set with a dummy
+resource which is not monitored, the resources will be placed together
+but will have no further dependency on each other.
+
+This command creates both the constraint and the dummy resource needed
+for such a colocation.
+
+Usage:
+........
+weak-bond resource-1 resource-2
+........
+
+[[cmdhelp_assist_template,Create template for primitives]]
+==== `template`
+
+This command takes a list of primitives as argument, and creates a new
+`rsc_template` for these primitives. It can only do this if the
+primitives do not already share a template and are of the same type.
+
+Usage:
+........
+template primitive-1 primitive-2 primitive-3
+........
+
+[[cmdhelp_history,cluster history]]
+=== `history`
+
+Examining Pacemaker's history is a particularly involved task.
+The number of subsystems to be considered, the complexity of the
+configuration, and the set of various information sources, most
+of which are not exactly human readable, keep analyzing resource
+or node problems accessible to only the most knowledgeable. Or,
+depending on the point of view, to the most persistent. The
+following set of commands has been devised in hope to make
+cluster history more accessible.
+
+Of course, looking at _all_ history could be time consuming
+regardless of how good tools at hand are. Therefore, one should
+first say which period he or she wants to analyze. If not
+otherwise specified, the last hour is considered. Logs and other
+relevant information is collected using `hb_report`. Since this
+process takes some time and we always need fresh logs,
+information is refreshed in a much faster way using `pssh(1)`. If
++python-pssh+ is not found on the system, examining live cluster
+is still possible though not as comfortable.
+
+Apart from examining live cluster, events may be retrieved from a
+report generated by `hb_report` (see also the +-H+ option). In
+that case we assume that the period stretching the whole report
+needs to be investigated. Of course, it is still possible to
+further reduce the time range.
+
+If you think you may have found a bug or just need clarification
+from developers or your support, the `session pack` command can
+help create a report.
+
+Example:
+...............
+crm(live)history# timeframe "Jul 18 12:00" "Jul 18 12:30"
+crm(live)history# session save strange_restart
+crm(live)history# session pack
+Report saved in .../strange_restart.tar.bz2
+crm(live)history#
+...............
+
+In order to reduce report size and allow developers to
+concentrate on the issue, you should beforehand limit the time
+frame. Giving a meaningful session name helps too.
+
+[[cmdhelp_history_info,Cluster information summary]]
+==== `info`
+
+The `info` command provides a summary of the information source, which
+can be either a live cluster snapshot or a previously generated
+report.
+
+Usage:
+...............
+info
+...............
+Example:
+...............
+info
+...............
+
+[[cmdhelp_history_latest,show latest news from the cluster]]
+==== `latest`
+
+The `latest` command shows a bit of recent history, more
+precisely whatever happened since the last cluster change (the
+latest transition). If the transition is running, the shell will
+first wait until it finishes.
+
+Usage:
+...............
+latest
+...............
+Example:
+...............
+latest
+...............
+
+[[cmdhelp_history_limit,limit timeframe to be examined]]
+==== `limit` (`timeframe`)
+
+All history commands look at events within certain period. It
+defaults to the last hour for the live cluster source. There is
+no limit for the `hb_report` source. Use this command to set the
+timeframe.
+
+The time period is parsed by the dateutil python module. It
+covers wide range of date formats. For instance:
+
+- 3:00      (today at 3am)
+- 15:00     (today at 3pm)
+- 2010/9/1 2pm  (September 1st 2010 at 2pm)
+
+We won't bother to give definition of the time specification in
+usage below. Either use common sense or read the
+http://labix.org/python-dateutil[dateutil] documentation.
+
+If dateutil is not available, then the time is parsed using
+strptime and only the kind as printed by `date(1)` is allowed:
+
+- Tue Sep 15 20:46:27 CEST 2010
+
+Usage:
+...............
+limit [<from_time> [<to_time>]]
+...............
+Examples:
+...............
+limit 10:15
+limit 15h22m 16h
+limit "Sun 5 20:46" "Sun 5 22:00"
+...............
+
+[[cmdhelp_history_source,set source to be examined]]
+==== `source`
+
+Events to be examined can come from the current cluster or from a
+`hb_report` report. This command sets the source. `source live`
+sets source to the running cluster and system logs. If no source
+is specified, the current source information is printed.
+
+In case a report source is specified as a file reference, the file
+is going to be unpacked in place where it resides. This directory
+is not removed on exit.
+
+Usage:
+...............
+source [<dir>|<file>|live]
+...............
+Examples:
+...............
+source live
+source /tmp/customer_case_22.tar.bz2
+source /tmp/customer_case_22
+source
+...............
+
+[[cmdhelp_history_refresh,refresh live report]]
+==== `refresh`
+
+This command makes sense only for the +live+ source and makes
+`crm` collect the latest logs and other relevant information from
+the logs. If you want to make a completely new report, specify
++force+.
+
+Usage:
+...............
+refresh [force]
+...............
+
+[[cmdhelp_history_detail,set the level of detail shown]]
+==== `detail`
+
+How much detail to show from the logs.
+
+Usage:
+...............
+detail <detail_level>
+
+detail_level :: small integer (defaults to 0)
+...............
+Example:
+...............
+detail 1
+...............
+
+[[cmdhelp_history_setnodes,set the list of cluster nodes]]
+==== `setnodes`
+
+In case the host this program runs on is not part of the cluster,
+it is necessary to set the list of nodes.
+
+Usage:
+...............
+setnodes node <node> [<node> ...]
+...............
+Example:
+...............
+setnodes node_a node_b
+...............
+
+[[cmdhelp_history_resource,resource events]]
+==== `resource`
+
+Show actions and any failures that happened on all specified
+resources on all nodes. Normally, one gives resource names as
+arguments, but it is also possible to use extended regular
+expressions. Note that neither groups nor clones or master/slave
+names are ever logged. The resource command is going to expand
+all of these appropriately, so that clone instances or resources
+which are part of a group are shown.
+
+Usage:
+...............
+resource <rsc> [<rsc> ...]
+...............
+Example:
+...............
+resource bigdb public_ip
+resource my_.*_db2
+resource ping_clone
+...............
+
+[[cmdhelp_history_node,node events]]
+==== `node`
+
+Show important events that happened on a node. Important events
+are node lost and join, standby and online, and fence. Use either
+node names or extended regular expressions.
+
+Usage:
+...............
+node <node> [<node> ...]
+...............
+Example:
+...............
+node node1
+...............
+
+[[cmdhelp_history_log,log content]]
+==== `log`
+
+Show messages logged on one or more nodes. Leaving out a node
+name produces combined logs of all nodes. Messages are sorted by
+time and, if the terminal emulations supports it, displayed in
+different colours depending on the node to allow for easier
+reading.
+
+The sorting key is the timestamp as written by syslog which
+normally has the maximum resolution of one second. Obviously,
+messages generated by events which share the same timestamp may
+not be sorted in the same way as they happened. Such close events
+may actually happen fairly often.
+
+Usage:
+...............
+log [<node> [<node> ...] ]
+...............
+Example:
+...............
+log node-a
+...............
+
+[[cmdhelp_history_exclude,exclude log messages]]
+==== `exclude`
+
+If a log is infested with irrelevant messages, those messages may
+be excluded by specifying a regular expression. The regular
+expressions used are Python extended. This command is additive.
+To drop all regular expressions, use +exclude clear+. Run
+`exclude` only to see the current list of regular expressions.
+Excludes are saved along with the history sessions.
+
+Usage:
+...............
+exclude [<regex>|clear]
+...............
+Example:
+...............
+exclude kernel.*ocfs2
+...............
+
+[[cmdhelp_history_peinputs,list or get PE input files]]
+==== `peinputs`
+
+Every event in the cluster results in generating one or more
+Policy Engine (PE) files. These files describe future motions of
+resources. The files are listed as full paths in the current
+report directory. Add +v+ to also see the creation time stamps.
+
+Usage:
+...............
+peinputs [{<range>|<number>} ...] [v]
+
+range :: <n1>:<n2>
+...............
+Example:
+...............
+peinputs
+peinputs 440:444 446
+peinputs v
+...............
+
+[[cmdhelp_history_transition,show transition]]
+==== `transition`
+
+This command will print actions planned by the PE and run
+graphviz (`dotty`) to display a graphical representation of the
+transition. Of course, for the latter an X11 session is required.
+This command invokes `ptest(8)` in background.
+
+The +showdot+ subcommand runs graphviz (`dotty`) to display a
+graphical representation of the +.dot+ file which has been
+included in the report. Essentially, it shows the calculation
+produced by `pengine` which is installed on the node where the
+report was produced. In optimal case this output should not
+differ from the one produced by the locally installed `pengine`.
+
+The `log` subcommand shows the full log for the duration of the
+transition.
+
+A transition can also be saved to a CIB shadow for further
+analysis or use with `cib` or `configure` commands (use the
+`save` subcommand). The shadow file name defaults to the name of
+the PE input file.
+
+If the PE input file number is not provided, it defaults to the
+last one, i.e. the last transition. The last transition can also
+be referenced with number 0. If the number is negative, then the
+corresponding transition relative to the last one is chosen.
+
+If there are warning and error PE input files or different nodes
+were the DC in the observed timeframe, it may happen that PE
+input file numbers collide. In that case provide some unique part
+of the path to the file.
+
+After the `ptest` output, logs about events that happened during
+the transition are printed.
+
+Usage:
+...............
+transition [<number>|<index>|<file>] [nograph] [v...] [scores] [actions] [utilization]
+transition showdot [<number>|<index>|<file>]
+transition log [<number>|<index>|<file>]
+transition save [<number>|<index>|<file> [name]]
+...............
+Examples:
+...............
+transition
+transition 444
+transition -1
+transition pe-error-3.bz2
+transition node-a/pengine/pe-input-2.bz2
+transition showdot 444
+transition log
+transition save 0 enigma-22
+...............
+
+[[cmdhelp_history_show,show status or configuration of the PE input file]]
+==== `show`
+
+Every transition is saved as a PE file. Use this command to
+render that PE file either as configuration or status. The
+configuration output is the same as `crm configure show`.
+
+Usage:
+...............
+show <pe> [status]
+
+pe :: <number>|<index>|<file>|live
+...............
+Examples:
+...............
+show 2066
+show pe-input-2080.bz2 status
+...............
+
+[[cmdhelp_history_graph,generate a directed graph from the PE file]]
+==== `graph`
+
+Create a graphviz graphical layout from the PE file (the
+transition). Every transition contains the cluster configuration
+which was active at the time. See also <<cmdhelp_configure_graph,generate a directed graph
+from configuration>>.
+
+Usage:
+...............
+graph <pe> [<gtype> [<file> [<img_format>]]]
+
+gtype :: dot
+img_format :: `dot` output format (see the +-T+ option)
+...............
+Example:
+...............
+graph -1
+graph 322 dot clu1.conf.dot
+graph 322 dot clu1.conf.svg svg
+...............
+
+[[cmdhelp_history_diff,cluster states/transitions difference]]
+==== `diff`
+
+A transition represents a change in cluster configuration or
+state. Use `diff` to see what has changed between two
+transitions.
+
+If you want to specify the current cluster configuration and
+status, use the string +live+.
+
+Normally, the first transition specified should be the one which
+is older, but we are not going to enforce that.
+
+Note that a single configuration update may result in more than
+one transition.
+
+Usage:
+...............
+diff <pe> <pe> [status] [html]
+
+pe :: <number>|<index>|<file>|live
+...............
+Examples:
+...............
+diff 2066 2067
+diff pe-input-2080.bz2 live status
+...............
+
+[[cmdhelp_history_wdiff,cluster states/transitions difference]]
+==== `wdiff`
+
+A transition represents a change in cluster configuration or
+state. Use `wdiff` to see what has changed between two
+transitions as word differences on a line-by-line basis.
+
+If you want to specify the current cluster configuration and
+status, use the string +live+.
+
+Normally, the first transition specified should be the one which
+is older, but we are not going to enforce that.
+
+Note that a single configuration update may result in more than
+one transition.
+
+Usage:
+...............
+wdiff <pe> <pe> [status]
+
+pe :: <number>|<index>|<file>|live
+...............
+Examples:
+...............
+wdiff 2066 2067
+wdiff pe-input-2080.bz2 live status
+...............
+
+[[cmdhelp_history_session,manage history sessions]]
+==== `session`
+
+Sometimes you may want to get back to examining a particular
+history period or bug report. In order to make that easier, the
+current settings can be saved and later retrieved.
+
+If the current history being examined is coming from a live
+cluster the logs, PE inputs, and other files are saved too,
+because they may disappear from nodes. For the existing reports
+coming from `hb_report`, only the directory location is saved
+(not to waste space).
+
+A history session may also be packed into a tarball which can
+then be sent to support.
+
+Leave out subcommand to see the current session.
+
+Usage:
+...............
+session [{save|load|delete} <name> | pack [<name>] | update | list]
+...............
+Examples:
+...............
+session save bnc966622
+session load rsclost-2
+session list
+...............
+
+[[cmdhelp_root_report,Create cluster status report]]
+=== `report`
+
+Interface to a tool for creating a cluster report. A report is an
+archive containing log files, configuration files, system information
+and other relevant data for a given time period. This is a useful tool
+for collecting data to attach to bug reports, or for detecting the
+root cause of errors resulting in resource failover, for example.
+
+See `crmsh_hb_report(8)` for more details on arguments,
+or call `crm report -h`
+
+Usage:
+...............
+report -f {time|"cts:"testnum} [-t time] [-u user] [-l file]
+       [-n nodes] [-E files] [-p patt] [-L patt] [-e prog]
+       [-MSDZAVsvhd] [dest]
+...............
+
+Examples:
+...............
+report -f 2pm report_1
+report -f "2007/9/5 12:30" -t "2007/9/5 14:00" report_2
+report -f 1:00 -t 3:00 -l /var/log/cluster/ha-debug report_3
+report -f "09sep07 2:00" -u hbadmin report_4
+report -f 18:00 -p "usern.*" -p "admin.*" report_5
+report -f cts:133 ctstest_133
+...............
+
+=== `end` (`cd`, `up`)
+
+The `end` command ends the current level and the user moves to
+the parent level. This command is available everywhere.
+
+Usage:
+...............
+end
+...............
+
+=== `help`
+
+The `help` command prints help for the current level or for the
+specified topic (command). This command is available everywhere.
+
+Usage:
+...............
+help [<topic>]
+...............
+
+=== `quit` (`exit`, `bye`)
+
+Leave the program.
+
+BUGS
+----
+Even though all sensible configurations (and most of those that
+are not) are going to be supported by the crm shell, I suspect
+that it may still happen that certain XML constructs may confuse
+the tool. When that happens, please file a bug report.
+
+The crm shell will not try to update the objects it does not
+understand. Of course, it is always possible to edit such objects
+in the XML format.
+
+AUTHORS
+-------
+Dejan Muhamedagic, <dejan at suse.de>
+Kristoffer Gronlund <kgronlund at suse.com>
+and many OTHERS
+
+SEE ALSO
+--------
+crm_resource(8), crm_attribute(8), crm_mon(8), cib_shadow(8),
+ptest(8), dotty(1), crm_simulate(8), cibadmin(8)
+
+
+COPYING
+-------
+Copyright \(C) 2008-2013 Dejan Muhamedagic.
+Copyright \(C) 2013 Kristoffer Gronlund.
+
+Free use of this software is granted under the terms of the GNU General Public License (GPL).
+
+//////////////////////
+ vim:ts=4:sw=4:expandtab:
+//////////////////////
diff --git a/doc/website-v1/news.txt b/doc/website-v1/news.txt
new file mode 100644
index 0000000..2c2b505
--- /dev/null
+++ b/doc/website-v1/news.txt
@@ -0,0 +1,11 @@
+= News
+
+link:/news/2014-06-30-release-2_1[2014-06-30 09:00]
+
+:leveloffset: 1
+include::news/2014-06-30-release-2_1.txt[]
+:leveloffset: 0
+
+''''
+
+link:https://savannah.nongnu.org/news/?group_id=10890[Old News Archive]
diff --git a/doc/website-v1/news/2014-06-30-release-2_1.txt b/doc/website-v1/news/2014-06-30-release-2_1.txt
new file mode 100644
index 0000000..4c59a90
--- /dev/null
+++ b/doc/website-v1/news/2014-06-30-release-2_1.txt
@@ -0,0 +1,93 @@
+Announcing crmsh release 2.1
+============================
+:Author: Kristoffer Gronlund
+:Email: kgronlund at suse.com
+:Date: 2014-06-30 09:00
+
+Today we are proud to announce the release of `crmsh` version 2.1!
+This version primarily fixes all known issues found since the release
+of `crmsh` 2.0 in April, but also has some major new features.
+
+A massive thank you to everyone who has helped out with bug fixes,
+comments and contributions for this release!
+
+For a complete list of changes since the previous version, please
+refer to the changelog:
+
+* https://github.com/crmsh/crmsh/blob/2.1.0/ChangeLog
+
+Packages for several popular Linux distributions can be downloaded
+from the Stable repository at the OBS:
+
+* http://download.opensuse.org/repositories/network:/ha-clustering:/Stable/
+
+Archives of the tagged release:
+
+* https://github.com/crmsh/crmsh/archive/2.1.0.tar.gz
+* https://github.com/crmsh/crmsh/archive/2.1.0.zip
+
+Here are some of the highlights of this release:
+
+== Rule expressions in attribute lists
+
+One of the biggest features in this release is full support for rule
+expressions wherever the XML syntax allows them.
+
+Here is an example of using rule expressions in an attribute list in
+order to set the virtual IP of an IPAddr2 resource to a different
+value on a specific node.
+
+----
+primitive vip-on-node1 IPAddr2 \
+    rule 10: #uname eq node1 ip=10.0.0.5 \
+    rule 1: ip=10.0.0.6
+----
+
+== Tags in the CIB
+
+A new feature added to Pacemaker recently is tags. This is a way
+to refer to multiple resources at once without creating any
+colocation or ordering relationship between them. For example, you
+could add all resources related to the database to a db tag, and
+then stop or start them all with a single command.
+
+----
+tag db drbd:Master fs sql-db
+----
+
+It is also possible to refer to tags in constraints.
+
+== Wildcards in show/edit
+
+The configure show and edit commands can now use glob-style
+wildcards to refer to multiple resources:
+
+----
+configure edit db-*
+----
+
+== Nvpair references
+
+Sometimes, different resources name the same parameters with different
+names. For example, an IPAddr2 may have an ip parameter that should be
+the same as a web servers server_ip parameter. By using nvpair
+references, it is possible to configure the ip in a single location.
+
+Note that this is a new feature in Pacemaker 1.1.12 and up.
+
+----
+primitive vip IPAddr2 params $my-ip:ip=192.168.0.1
+primitive www apache params @my-ip:server_ip
+----
+
+== New ACL syntax
+
+The support for Access Control Lists has been revised in Pacemaker
+1.1.12, and this release of crmsh supports the new syntax. Two new 
+commands have been added: `acl_target` and `acl_group`. For more details,
+see the documentation.
+
+Thank you,
+
+Kristoffer and Dejan
+
diff --git a/doc/website-v1/postprocess.py b/doc/website-v1/postprocess.py
new file mode 100644
index 0000000..8ae8833
--- /dev/null
+++ b/doc/website-v1/postprocess.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python
+# create a table of contents for pages that need it
+
+import sys
+import re
+import argparse
+
+TOC_PAGES = ['man/index.html',
+             'man-2.0/index.html',
+             'man-1.2/index.html']
+V2_PAGES = ['index.html']
+INSERT_AFTER = '<!--TOC-->'
+
+def read_toc_data(infile, debug):
+    topics_data = []
+    commands_data = []
+    f = open(infile)
+    for line in f:
+        if line.startswith('[['):
+            line = line[2:-3]  # strip [[ and ]]\n
+            info, short_help = line.split(',', 1)
+            short_help = short_help.strip()
+            info_split = info.split('_')
+            if info_split[0] == 'topics':
+                if len(info_split) == 2:
+                    topics_data.append((1, short_help, info))
+                elif len(info_split) >= 3:
+                    topics_data.append((2, short_help, info))
+            elif info_split[0] == 'cmdhelp':
+                if len(info_split) == 2:
+                    commands_data.append((2, info_split[1], info))
+                elif len(info_split) >= 3:
+                    commands_data.append((3, info_split[2], info))
+    toc = ''
+    if len(topics_data) > 0 or len(commands_data) > 0:
+        toc = '<div id="toc">\n'
+        for depth, text, link in topics_data:
+            toc += '<div class="toclevel%s"><a href="#%s">%s</a></div>\n' % (
+                depth, link, text)
+        for depth, text, link in commands_data:
+            toc += '<div class="toclevel%s"><a href="#%s">%s</a></div>\n' % (
+                depth, link, text)
+        toc += '</div>\n'
+    return toc
+
+def generate_toc(infile, outfile, debug):
+
+    if debug:
+        print "Infile:", infile
+    toc = read_toc_data(infile, debug)
+    '''
+    toc_data = []
+    section = re.compile(r"<h(?P<depth>[0-9])( id=\"(?P<id>[^\"]+)\")?>(?P<text>.*)</h[0-9]>")
+    for line in f:
+        m = section.match(line)
+        if m:
+            if debug:
+                print "toc_data: %s" % str(((m.group('depth'), m.group('text'), m.group('id'))))
+            toc_data.append((m.group('depth'), m.group('text'), m.group('id')))
+
+    toc = ''
+    if len(toc_data) > 0:
+        toc = '<div id="toc">\n'
+        for depth, text, link in toc_data:
+            if depth >= 2 and link is not None:
+                toc += '<div class="toclevel%s"><a href="#%s">%s</a></div>\n' % (
+                    int(depth) - 1, link, text)
+        toc += '</div>\n'
+'''
+
+    # Write TOC to outfile
+    if outfile:
+        if debug:
+            print "Writing TOC:"
+            print "----"
+            print toc
+            print "----"
+            print "Outfile:", outfile
+        fil = open(outfile)
+        f = fil.readlines()
+        fil.close()
+        f2 = open(outfile, 'w')
+        for line in f:
+            f2.write(line)
+            if toc and line.startswith(INSERT_AFTER):
+                f2.write(toc)
+        f2.close()
+
+def generate_v2(page, debug):
+    f = open(page).readlines()
+    toc_data = []
+    section = re.compile(r"<h(?P<depth>[0-9])( id=\"(?P<id>[^\"]+)\")?>(?P<text>.*)</h[0-9]>")
+    for line in f:
+        m = section.match(line)
+        if m:
+            if debug:
+                print "toc_data: %s" % str(((m.group('depth'), m.group('text'), m.group('id'))))
+            toc_data.append((m.group('depth'), m.group('text'), m.group('id')))
+
+    toc = ''
+    if len(toc_data) > 0:
+        toc = '<div id="toc">\n'
+        for depth, text, link in toc_data:
+            if depth >= 2 and link is not None:
+                toc += '<div class="toclevel%s"><a href="#%s">%s</a></div>\n' % (
+                    int(depth) - 1, link, text)
+        toc += '</div>\n'
+    f2 = open(page, 'w')
+    for line in f:
+        f2.write(line)
+        if toc and line.startswith(INSERT_AFTER):
+            f2.write(toc)
+    f2.close()
+
+def main():
+    parser = argparse.ArgumentParser(description="Generate table of contents")
+    parser.add_argument('-d', '--debug', dest='debug', action='store_true',
+                        help="Enable debug output")
+    parser.add_argument('-o', '--output', metavar='output', type=str,
+                        help="File to inject TOC into")
+    parser.add_argument('input', metavar='input', type=str,
+                        help="File to read TOC metadata from")
+    args = parser.parse_args()
+    debug = args.debug
+    outfile = args.output
+    infile = args.input
+    print "+ %s -> %s" % (infile, outfile)
+    gen = False
+    for tocpage in TOC_PAGES:
+        if not gen and outfile.endswith(tocpage):
+            generate_toc(infile, outfile, debug)
+            gen = True
+    for tocpage in V2_PAGES:
+        if not gen and outfile.endswith(tocpage):
+            generate_v2(outfile, debug)
+            gen = True
+
+if __name__ == "__main__":
+    main()
diff --git a/doc/website-v1/rsctest-guide.txt b/doc/website-v1/rsctest-guide.txt
new file mode 100644
index 0000000..2dcd865
--- /dev/null
+++ b/doc/website-v1/rsctest-guide.txt
@@ -0,0 +1,238 @@
+= Resource testing =
+
+Never created a pacemaker cluster configuration before? Please
+read on.
+
+Ever created a pacemaker configuration without errors? All
+resources worked from the get go on all your nodes? Really? We
+want a photo of you!
+
+Seriously, it is so error prone to get a cluster resource
+definition right that I think I ever only managed to do it with
+`Dummy` resources. There are many intricate details that have to be
+just right, and all of them are stuffed in a single place as simple
+name-value attributes. Then there are multiple nodes, each node
+containing a complex system environment inevitably always in flux and
+changing (entropy anybody?).
+
+Now, once you defined your set of resources and are about to
+_commit_ the configuration (at that point it usually takes a
+deep breath to do so), be ready to meet an avalanche of error
+messages, not all of which are easy to understand or follow. Not
+to mention that you need to read the logs too. Even though we do
+have a link:history-tutorial.html[tool] to help with digging through
+the logs, it is going to be an interesting experience and not quite
+recommended if you're just starting with pacemaker clusters. Even the
+experts can save a lot of time and headaches by following the advice
+below.
+
+== Basic usage ==
+
+Enter resource testing. It is a special feature designed to help
+users find problems in resource configurations.
+
+The usage is very simple:
+
+----
+crm(live)configure# rsctest web-server 
+Probing resources ..
+testing on xen-f: apache web-ip
+testing on xen-g: apache web-ip
+crm(live)configure# 
+----
+
+What actually happened above and what is it good for? From the
+output we can infer that the `web-server` resource is actually a
+group comprising one apache web server and one IP address.
+Indeed:
+
+----
+crm(live)configure# show web-server 
+group web-server apache web-ip \
+        meta target-role="Stopped"
+crm(live)configure# 
+----
+
+The `rsctest` command first established that the resources are
+stopped on all nodes in the cluster. Then it tests the resources
+in the order defined by the resource group on all nodes. It does
+this by manually starting the resources, one by one, then running
+a "monitor" for each resource to make sure that the resources are
+healthy, and finally stopping the resources in reverse order.
+
+Since there is no additional output, the test passed. It looks
+like we have a properly defined web server group.
+
+== Reporting problems ==
+
+Now, the above run was not very interesting so let's spoil the
+idyll:
+
+----
+xen-f:~ # mv /etc/apache2/httpd.conf /tmp
+----
+
+We moved the apache configuration file away on node `xen-f`.  The
+`apache` resource should fail now:
+
+----
+crm(live)configure# rsctest web-server 
+Probing resources ..
+testing on xen-f: apache
+host xen-f (exit code 5)
+xen-f stderr:
+2013/10/17_16:51:26 ERROR: Configuration file /etc/apache2/httpd.conf not found!
+2013/10/17_16:51:26 ERROR: environment is invalid, resource considered stopped
+
+testing on xen-g: apache web-ip
+crm(live)configure# 
+----
+
+As expected, `apache` failed to start on node `xen-f`. When the
+cluster resource manager runs an operation on a resource, all
+messages are logged (there is no terminal attached to the
+cluster, anyway). All one can see in the resource status is the type
+of the exit code. In this case, it is an installation problem.
+
+For instance, the output could look like this:
+
+----
+xen-f:~ # crm status
+Last updated: Thu Oct 17 19:21:44 2013
+Last change: Thu Oct 17 19:21:28 2013 by root via crm_resource on xen-f
+...
+Failed actions:
+    apache_start_0 on xen-f 'not installed' (5): call=2074, status=complete,
+last-rc-change='Thu Oct 17 19:21:31 2013', queued=164ms, exec=0ms
+----
+
+That does not look very informative. With `rsctest` we can
+immediately see what the problem is. It saves us prowling the
+logs looking for messages of the `apache` resource agent.
+
+Note that the IP address is not tested, because the resource it
+depends on could not be started.
+
+== What is tested? ==
+
+The start, monitor, and stop operations, in exactly that order,
+are tested for every resource specified. Note that normally the
+two latter operations should never fail if the resource agent is
+well implemented. The RA should under normal circumstances be
+able to stop or monitor a started resource. However, this is
+_not_ a replacement for resource agent testing. If that is what
+you are looking for, see
+http://www.linux-ha.org/doc/dev-guides/_testing_resource_agents.html[the
+RA testing chapter] of the RA development guide.
+
+== Protecting resources ==
+
+The `rsctest` command goes to great lengths to prevent starting a
+resource on more than one node at the same time. For some stuff
+that would actually mean data corruption and we certainly don't
+want that to happen.
+
+----
+xen-f:~ # (echo start web-server; echo show web-server) | crm -w resource
+resource web-server is running on: xen-g 
+xen-f:~ # crm configure rsctest web-server
+Probing resources .WARNING: apache:probe: resource running at xen-g
+.WARNING: web-ip:probe: resource running at xen-g
+
+Stop all resources before testing!
+xen-f:~ # crm configure rsctest web-server xen-f
+Probing resources .WARNING: apache:probe: resource running at xen-g
+.WARNING: web-ip:probe: resource running at xen-g
+
+Stop all resources before testing!
+xen-f:~ # 
+----
+
+As you can see, if `rsctest` finds any of the resources running
+on any node it refuses to run any tests.
+
+== Multi-state and clone resources ==
+
+Apart from groups, the `rsctest` can also handle the other two
+special kinds of resources. Let's take a look at one `drbd`-based
+configuration:
+
+----
+crm(live)configure# show ms_drbd_nfs drbd0-vg 
+primitive drbd0-vg ocf:heartbeat:LVM \
+        params volgrpname="drbd0-vg"
+primitive p_drbd_nfs ocf:linbit:drbd \
+        meta target-role="Stopped" \
+        params drbd_resource="nfs" \
+        op monitor interval="15" role="Master" \
+        op monitor interval="30" role="Slave" \
+        op start interval="0" timeout="300" \
+        op stop interval="0" timeout="120"
+ms ms_drbd_nfs p_drbd_nfs \
+        meta notify="true" clone-max="2"
+crm(live)configure# 
+----
+
+The `nfs` drbd resource contains a volume group `drbd0-vg`.
+
+----
+crm(live)configure# rsctest ms_drbd_nfs drbd0-vg 
+Probing resources ..
+testing on xen-f: p_drbd_nfs drbd0-vg
+testing on xen-g: p_drbd_nfs drbd0-vg
+crm(live)configure# 
+----
+
+For the multi-state (master-slave) resources, the involved
+resource motions are somewhat more complex: the resource is first
+started on both nodes and then promoted on the node where the
+next resource is to be tested (in this case the volume group).
+Then it gets demoted to slave and promoted on the other
+node to master so that the depending resources can be tested on
+that node too.
+
+Note that even though we asked for `ms_drbd_nfs` to be tested,
+there is `p_drbd_nfs` in the output which is the primitive
+encapsulated in the master-slave resource. You can specify either
+one.
+
+== Stonith resources ==
+
+The stonith resources are also special and need special
+treatment. What is tested is just the device status. Actually
+fencing nodes was deemed too drastic. Please use `node fence` to
+test the fencing device effectiveness. It also does not matter
+whether the stonith resource is "running" on any node: being
+started is just something that happens virtually in the
+`stonithd` process.
+
+== Summary ==
+
+- use `rsctest` to make sure that the resources can be started
+  correctly on all nodes
+
+- `rsctest` protects resources by making sure beforehand that
+  none of them is currently running on any of the cluster nodes
+
+- `rsctest` understands groups, master-slave (multi-state), and
+  clone resources, but nothing else of the configuration
+  (constraints or any other placement/order cluster configuration
+  elements)
+
+- it is up to the user to test resources only on nodes which are
+  really supposed to run them and in a proper order (if that
+  order is expressed via constraints)
+
+- `rsctest` cannot protect resources if they are running on
+  nodes which are not present in the cluster or from bad RA
+  implementations (but neither would a cluster resource manager)
+
+- `rsctest` was designed as a debugging and configuration aid, and is
+  not intended to provide full Resource Agent test coverage.
+
+== `crmsh` help and online resources (_sic!_) ==
+
+- link:crm.8.html#topics_Testing[`crm help Testing`]
+
+- link:crm.8.html#cmdhelp_configure_rsctest[`crm configure help
+rsctest`]
diff --git a/doc/website-v1/scripts.txt b/doc/website-v1/scripts.txt
new file mode 100644
index 0000000..2093093
--- /dev/null
+++ b/doc/website-v1/scripts.txt
@@ -0,0 +1,445 @@
+= Cluster Scripts =
+:source-highlighter: pygments
+
+.Version information
+NOTE: This section applies to `crmsh 2.0+` only.
+
+== Introduction ==
+
+A big part of the configuration and management of a cluster is
+collecting information about all cluster nodes and deploying changes
+to those nodes. Often, just performing the same procedure on all nodes
+will encounter problems, due to subtle differences in the
+configuration.
+
+For example, when configuring a cluster for the first time, the
+software needs to be installed and configured on all nodes before the
+cluster software can be launched and configured using `crmsh`. This
+process is cumbersome and error-prone, and the goal is for scripts to
+make this process easier.
+
+Another important function of scripts is collecting information and
+reporting potential issues with the cluster. For example, software
+versions may differ between nodes, causing byzantine errors or random
+failure. `crmsh` comes packaged with a `health` script which will
+detect and warn about many of these types of problems.
+
+There are many tools for managing a collection of nodes, and scripts
+are not intended to replace these tools. Rather, they provide an
+integrated way to perform tasks across the cluster that would
+otherwise be tedious, repetitive and error-prone. The scripts
+functionality in the crm shell is mainly inspired by Ansible, a
+light-weight and efficient configuration management tool.
+
+Scripts are implemented using the `parallel-ssh` package which
+provides a thin wrapper on top of SSH. This allows the scripts to
+function through the usual SSH channels used for system maintenance,
+requiring no additional software to be installed or maintained.
+
+== Usage ==
+
+Scripts are available through the `cluster` sub-level in the crm
+shell. Some scripts have custom commands linked to them for
+convenience, such as the `init`, `join` and `remove` commands for
+creating new clusters, introducing new nodes into the cluster and for
+removing nodes from a running cluster.
+
+Other scripts can be accessed through the `script` sub-level inside
+`cluster`.
+
+=== List available scripts ===
+
+To list the available scripts, use the following command:
+
+.........
+# crm script
+list
+.........
+
+The available scripts are listed along with a short description.
+
+=== Script description ===
+
+To get more details about a script, run the `describe` command. For
+example, to get more information about what the `health` script does
+and what parameters it accepts, use the following command:
+
+.........
+# crm script
+describe health
+.........
+
+`describe` will print a longer explanation for the script, along with
+a list of parameters, each parameter having a description, a note
+saying if it is an optional or required parameter, and if optional,
+what the default value is.
+
+=== Running a script ===
+
+To run a script, all required parameters and any optional parameters
+that should have values other than the default should be provided as
+`key=value` pairs on the command line. The following example shows how 
+to call the `health` script with verbose output enabled:
+
+........
+# crm script
+run health verbose=true
+........
+
+
+==== Single-stepping a script ====
+
+It is possible to run a script step-by-step, with manual intervention
+between steps. First of all, list the steps of the script to run:
+
+........
+crm script steps health
+........
+
+To execute a single step, two things need to be provided:
+
+1. The name of the step to execute (printed by `steps`)
+2. a file in which `crmsh` stores the state of execution.
+
+Note that it is entirely possible to run steps out-of-order, however
+this is unlikely to work in practice since steps often rely on the
+output of previous steps.
+
+The following command will execute the first step of the `health`
+script and store the output in a temporary file named `health.json`:
+
+........
+crm script run health \
+    step='Collect cluster information' \
+    statefile='health.json'
+........
+
+The statefile contains the script parameters and the output of
+previous steps, encoded as `json` data.
+
+To continue executing the next step in sequence, replace the step name
+with the next step:
+
+........
+crm script run health \
+    step='Report cluster state' \
+    statefile='health.json'
+........
+
+Note that the `dry_run` flag that can be used to do partial execution
+of scripts is not taken into consideration when single-stepping
+through a script.
+
+== Creating a script ==
+
+This section will describe how to create a new script, where to put
+the script to allow `crmsh` to find it, and how to test that the
+script works as intended.
+
+=== How scripts work, in detail ===
+
+When the script runs, the steps defined in `main.yml` as described
+below are executed one at a time. Each step describes an action that
+is applied to the cluster, either by calling out and running scripts
+on each of the cluster nodes, or by running a script locally on the
+node from which the command was executed.
+
+=== Actions ===
+
+Scripts perform actions that are classified into a few basic
+types. Each action is performed by calling out to a shell script,
+but the arguments and location of that script varies depending on the
+type.
+
+Here are the types of script actions that can be performed:
+
+Collect::
+  * Runs on all cluster nodes
+  * Gathers information about the nodes, both general information and
+    information specific to the script.
+
+Validate::
+  * Runs on the local node
+  * Validate parameter values and node state based on collected
+    information. Can modify default values and report issues that
+    would prevent the script from applying successfully.
+
+Apply::
+  * Runs on all or any cluster nodes
+  * Applies changes, returning information about the applied changes
+    to the local node.
+
+Apply-Local::
+  * Runs on the local node
+  * Applies changes to the cluster, where an action taken on a single
+    node affect the entire cluster. This includes updating the CIB in
+    Pacemaker, and also reloading the configuration for Corosync.
+
+Report::
+  * Runs on the local node
+  * This is similar to the _Apply-Local_ action, with the difference
+    that the output of a Report action is not interpreted as JSON data
+    to be passed to the next action. Instead, the output is printed to
+    the screen.
+
+
+=== Basic structure ===
+
+The crm shell looks for scripts in two primary locations: Included
+scripts  are installed in the system-wide shared folder, usually
+`/usr/share/crmsh/scripts/`. Local and custom scripts are loaded from
+the user-local XDG_CONFIG folder, usually found at
+`~/.local/crm/scripts/`. These locations may differ depending on how
+the crm shell was installed and which system is used, but these are
+the locations used on most distributions.
+
+To create a new script, make a new folder in the user-local scripts
+folder and give it a unique name. In this example, we will call our
+new script `check-uptime`.
+
+........
+mkdir -p ~/.local/crm/scripts/check-uptime
+........
+
+In this directory, create a file called `main.yml`. This is a YAML
+document which describes the script, which parameters it requires, and
+what actions it will perform.
+
+YAML is a human-readable markup language which is designed to be easy
+to read and modify, while at the same time be compatible with JSON. To
+learn more, see http:://yaml.org/[yaml.org].
+
+Here is an example `main.yml` file, heavily commented to explain what
+each section means.
+
+[source,yaml]
+----
+---
+# The triple-dash indicates that this is a yaml document.
+# All yaml documents should begin with this line.
+- name: Check uptime of nodes
+  description: >
+    This script will fetch the uptime of
+    all nodes and report which node has been
+    up the longest.
+  parameters:
+    # Parameters must have a name and description.
+    # If a default value is provided, the parameter
+    # is considered optional. Parameters without a
+    # default value must be provided when running the
+    # script.
+    - name: show_all
+      description: Show all uptimes
+      default: false
+  steps:
+    # Steps consist of a descriptive name and an action which
+    # calls a script to do its work. The script should be an
+    # executable file located in the same folder as main.yml.
+    #
+    # Script files can be written in any language, as long as
+    # the cluster nodes know how to execute them.
+    #
+    # These are the valid actions:
+    # collect:
+    #     Runs on all nodes. Should not perform changes, only
+    #     gather and return information.
+    # validate:
+    #     Runs on the local node only. Should report problems
+    #     that would prevent further progress. If validate returns
+    #     a map of values, matching script parameters are updated
+    #     to reflect those values.
+    # apply:
+    #     Runs on all nodes. Applies changes.
+    #     If the dry_run flag is set, script execution stops
+    #     before the first apply action.
+    #
+    # apply_local:
+    #     Runs on the local node only. Otherwise same as apply.
+    #
+    # report:
+    #     Runs on the local node only. Output from this step is
+    #     printed, not saved as input to the following steps.
+    #     This output does not have to be in JSON format.
+    - name: Fetch uptime
+      collect: fetch.py
+    - name: Report uptime
+      report: report.py
+----
+
+The actions must not be Python scripts. They can be plain bash scripts
+or any other executable script as long as the nodes have the necessary
+dependencies installed. However, see below why implementing scripts in
+Python is easier.
+
+Actions report their progress either by returning JSON on standard
+output, or by returning a non-zero return value and printing an error
+message to standard error.
+
+Any JSON returned by an action will be available to the following
+steps in the script. When the script executes, it does so in a
+temporary folder created for that purpose. In that folder is a file
+named `script.input`, containing a JSON array with the output produced
+by previous steps.
+
+The first element in the array (the zeroth element, to be precise) is
+a dict containing the parameter values. 
+
+The following elements are dicts with the hostname of each node as key
+and the output of the action generated by that node as value.
+
+In most cases, only local actions (`validate` and `apply_local`) will
+use the information in previous steps, but scripts are not limited in
+what they can do.
+
+With this knowledge, we can implement `fetch.py` and `report.py`.
+
+`fetch.py`:
+
+[source,python]
+----
+#!/usr/bin/env python
+import crm_script as crm
+try:
+    uptime = open('/proc/uptime').read().split()[0]
+    crm.exit_ok(uptime)
+except:
+    crm.exit_fail("Couldn't open /proc/uptime")
+----
+
+`report.py`:
+
+[source,python]
+----
+#!/usr/bin/env python
+import crm_script as crm
+show_all = crm.is_true(crm.param('show_all'))
+uptimes = crm.output(1).items()
+max_uptime = 0, ''
+for host, uptime in uptimes:
+    if uptime > max_uptime[0]:
+        max_uptime = uptime, host
+if show_all:
+    print "Uptimes: %s" % (', '.join("%s: %s" % v for v in uptimes))
+print "Longest uptime is %s seconds on host %s" % max_uptime
+----
+
+See below for more details on the helper library `crm_script`.
+
+Save the scripts as executable files in the same directory as the
+`main.yml` file.
+
+Before running the script, it is possible to verify that the files are
+in a valid format and in the right location. Run the following
+command:
+
+........
+crm script verify check-uptime
+........
+
+If the verification is successful, try executing the script with the
+following command:
+
+........
+crm script run check-uptime
+........
+
+Example output:
+
+[source,bash]
+----
+# crm script run check-uptime
+INFO: Check uptime of nodes
+INFO: Nodes: ha-three, ha-one
+OK: Fetch uptimes
+OK: Report uptime
+Longest uptime is 161054.04 seconds on host ha-one
+----
+
+To see if the `show_all` parameter works as intended, run the
+following:
+
+........
+crm script run check-uptime show_all=yes
+........
+
+Example output:
+
+[source,bash]
+----
+# crm script run check-uptime show_all=yes
+INFO: Check uptime of nodes
+INFO: Nodes: ha-three, ha-one
+OK: Fetch uptimes
+OK: Report uptime
+Uptimes: ha-one: 161069.83, ha-three: 159950.38
+Longest uptime is 161069.83 seconds on host ha-one
+----
+
+=== Remote permissions ===
+
+Some scripts may require super-user access to remote or local
+nodes. It is recommended that this is handled through SSH certificates
+and agents, to facilitate password-less access to nodes.
+
+=== Running scripts without a cluster ===
+
+All cluster scripts can optionally take a `nodes` argument, which
+determines the nodes that the script will run on. This node list is
+not limited to nodes already in the cluster. It is even possible to
+execute cluster scripts before a cluster is set up, such as the
+`health` and `init` scripts used by the `cluster` sub-level.
+
+........
+crm script run health nodes=example1,example2
+........
+
+The list of nodes can be comma- or space-separated, but if the list
+contains spaces, the whole argument will have to be quoted:
+
+........
+crm script run health nodes="example1 example2"
+........
+
+=== Running in validate mode ===
+
+It may be desirable to do a dry-run of a script, to see if any
+problems are present that would make the script fail before trying to
+apply it. To do this, add the argument `dry_run=yes` to the invocation:
+
+.........
+crm script run health dry_run=yes
+.........
+
+The script execution will stop at the first `apply` action. Note that
+non-modifying steps that happen after the first `apply` action will
+not be performed in a dry run.
+
+=== Helper library ===
+
+When the script data is copied to each node, a small helper library is
+also passed along with the script. This library can be found in
+`utils/crm_script.py` in the source repository. This library helps
+with producing output in the correct format, parsing the
+`script.input` data provided to scripts, and more.
+
+.`crm_script` API
+`host()`::
+    Returns hostname of current node
+`get_input()`::
+    Returns the input data list. The first element in the list
+    is a dict of the script parameters. The rest are the output
+    from previous steps.
+`parameters()`::
+    Returns the script parameters as a dict.
+`param(name)`::
+    Returns the value of the named script parameter.
+`output(step_idx)`::
+    Returns the output of the given step, with the first step being step 1.
+`exit_ok(data)`::
+    Exits the step returning `data` as output.
+`exit_fail(msg)`::
+    Exits the step returning `msg` as error message.
+`is_true(value)`::
+    Converts a truth value from string to boolean.
+`call(cmd, shell=False)`::
+    Perform a system call. Returns `(rc, stdout, stderr)`.
diff --git a/doc/website-v1/start-guide.txt b/doc/website-v1/start-guide.txt
new file mode 100644
index 0000000..5b3810b
--- /dev/null
+++ b/doc/website-v1/start-guide.txt
@@ -0,0 +1,290 @@
+= Getting Started
+
+So, you've successfully installed `crmsh` on one or more machines, and
+now you want to configure a basic cluster. This guide is intended to
+provide step-by-step instructions for configuring Pacemaker
+with a single resource capable of failing over between a pair of
+nodes, and then builds on that base to cover some more advanced topics
+of cluster management.
+
+****
+Haven't installed yet? Please follow the 
+link:/installation[installation instructions]
+before continuing this guide. Only `crmsh` and
+its dependencies need to be installed before 
+following this guide.
+****
+
+Before continuing, make sure that this command executes successfully
+on all nodes, and returns a version number that is `2.1` or higher:
+
+........
+crm version
+........
+
+.Example cluster
+**************************
+
+These are the machines used as an example in this guide. Please
+replace the references to these names and IP addresses to the values
+appropriate for your cluster:
+
+
+[options="header,footer"]
+|=======================
+|Name  |IP
+|alice |10.0.0.2
+|bob   |10.0.0.3
+|=======================
+**************************
+
+
+== The cluster stack
+
+The composition of the GNU/Linux cluster stack has changed somewhat
+over the years. The stack described here is the currently most common
+variant, but there are other ways of configuring these tools.
+
+Simply put, a High Availability cluster is a set of machines (commonly
+referred to as *nodes*) with redundant capacity, such that if one or
+more of these machines experience failure of any kind, the other nodes
+in the cluster can take over the responsibilities previously handled
+by the failed node.
+
+The cluster stack is a set of programs running on all of these nodes,
+communicating with each other over the network to monitor each other
+and deciding where, when and how resources are stopped, started or
+reconfigured.
+
+The main component of the stack is *Pacemaker*, the software
+responsible for managing cluster resources, allocating them to cluster
+nodes according to the rules specified in the *CIB*.
+
+The CIB is an XML document maintained by Pacemaker, which describes
+all cluster resources, their configuration and the constraints that
+decide where and how they are managed. This document is not edited
+directly, and with the help of `crmsh` it is possible to avoid
+exposure to the underlying XML at all.
+
+Beneath Pacemaker in the stack sits *Corosync*, a cluster
+communication system. Corosync provides the communication capabilities
+and cluster membership functionality used by Pacemaker. Corosync is
+configured through the file `/etc/corosync/corosync.conf`. `crmsh`
+provides tools for configuring corosync similar to Pacemaker.
+
+Aside from these two components, the stack also consists of a
+collection of *Resource Agents*. These are basically scripts that wrap
+software that the cluster needs to manage, providing a unified
+interface to configuration, supervision and management of the
+software. For example, there are agents that handle virtual IP
+resources, web servers, databases and filesystems.
+
+`crmsh` is a command line tool which interfaces against all of these
+components, providing a unified interface for configuration and
+management of the whole cluster stack.
+
+== SSH
+
+`crmsh` runs as a command line tool on any one of the cluster
+nodes. In order for to to control all cluster nodes, it needs to be
+able to execute commands remotely. `crmsh` does this by invoking
+`ssh`.
+
+Configure `/etc/hosts` on each of the nodes so that the names of the
+other nodes map to the IP addresses of those nodes. For example in a
+cluster consisting of `alice` and `bob`, executing `ping bob` when
+logged in as root on `alice` should successfully locate `bob` on the
+network. Given the IP addresses of `alice` and `bob` above, the
+following should be entered into `/etc/hosts` on both nodes:
+
+........
+10.0.0.2      alice
+10.0.0.3      bob
+........
+
+Once this is done, SSH keys need to be installed for password-less
+access between the nodes. This is something that will hopefully be
+automated in the future, but is unfortunately a manual step at this
+point in time.
+
+The following commands should be executed as `root` on one of the
+nodes (in this example, `alice`):
+
+...............
+# ensure that the ssh server is started
+sudo systemctl start sshd
+# create the shared key
+mkdir -m 700 -p /root/.ssh
+ssh-keygen -q -f /root/.ssh/id_rsa -C "Cluster Internal" -N ''
+cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys
+...............
+
+On the other nodes in the cluster (in this example, `bob`), execute
+the following as `root`:
+
+...............
+mkdir -m 700 -p /root/.ssh
+scp -oStrictHostKeyChecking=no \
+    root at alice:'/root/.ssh/id_rsa*' /root/.ssh
+cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys
+...............
+
+This enables SSH to connect without prompting for a password between
+the nodes. Make sure this works before continuing with this guide.
+
+== Install and configure
+
+To install the basic packages and configure `systemd` to manage
+Corosync and Pacemaker, the following command is provided by `crmsh`:
+
+........
+crm cluster init nodes=alice,bob
+........
+
+== Firewall
+
+If your cluster nodes have a firewall configured, you will need to
+open the following ports:
+
+* TCP: 5560
+* UDP: 5404, 5405
+* TCP: 21064 (if using DLM)
+* TCP: 30865 (if using csync2)
+* TCP: 7630 (if using the hawk web UI)
+
+TIP: Issues with the firewall may manifest itself by cluster nodes
+     being shown as `UNCLEAN` in the `crm status` output. If you see
+     this, it is likely that a firewall rule is blocking cluster
+     communications, or there may be some other problem with the
+     network connection between the nodes.
+
+== Quorum
+
+At this point, corosync needs to be configured for the particular
+cluster being created. First of all, `quorum` needs to taken into
+consideration. To make things as easy as possible, it is advisable to
+have a total number of nodes in the cluster that is not divisible by
+two. In other words, the number of nodes in the cluster should usually
+be either 3 or 5. This is to ensure __quorum__, that is, in the case
+of a loss of network connectivity between some subsets of nodes, one
+of the network partitions will either have more members than the
+others, or every node in the cluster will be isolated.
+
+To configure corosync to manage quorum for you, you need to enable
+`votequorum` in the corosync configuration. To do this, the following
+command can be used:
+
+........
+crm corosync set quorum.provider corosync_votequorum
+........
+
+Corosync also needs to know the number of nodes required to be
+considered a majority vote. Usually, this should be set to 2:
+
+........
+crm corosync set quorum.expected_votes 2
+........
+
+After changing the quorum settings, the changes need to be propagated
+across the cluster and corosync needs to be restarted. To do this, the
+following sequence of commands can be used:
+
+........
+crm corosync push
+crm cluster stop
+crm cluster start
+........
+
+NOTE: Restarting the cluster is only necessary if the cluster has
+      already been started.
+
+== Start Pacemaker
+
+To start Corosync and Pacemaker, the following command can be used:
+
+........
+crm cluster start
+........
+
+== Check cluster status
+
+To see if Pacemaker is running, what nodes are part of the cluster and
+what resources are active, use the `status` command:
+
+.........
+crm status
+.........
+
+If this command fails or times out, there is some problem with
+Pacemaker or Corosync on the local machine. Perhaps some dependency is
+missing, a firewall is blocking cluster communication or some other
+unrelated problem has occurred. If this is the case, the `cluster
+health` command may be of use.
+
+== Cluster health check
+
+To check the health status of the machines in the cluster, use the
+following command:
+
+........
+crm cluster health
+........
+
+This command will perform multiple diagnostics on all nodes in the
+cluster, and return information about low disk space, communication
+issues or problems with mismatching software versions between nodes,
+for example.
+
+If no cluster has been configured or there is some fundamental problem
+with cluster communications, `crmsh` may be unable to figure out what
+nodes are part of the cluster. If this is the case, the list of nodes
+can be provided to the health command directly:
+
+........
+crm cluster health nodes=alice,bob
+........
+
+== Adding a resource
+
+To test the cluster and make sure it is working properly, we can
+configure a Dummy resource. The Dummy resource agent is a simple
+resource that doesn't actually manage any software. It exposes a
+single numerical parameter called `state` which can be used to test
+the basic functionality of the cluster before introducing the
+complexities of actual resources.
+
+To configure a Dummy resource, run the following command:
+
+........
+crm configure primitive p0 Dummy
+........
+
+This creates a new resource, gives it the name `p0` and sets the
+agent for the resource to be the `Dummy` agent.
+
+`crm status` should now show the `p0` resource as started on one
+of the cluster nodes:
+
+........
+# crm status
+Last updated: Wed Jul  2 21:49:26 2014
+Last change: Wed Jul  2 21:49:19 2014
+Stack: corosync
+Current DC: alice (2) - partition with quorum
+Version: 1.1.11-c3f1a7f
+2 Nodes configured
+1 Resources configured
+
+
+Online: [ alice bob ]
+
+ p0	(ocf::heartbeat:Dummy):	Started alice
+........
+
+The resource can be stopped or started using the `resource start` and
+`resource stop` commands:
+
+........
+crm resource stop p0
+crm resource start p0
+........
diff --git a/hb_report/Makefile.am b/hb_report/Makefile.am
new file mode 100644
index 0000000..5a745c3
--- /dev/null
+++ b/hb_report/Makefile.am
@@ -0,0 +1,25 @@
+#
+# heartbeat: Linux-HA heartbeat code
+#
+# Copyright (C) 2001 Michael Moerz
+#
+# 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.
+#
+MAINTAINERCLEANFILES  = Makefile.in
+hanoarchdir           = $(datadir)/@PACKAGE@
+hanoarch_DATA         = utillib.sh ha_cf_support.sh openais_conf_support.sh
+hanoarch_SCRIPTS          = hb_report
+
+EXTRA_DIST            = $(hanoarch_DATA)
diff --git a/hb_report/ha_cf_support.sh b/hb_report/ha_cf_support.sh
new file mode 100644
index 0000000..7b35c98
--- /dev/null
+++ b/hb_report/ha_cf_support.sh
@@ -0,0 +1,83 @@
+ # Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+ # 
+ # 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.1 of the License, or (at your option) any later version.
+ # 
+ # This software 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 library; if not, write to the Free Software
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ #
+
+#
+# Stack specific part (heartbeat)
+# ha.cf/logd.cf parsing
+#
+getcfvar() {
+	[ -f "$CONF" ] || return
+	sed 's/#.*//' < $CONF |
+		grep -w "^$1" |
+		sed 's/^[^[:space:]]*[[:space:]]*//'
+}
+iscfvarset() {
+	test "`getcfvar $1`"
+}
+iscfvartrue() {
+	getcfvar "$1" |
+		egrep -qsi "^(true|y|yes|on|1)"
+}
+uselogd() {
+	iscfvartrue use_logd &&
+		return 0  # if use_logd true
+	iscfvarset logfacility ||
+	iscfvarset logfile ||
+	iscfvarset debugfile ||
+		return 0  # or none of the log options set
+	false
+}
+get_hb_logvars() {
+	# unless logfacility is set to none, heartbeat/ha_logd are
+	# going to log through syslog
+	HA_LOGFACILITY=`getcfvar logfacility`
+	[ "" = "$HA_LOGFACILITY" ] && HA_LOGFACILITY=$DEFAULT_HA_LOGFACILITY
+	[ none = "$HA_LOGFACILITY" ] && HA_LOGFACILITY=""
+	HA_LOGFILE=`getcfvar logfile`
+	HA_DEBUGFILE=`getcfvar debugfile`
+}
+getlogvars() {
+	HA_LOGFACILITY=${HA_LOGFACILITY:-$DEFAULT_HA_LOGFACILITY}
+	HA_LOGLEVEL="info"
+	cfdebug=`getcfvar debug` # prefer debug level if set
+	isnumber $cfdebug || cfdebug=""
+	[ "$cfdebug" ] && [ $cfdebug -gt 0 ] &&
+		HA_LOGLEVEL="debug"
+	if uselogd; then
+		[ -f "$LOGD_CF" ] || {
+			debug "logd used but logd.cf not found: using defaults"
+			return  # no configuration: use defaults
+		}
+		debug "reading log settings from $LOGD_CF"
+		get_logd_logvars
+	else
+		debug "reading log settings from $CONF"
+		get_hb_logvars
+	fi
+}
+cluster_info() {
+	echo "heartbeat version: `$HA_BIN/heartbeat -V`"
+}
+essential_files() {
+	cat<<EOF
+d $HA_VARLIB 0755 root root
+d $HA_VARLIB/ccm 0750 hacluster haclient
+d $PCMK_LIB 0750 hacluster haclient
+d $PE_STATE_DIR 0750 hacluster haclient
+d $CIB_DIR 0750 hacluster haclient
+EOF
+}
diff --git a/hb_report/hb_report.in b/hb_report/hb_report.in
new file mode 100755
index 0000000..916621d
--- /dev/null
+++ b/hb_report/hb_report.in
@@ -0,0 +1,1469 @@
+#!/bin/sh
+
+ # Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+ # 
+ # 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.1 of the License, or (at your option) any later version.
+ # 
+ # This software 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 library; if not, write to the Free Software
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ #
+
+. @OCF_ROOT_DIR@/lib/heartbeat/ocf-shellfuncs
+
+HA_NOARCHBIN=@datadir@/@PACKAGE_NAME@
+
+. $HA_NOARCHBIN/utillib.sh
+
+unset LANG
+export LC_ALL=POSIX
+
+PROG="report"
+
+# the default syslog facility is not (yet) exported by heartbeat
+# to shell scripts
+#
+DEFAULT_HA_LOGFACILITY="daemon"
+export DEFAULT_HA_LOGFACILITY
+LOGD_CF=`findlogdcf @sysconfdir@ $HA_DIR`
+export LOGD_CF
+
+SSH_PASSWORD_NODES=""
+: ${SSH_OPTS="-o StrictHostKeyChecking=no -o EscapeChar=none"}
+LOG_PATTERNS="CRIT: ERROR:"
+# PEINPUTS_PATT="peng.*PEngine Input stored"
+
+# Important events
+#
+# Patterns format:
+#	title	extended_regexp
+# NB: don't use spaces in titles or regular expressions!
+EVENT_PATTERNS="
+membership	crmd.*ccm_event.*(NEW|LOST)|pcmk_peer_update.*(lost|memb):
+quorum		crmd.*crm_update_quorum:.Updating.quorum.status|crmd.*ais.disp.*quorum.(lost|ac?quir)
+pause		Process.pause.detected
+resources	lrmd.*rsc:(start|stop)
+stonith		crmd.*te_fence_node.*Exec|stonith-ng.*log_oper.*reboot|stonithd.*(requests|(Succeeded|Failed).to.STONITH|result=)
+start_stop	Configuration.validated..Starting.heartbeat|Corosync.Cluster.Engine|Executive.Service.RELEASE|crm_shutdown:.Requesting.shutdown|pcmk_shutdown:.Shutdown.complete
+"
+
+init_tmpfiles
+
+#
+# the instance where user runs hb_report is the master
+# the others are slaves
+#
+if [ x"$1" = x__slave ]; then
+	SLAVE=1
+fi
+
+usage() {
+	cat<<EOF
+usage: $PROG -f {time|"cts:"testnum} [-t time]
+       [-u user] [-X ssh-options] [-l file] [-n nodes] [-E files]
+       [-p patt] [-L patt] [-e prog] [-MSDZAQVsvhd] [dest]
+
+	-f time: time to start from or a CTS test number
+	-t time: time to finish at (dflt: now)
+	-d     : don't compress, but leave result in a directory
+	-n nodes: node names for this cluster; this option is additive
+	         (use either -n "a b" or -n a -n b)
+	         if you run $PROG on the loghost or use autojoin,
+	         it is highly recommended to set this option
+	-u user: ssh user to access other nodes (dflt: empty, root, hacluster)
+	-X ssh-options: extra ssh(1) options
+	-l file: log file
+	-E file: extra logs to collect; this option is additive
+	         (dflt: /var/log/messages)
+	-s     : sanitize the PE and CIB files
+	-p patt: regular expression to match variables containing sensitive data;
+	         this option is additive (dflt: "passw.*")
+	-L patt: regular expression to match in log files for analysis;
+	         this option is additive (dflt: $LOG_PATTERNS)
+	-e prog: your favourite editor
+	-Q     : don't run resource intensive operations (speed up)
+	-M     : don't collect extra logs (/var/log/messages)
+	-D     : don't invoke editor to write description
+	-Z     : if destination directories exist, remove them instead of exiting
+	         (this is default for CTS)
+	-A     : this is an OpenAIS cluster
+	-S     : single node operation; don't try to start report
+	         collectors on other nodes
+	-v     : increase verbosity
+	-V     : print version
+	dest   : report name (may include path where to store the report)
+EOF
+
+[ "$1" != short ] &&
+	cat<<EOF
+
+	. the multifile output is stored in a tarball {dest}.tar.bz2
+	. the time specification is as in either Date::Parse or
+	  Date::Manip, whatever you have installed; Date::Parse is
+	  preferred
+	. we try to figure where is the logfile; if we can't, please
+	  clue us in ('-l')
+	. we collect only one logfile and /var/log/messages; if you
+	  have more than one logfile, then use '-E' option to supply
+	  as many as you want ('-M' empties the list)
+
+	Examples
+
+	  $PROG -f 2pm report_1
+	  $PROG -f "2007/9/5 12:30" -t "2007/9/5 14:00" report_2
+	  $PROG -f 1:00 -t 3:00 -l /var/log/cluster/ha-debug report_3
+	  $PROG -f "09sep07 2:00" -u hbadmin report_4
+	  $PROG -f 18:00 -p "usern.*" -p "admin.*" report_5
+	  $PROG -f cts:133 ctstest_133
+
+	. WARNING . WARNING . WARNING . WARNING . WARNING . WARNING .
+
+	  We won't sanitize the CIB and the peinputs files, because
+	  that would make them useless when trying to reproduce the
+	  PE behaviour. You may still choose to obliterate sensitive
+	  information if you use the -s and -p options, but in that
+	  case the support may be lacking as well. The logs and the
+	  crm_mon, ccm_tool, and crm_verify output are *not* sanitized.
+
+	  Additional system logs (/var/log/messages) are collected in
+	  order to have a more complete report. If you don't want that
+	  specify -M.
+
+	  IT IS YOUR RESPONSIBILITY TO PROTECT THE DATA FROM EXPOSURE!
+EOF
+	exit
+}
+version() {
+	echo "@PACKAGE_NAME@: @PACKAGE_VERSION@ (@BUILD_VERSION@)"
+	exit
+}
+#
+# these are "global" variables
+#
+setvarsanddefaults() {
+	local now=`perl -e 'print time()'`
+	# used by all
+	DEST=""
+	FROM_TIME=""
+	CTS=""
+	TO_TIME=0
+	HA_LOG=""
+	UNIQUE_MSG="Mark:HB_REPORT:$now"
+	SANITIZE="passw.*"
+	DO_SANITIZE=""
+	FORCE_REMOVE_DEST=""
+	COMPRESS="1"
+	# logs to collect in addition
+	# NB: they all have to be in syslog format
+	#
+	EXTRA_LOGS="/var/log/messages"
+	# used only by the master
+	NO_SSH=""
+	SSH_USER=""
+	TRY_SSH="root hacluster"
+	SLAVEPIDS=""
+	NO_DESCRIPTION="1"
+	SKIP_LVL=0
+	VERBOSITY=0
+}
+#
+# caller may skip collecting information if the skip level
+# exceeds the given value
+#
+skip_lvl() {
+	[ $SKIP_LVL -ge $1 ]
+}
+chkname() {
+	[ "$1" ] || usage short
+	echo $1 | grep -qs '[^a-zA-Z0-9 at _+=:.-]' &&
+		fatal "$1 contains illegal characters"
+}
+set_dest() {
+	# default DEST has already been set earlier (if the
+	# argument is missing)
+	if [ $# -eq 1 ]; then
+		DEST=`basename $1`
+		DESTDIR=`dirname $1`
+	fi
+	chkname $DEST
+	if [ -z "$COMPRESS" -a -e "$DESTDIR/$DEST" ]; then
+		if [ "$FORCE_REMOVE_DEST" -o "$CTS" ]; then
+			rm -rf $DESTDIR/$DEST
+		else
+			fatal "destination directory $DESTDIR/$DEST exists, please cleanup or use -Z" 
+		fi
+	fi
+}
+chktime() {
+	[ "$1" ] || fatal "bad time specification: $2 (try 'YYYY-M-D H:M:S') "
+}
+no_dir() {
+	fatal "could not create the working directory $WORKDIR"
+}
+time2str() {
+	perl -e "use POSIX; print strftime('%x %X',localtime($1));"
+}
+# try to figure out where pacemaker ... etc
+get_pe_state_dir() {
+	PE_STATE_DIR=`python -c "import crmsh.config; print crmsh.config.path.pe_state_dir"`
+	test -d "$PE_STATE_DIR"
+}
+get_cib_dir() {
+	CIB_DIR=`python -c "import crmsh.config; print crmsh.config.path.crm_config"`
+	test -d "$CIB_DIR"
+}
+get_pe_state_dir2() {
+	# PE_STATE_DIR
+	local localstatedir lastf
+	localstatedir=`dirname $HA_VARLIB`
+	lastf=$(2>/dev/null ls -rt `2>/dev/null find /var/lib -name pengine -type d |
+		sed 's,$,/*.last,'` | tail -1)
+	if [ -f "$lastf" ]; then
+		PE_STATE_DIR=`dirname $lastf`
+	else
+		for p in pacemaker/pengine pengine heartbeat/pengine; do
+			if [ -d $localstatedir/$p ]; then
+				debug "setting PE_STATE_DIR to $localstatedir/$p"
+				PE_STATE_DIR=$localstatedir/$p
+				break
+			fi
+		done
+	fi
+}
+get_cib_dir2() {
+	# CIB
+	# HA_VARLIB is normally set to {localstatedir}/heartbeat
+	local localstatedir
+	localstatedir=`dirname $HA_VARLIB`
+	for p in pacemaker/cib heartbeat/crm; do
+		if [ -f $localstatedir/$p/cib.xml ]; then
+			debug "setting CIB_DIR to $localstatedir/$p"
+			CIB_DIR=$localstatedir/$p
+			break
+		fi
+	done
+}
+get_crm_daemon_dir() {
+	# CRM_DAEMON_DIR
+	local libdir p
+	libdir=`dirname $HA_BIN`
+	for p in pacemaker heartbeat; do
+		if [ -x $libdir/$p/crmd ]; then
+			debug "setting CRM_DAEMON_DIR to $libdir/$p"
+			CRM_DAEMON_DIR=$libdir/$p
+			return 0
+		fi
+	done
+	return 1
+}
+get_crm_daemon_dir2() {
+	# CRM_DAEMON_DIR again (brute force)
+	local p d d2
+	for p in /usr /usr/local /opt; do
+		for d in libexec lib64 lib; do
+			for d2 in pacemaker heartbeat; do
+				if [ -x $p/$d/$d2/crmd ]; then
+					debug "setting CRM_DAEMON_DIR to $p/$d/$d2"
+					CRM_DAEMON_DIR=$p/$d/$d2
+					break
+				fi
+			done
+		done
+	done
+}
+compatibility_pcmk() {
+	get_crm_daemon_dir || get_crm_daemon_dir2
+	if [ ! -d "$CRM_DAEMON_DIR" ]; then
+		fatal "cannot find pacemaker daemon directory!"
+	fi
+	get_pe_state_dir || get_pe_state_dir2
+	get_cib_dir || get_cib_dir2
+	debug "setting PCMK_LIB to `dirname $CIB_DIR`"
+	PCMK_LIB=`dirname $CIB_DIR`
+	# PTEST
+	PTEST=`echo_ptest_tool`
+	export PE_STATE_DIR CIB_DIR CRM_DAEMON_DIR PCMK_LIB PTEST
+}
+
+#
+# find log files
+#
+logmark() {
+	logger -p $*
+	debug "run: logger -p $*"
+}
+#
+# first try syslog files, if none found then use the
+# logfile/debugfile settings
+#
+findlog() {
+	local logf=""
+	collect_journal $FROM_TIME $TO_TIME $WORKDIR/$JOURNAL_F
+	if [ "$HA_LOGFACILITY" ]; then
+		logf=`findmsg $UNIQUE_MSG | awk '{print $1}'`
+	fi
+	if [ -f "$logf" ]; then
+		echo $logf
+	elif [ -f "$WORKDIR/$JOURNAL_F" ]; then
+		echo $WORKDIR/$JOURNAL_F
+	else
+		echo ${HA_DEBUGFILE:-$HA_LOGFILE}
+		[ "${HA_DEBUGFILE:-$HA_LOGFILE}" ] &&
+			debug "will try with ${HA_DEBUGFILE:-$HA_LOGFILE}"
+	fi
+}
+
+#
+# find log slices
+#
+
+find_decompressor() {
+	if echo $1 | grep -qs 'bz2$'; then
+		echo "bzip2 -dc"
+	elif echo $1 | grep -qs 'gz$'; then
+		echo "gzip -dc"
+	elif echo $1 | grep -qs 'xz$'; then
+		echo "xz -dc"
+	else
+		echo "cat"
+	fi
+}
+#
+# check if the log contains a piece of our segment
+#
+is_our_log() {
+	local logf=$1
+	local from_time=$2
+	local to_time=$3
+
+	local cat=`find_decompressor $logf`
+	local first_time="`$cat $logf | head -10 | find_first_ts`"
+	local last_time="`$cat $logf | tail -10 | tac | find_first_ts`"
+	if [ x = "x$first_time" -o x = "x$last_time" ]; then
+		return 0 # skip (empty log?)
+	fi
+	if [ $from_time -gt $last_time ]; then
+		# we shouldn't get here anyway if the logs are in order
+		return 2 # we're past good logs; exit
+	fi
+	if [ $from_time -ge $first_time ]; then
+		return 3 # this is the last good log
+	fi
+	# have to go further back
+	if [ $to_time -eq 0 -o $to_time -ge $first_time ]; then
+		return 1 # include this log
+	else
+		return 0 # don't include this log
+	fi
+}
+#
+# go through archived logs (timewise backwards) and see if there
+# are lines belonging to us
+# (we rely on untouched log files, i.e. that modify time
+# hasn't been changed)
+#
+arch_logs() {
+	local next_log
+	local logf=$1
+	local from_time=$2
+	local to_time=$3
+
+	# look for files such as: ha-log-20090308 or
+	# ha-log-20090308.gz (.bz2) or ha-log.0, etc
+	ls -t $logf $logf*[0-9z] 2>/dev/null |
+	while read next_log; do
+		is_our_log $next_log $from_time $to_time
+		case $? in
+		0) ;;  # noop, continue
+		1) echo $next_log  # include log and continue
+			debug "found log $next_log"
+			;;
+		2) break;; # don't go through older logs!
+		3) echo $next_log  # include log and continue
+			debug "found log $next_log"
+			break
+			;; # don't go through older logs!
+		esac
+	done
+}
+#
+# print part of the log
+#
+print_log() {
+	local cat=`find_decompressor $1`
+	$cat $1
+}
+print_logseg() {
+	if test -x $HA_NOARCHBIN/print_logseg; then
+		$HA_NOARCHBIN/print_logseg "$1" "$2" "$3"
+		return
+	fi
+
+	local logf=$1
+	local from_time=$2
+	local to_time=$3
+	local tmp sourcef
+
+	# uncompress to a temp file (if necessary)
+	local cat=`find_decompressor $logf`
+	if [ "$cat" != "cat" ]; then
+		tmp=`mktemp` ||
+			fatal "disk full"
+		add_tmpfiles $tmp
+		$cat $logf > $tmp ||
+			fatal "disk full"
+		sourcef=$tmp
+	else
+		sourcef=$logf
+		tmp=""
+	fi
+
+	if [ "$from_time" = 0 ]; then
+		FROM_LINE=1
+	else
+		FROM_LINE=`findln_by_time $sourcef $from_time`
+	fi
+	if [ -z "$FROM_LINE" ]; then
+		warning "couldn't find line for time $from_time; corrupt log file?"
+		return
+	fi
+
+	TO_LINE=""
+	if [ "$to_time" != 0 ]; then
+		TO_LINE=`findln_by_time $sourcef $to_time`
+		if [ -z "$TO_LINE" ]; then
+			warning "couldn't find line for time $to_time; corrupt log file?"
+			return
+		fi
+	fi
+	dumplog $sourcef $FROM_LINE $TO_LINE
+	debug "including segment [$FROM_LINE-$TO_LINE] from $logf"
+}
+#
+# print some log info (important for crm history)
+#
+loginfo() {
+	local logf=$1
+	local fake=$2
+	local nextpos=`python -c "f=open('$logf');f.seek(0,2);print f.tell()+1"`
+	if [ "$fake" ]; then
+		echo "synthetic:$logf $nextpos"
+	else
+		echo "$logf $nextpos"
+	fi
+}
+#
+# find log/set of logs which are interesting for us
+#
+dumplogset() {
+	local logf=$1
+	local from_time=$2
+	local to_time=$3
+
+	local logf_set=`arch_logs $logf $from_time $to_time`
+	if [ x = "x$logf_set" ]; then
+		return
+	fi
+
+	local num_logs=`echo "$logf_set" | wc -l`
+	local oldest=`echo $logf_set | awk '{print $NF}'`
+	local newest=`echo $logf_set | awk '{print $1}'`
+	local mid_logfiles=`echo $logf_set | awk '{for(i=NF-1; i>1; i--) print $i}'`
+
+	# the first logfile: from $from_time to $to_time (or end)
+	# logfiles in the middle: all
+	# the last logfile: from beginning to $to_time (or end)
+	case $num_logs in
+	1) print_logseg $newest $from_time $to_time;;
+	*)
+		print_logseg $oldest $from_time 0
+		for f in $mid_logfiles; do
+			print_log $f
+			debug "including complete $f logfile"
+		done
+		print_logseg $newest 0 $to_time
+	;;
+	esac
+}
+
+#
+# cts_findlogseg finds lines for the CTS test run (FROM_LINE and
+# TO_LINE) and then extracts the timestamps to get FROM_TIME and
+# TO_TIME
+#
+cts_findlogseg() {
+	local testnum=$1
+	local logf=$2
+	if [ "x$logf" = "x" ]; then
+		logf=`findmsg "Running test.*\[ *$testnum\]" | awk '{print $1}'`
+	fi
+	getstampproc=`find_getstampproc < $logf`
+	export getstampproc # used by linetime
+
+	FROM_LINE=`grep -n "Running test.*\[ *$testnum\]" $logf | tail -1 | sed 's/:.*//'`
+	if [ -z "$FROM_LINE" ]; then
+		warning "couldn't find line for CTS test $testnum; corrupt log file?"
+		exit 1
+	else
+		FROM_TIME=`linetime $logf $FROM_LINE`
+	fi
+	TO_LINE=`grep -n "Running test.*\[ *$(($testnum+1))\]" $logf | tail -1 | sed 's/:.*//'`
+	[ "$TO_LINE" -a $FROM_LINE -lt $TO_LINE ] ||
+		TO_LINE=`wc -l < $logf`
+	TO_TIME=`linetime $logf $TO_LINE`
+	debug "including segment [$FROM_LINE-$TO_LINE] from $logf"
+	dumplog $logf $FROM_LINE $TO_LINE
+}
+
+#
+# this is how we pass environment to other hosts
+#
+dumpenv() {
+	cat<<EOF
+DEST=$DEST
+FROM_TIME=$FROM_TIME
+TO_TIME=$TO_TIME
+USER_NODES="$USER_NODES"
+NODES="$NODES"
+MASTER_NODE="$MASTER_NODE"
+HA_LOG=$HA_LOG
+MASTER_IS_HOSTLOG=$MASTER_IS_HOSTLOG
+UNIQUE_MSG=$UNIQUE_MSG
+SANITIZE="$SANITIZE"
+DO_SANITIZE="$DO_SANITIZE"
+SKIP_LVL="$SKIP_LVL"
+EXTRA_LOGS="$EXTRA_LOGS"
+USER_CLUSTER_TYPE="$USER_CLUSTER_TYPE"
+CONF="$CONF"
+B_CONF="$B_CONF"
+PACKAGES="$PACKAGES"
+CORES_DIRS="$CORES_DIRS"
+VERBOSITY="$VERBOSITY"
+EOF
+}
+is_collector() {
+	test "$SLAVE"
+}
+is_node() {
+	test "$THIS_IS_NODE"
+}
+is_master() {
+	! is_collector && test "$WE" = "$MASTER_NODE"
+}
+start_slave_collector() {
+	local node=$1
+
+	dumpenv |
+	if [ "$node" = "$WE" ]; then
+		debug "running: $LOCAL_SUDO hb_report __slave"
+		$LOCAL_SUDO @datadir@/@PACKAGE_NAME@/hb_report __slave
+	else
+		debug "running: ssh $SSH_OPTS $node \"$SUDO hb_report __slave"
+		ssh $SSH_OPTS $node \
+			"$SUDO @datadir@/@PACKAGE_NAME@/hb_report __slave"
+	fi | (cd $WORKDIR && tar xf -)
+}
+
+#
+# does ssh work?
+# and how
+# test the provided ssh user
+# or try to find a ssh user which works without password
+# if ssh works without password, we can run the collector in the
+# background and save some time
+#
+testsshconn() {
+	ssh $SSH_OPTS -T -o Batchmode=yes $1 true 2>/dev/null
+}
+findsshuser() {
+	local n u rc
+	local ssh_s ssh_user="__undef" try_user_list
+	if [ -z "$SSH_USER" ]; then
+		try_user_list="__default $TRY_SSH"
+	else
+		try_user_list="$SSH_USER"
+	fi
+	for n in $NODES; do
+		rc=1
+		[ "$n" = "$WE" ] && continue
+		for u in $try_user_list; do
+			if [ "$u" != '__default' ]; then
+				ssh_s=$u@$n
+			else
+				ssh_s=$n
+			fi
+			if testsshconn $ssh_s; then
+				debug "ssh $ssh_s OK"
+				ssh_user="$u"
+				try_user_list="$u" # we support just one user
+				rc=0
+				break
+			else
+				debug "ssh $ssh_s failed"
+			fi
+		done
+		[ $rc = 1 ] &&
+			SSH_PASSWORD_NODES="$SSH_PASSWORD_NODES $n"
+	done
+	if [ -n "$SSH_PASSWORD_NODES" ]; then
+		warning "passwordless ssh to node(s) $SSH_PASSWORD_NODES does not work"
+	fi
+
+	if [ "$ssh_user" = "__undef" ]; then
+		return 1
+	fi
+	if [ "$ssh_user" != "__default" ]; then
+		SSH_USER=$ssh_user
+	fi
+	return 0
+}
+node_needs_pwd() {
+	local n
+	for n in $SSH_PASSWORD_NODES; do
+		[ "$n" = "$1" ] && return 0
+	done
+	return 1
+}
+say_ssh_user() {
+	if [ -n "$SSH_USER" ]; then
+		echo $SSH_USER
+	else
+		echo your user
+	fi
+}
+
+#
+# the usual stuff
+#
+getbacktraces() {
+	local f bf flist
+	flist=$(
+		for f in `find_files "$CORES_DIRS" $1 $2`; do
+			bf=`basename $f`
+			test `expr match $bf core` -gt 0 &&
+				echo $f
+		done)
+	[ "$flist" ] && {
+		getbt $flist > $3
+		debug "found backtraces: $flist"
+	}
+}
+pe2dot() {
+	local pef=`basename $1`
+	local dotf=`basename $pef .bz2`.dot
+	test -z "$PTEST" && return
+	(
+	cd `dirname $1`
+	$PTEST -D $dotf -x $pef >/dev/null 2>&1
+	)
+}
+getpeinputs() {
+	local pe_dir flist
+	local f
+	pe_dir=$PE_STATE_DIR
+	debug "looking for PE files in $pe_dir"
+	flist=$(
+		find_files $pe_dir $1 $2 | grep -v '[.]last$'
+	)
+	[ "$flist" ] && {
+		mkdir $3/`basename $pe_dir`
+		(
+		cd $3/`basename $pe_dir`
+		for f in $flist; do
+			ln -s $f
+		done
+		)
+		debug "found `echo $flist | wc -w` pengine input files in $pe_dir"
+	}
+	if [ `echo $flist | wc -w` -le 20 ]; then
+		for f in $flist; do
+			skip_lvl 1 || pe2dot $3/`basename $pe_dir`/`basename $f`
+		done
+	else
+		debug "too many PE inputs to create dot files"
+	fi
+}
+getratraces() {
+	local trace_dir flist
+	local f
+	trace_dir=$HA_VARLIB/trace_ra
+	test -d "$trace_dir" || return 0
+	debug "looking for RA trace files in $trace_dir"
+	flist=$(find_files $trace_dir $1 $2 | sed "s,`dirname $trace_dir`/,,g")
+	[ "$flist" ] && {
+		tar -cf - -C `dirname $trace_dir` $flist | tar -xf - -C $3
+		debug "found `echo $flist | wc -w` RA trace files in $trace_dir"
+	}
+}
+touch_DC_if_dc() {
+	local dc
+	dc=`crmadmin -D 2>/dev/null | awk '{print $NF}'`
+	if [ "$WE" = "$dc" ]; then
+		touch $1/DC
+	fi
+}
+corosync_blackbox() {
+	local from_time=$1
+	local to_time=$2
+	local outf=$3
+	local inpf
+	inpf=`find_files /var/lib/corosync $from_time $to_time | grep -w fdata`
+	if [ -f "$inpf" ]; then
+		corosync-blackbox > $outf
+		touch -r $inpf $outf
+	fi
+}
+getconfigurations() {
+	local conf
+	local dest=$1
+	for conf in $CONFIGURATIONS; do
+		if [ -f $conf ]; then
+			cp -p $conf $dest
+		elif [ -d $conf ]; then
+			(
+			cd `dirname $conf` &&
+			tar cf - `basename $conf` | (cd $dest && tar xf -)
+			)
+		fi
+	done
+}
+
+
+#
+# some basic system info and stats
+#
+sys_info() {
+	cluster_info
+	@datadir@/@PACKAGE_NAME@/hb_report -V # our info
+	echo "resource-agents: `grep 'Build version:' @OCF_ROOT_DIR@/lib/heartbeat/ocf-shellfuncs`"
+	crm_info
+	pkg_versions $PACKAGES
+	skip_lvl 1 || verify_packages $PACKAGES
+	echo "Platform: `uname`"
+	echo "Kernel release: `uname -r`"
+	echo "Architecture: `uname -m`"
+	[ `uname` = Linux ] &&
+		echo "Distribution: `distro`"
+}
+sys_stats() {
+	set -x
+	uname -n
+	uptime
+	ps axf
+	ps auxw
+	top -b -n 1
+	ip addr
+	netstat -i
+	arp -an
+	test -d /proc && {
+		cat /proc/cpuinfo
+	}
+	lsscsi
+	lspci
+	mount
+	# df can block, run in background, allow for 5 seconds (!)
+	local maxcnt=5
+	df &
+	while kill -0 $! >/dev/null 2>&1; do
+		sleep 1
+		if [ $maxcnt -le 0 ]; then
+			warning "df appears to be hanging, continuing without it"
+			break
+		fi
+		maxcnt=$((maxcnt-1))
+	done
+	set +x
+}
+time_status() {
+	date
+	ntpdc -pn
+}
+dlm_dump() {
+	if which dlm_tool >/dev/null 2>&1 ; then
+		echo NOTICE - Lockspace overview:
+		dlm_tool ls
+		dlm_tool ls | grep name |
+			while read X N ; do 
+				echo NOTICE - Lockspace $N:
+				dlm_tool lockdump $N
+			done
+		echo NOTICE - Lockspace history:
+		dlm_tool dump
+	fi
+}
+
+
+#
+# replace sensitive info with '****'
+#
+sanitize() {
+	local f rc
+	for f in $1/$B_CONF; do
+		[ -f "$f" ] && sanitize_one $f
+	done
+	rc=0
+	for f in $1/$CIB_F $1/pengine/*; do
+		if [ -f "$f" ]; then
+			if [ "$DO_SANITIZE" ]; then
+				sanitize_one $f
+			else
+				test_sensitive_one $f && rc=1
+			fi
+		fi
+	done
+	[ $rc -ne 0 ] && {
+		warning "some PE or CIB files contain possibly sensitive data"
+		warning "you may not want to send this report to a public mailing list"
+	}
+}
+
+#
+# remove duplicates if files are same, make links instead
+#
+consolidate() {
+	for n in $NODES; do
+		if [ -f $1/$2 ]; then
+			rm $1/$n/$2
+		else
+			mv $1/$n/$2 $1
+		fi
+		ln -s ../$2 $1/$n
+	done
+}
+
+#
+# some basic analysis of the report
+#
+checkcrmvfy() {
+	for n in $NODES; do
+		if [ -s $1/$n/$CRM_VERIFY_F ]; then
+			echo "WARN: crm_verify reported warnings at $n:"
+			cat $1/$n/$CRM_VERIFY_F
+		fi
+	done
+}
+checkbacktraces() {
+	for n in $NODES; do
+		[ -s $1/$n/$BT_F ] && {
+			echo "WARN: coredumps found at $n:"
+			egrep 'Core was generated|Program terminated' \
+					$1/$n/$BT_F |
+				sed 's/^/	/'
+		}
+	done
+}
+checkpermissions() {
+	for n in $NODES; do
+		if [ -s $1/$n/$PERMISSIONS_F ]; then
+			echo "WARN: problem with permissions/ownership at $n:"
+			cat $1/$n/$PERMISSIONS_F
+		fi
+	done
+}
+checklogs() {
+	local logs pattfile l n
+	logs=$(find $1 -name $HALOG_F;
+		for l in $EXTRA_LOGS; do find $1/* -name `basename $l`; done)
+	[ "$logs" ] || return
+	pattfile=`mktemp` ||
+		fatal "cannot create temporary files"
+	add_tmpfiles $pattfile
+	for p in $LOG_PATTERNS; do
+		echo "$p"
+	done > $pattfile
+	echo ""
+	echo "Log patterns:"
+	for n in $NODES; do
+		cat $logs | grep -f $pattfile
+	done
+}
+
+#
+# check if files have same content in the cluster
+#
+cibdiff() {
+	local d1 d2
+	d1=`dirname $1`
+	d2=`dirname $2`
+	if [ -f $d1/RUNNING -a -f $d2/RUNNING ] ||
+		[ -f $d1/STOPPED -a -f $d2/STOPPED ]; then
+		if which crm_diff > /dev/null 2>&1; then
+			crm_diff -c -n $1 -o $2
+		else
+			info "crm_diff(8) not found, cannot diff CIBs"
+		fi
+	else
+		echo "can't compare cibs from running and stopped systems"
+	fi
+}
+txtdiff() {
+	diff -bBu $1 $2
+}
+diffcheck() {
+	[ -f "$1" ] || {
+		echo "$1 does not exist"
+		return 1
+	}
+	[ -f "$2" ] || {
+		echo "$2 does not exist"
+		return 1
+	}
+	case `basename $1` in
+	$CIB_F)
+		cibdiff $1 $2;;
+	$B_CONF)
+		txtdiff $1 $2;; # confdiff?
+	*)
+		txtdiff $1 $2;;
+	esac
+}
+analyze_one() {
+	local rc node0 n
+	rc=0
+	node0=""
+	for n in $NODES; do
+		if [ "$node0" ]; then
+			diffcheck $1/$node0/$2 $1/$n/$2
+			rc=$(($rc+$?))
+		else
+			node0=$n
+		fi
+	done
+	return $rc
+}
+analyze() {
+	local f flist
+	flist="$HOSTCACHE $MEMBERSHIP_F $CIB_F $CRM_MON_F $B_CONF logd.cf $SYSINFO_F"
+	for f in $flist; do
+		printf "Diff $f... "
+		ls $1/*/$f >/dev/null 2>&1 || {
+			echo "no $1/*/$f :/"
+			continue
+		}
+		if analyze_one $1 $f; then
+			echo "OK"
+			[ "$f" != $CIB_F ] &&
+				consolidate $1 $f
+		else
+			echo ""
+		fi
+	done
+	checkcrmvfy $1
+	checkbacktraces $1
+	checkpermissions $1
+	checklogs $1
+}
+events_all() {
+	local Epatt title p
+	Epatt=`echo "$EVENT_PATTERNS" |
+		while read title p; do [ -n "$p" ] && echo -n "|$p"; done |
+		sed 's/.//'
+	`
+	grep -E "$Epatt" $1
+}
+events() {
+	local destdir n
+	destdir=$1
+	if [ -f $destdir/$HALOG_F ]; then
+		events_all $destdir/$HALOG_F > $destdir/events.txt
+		for n in $NODES; do
+			awk "\$4==\"$n\"" $destdir/events.txt > $destdir/$n/events.txt
+		done
+	else
+		for n in $NODES; do
+			[ -f $destdir/$n/$HALOG_F ] ||
+				continue
+			events_all $destdir/$n/$HALOG_F > $destdir/$n/events.txt
+		done
+	fi
+}
+
+#
+# description template, editing, and other notes
+#
+mktemplate() {
+	cat<<EOF
+Please edit this template and describe the issue/problem you
+encountered. Then, post to
+	http://clusterlabs.org/mailman/listinfo/users
+or file a bug at
+	https://github.com/ClusterLabs/crmsh/issues
+
+Thank you.
+
+Date: `date`
+By: $PROG $userargs
+Subject: [short problem description]
+Severity: [choose one] enhancement minor normal major critical blocking
+Component: [choose one] CRM LRM CCM RA fencing $CLUSTER_TYPE comm GUI tools other
+
+Detailed description:
+---
+[...]
+---
+
+EOF
+
+	if [ -f $WORKDIR/$SYSINFO_F ]; then
+		echo "Common system info found:"
+		cat $WORKDIR/$SYSINFO_F
+	else
+		for n in $NODES; do
+			if [ -f $WORKDIR/$n/$SYSINFO_F ]; then
+				echo "System info $n:"
+				sed 's/^/	/' $WORKDIR/$n/$SYSINFO_F
+			fi
+		done
+	fi
+}
+edittemplate() {
+	local ec
+	if ec=`pickfirst $EDITOR vim vi emacs nano`; then
+		$ec $1
+	else
+		warning "could not find a text editor"
+	fi
+}
+pickcompress() {
+	if COMPRESS_PROG=`pickfirst bzip2 gzip xz`; then
+		if [ "$COMPRESS_PROG" = xz ]; then
+			COMPRESS_EXT=.xz
+		elif [ "$COMPRESS_PROG" = bzip2 ]; then
+			COMPRESS_EXT=.bz2
+		else
+			COMPRESS_EXT=.gz
+		fi
+	else
+		warning "could not find a compression program; the resulting tarball may be huge"
+		COMPRESS_PROG=cat
+		COMPRESS_EXT=
+	fi
+}
+# get the right part of the log
+getlog() {
+	local cnt
+	local outf
+	outf=$WORKDIR/$HALOG_F
+
+	if [ "$HA_LOG" ]; then  # log provided by the user?
+		[ -f "$HA_LOG" ] || {  # not present
+			is_collector ||  # warning if not on slave
+				warning "$HA_LOG not found; we will try to find log ourselves"
+			HA_LOG=""
+		}
+	fi
+	if [ "$HA_LOG" = "" ]; then
+		HA_LOG=`findlog`
+		[ "$HA_LOG" ] &&
+			cnt=`fgrep -c $UNIQUE_MSG < $HA_LOG`
+	fi
+	if [ "$HA_LOG" = "" -o ! -f "$HA_LOG" ]; then
+		if [ "$CTS" ]; then
+			cts_findlogseg $CTS > $outf
+		else
+			warning "no log at $WE"
+		fi
+		return
+	fi
+	if [ "$cnt" ] && [ $cnt -gt 1 -a $cnt -eq $NODECNT ]; then
+		MASTER_IS_HOSTLOG=1
+		info "found the central log!"
+	fi
+
+	if [ "$NO_str2time" ]; then
+		warning "a log found; but we cannot slice it"
+		warning "please install the perl Date::Parse module"
+	elif [ "$CTS" ]; then
+		cts_findlogseg $CTS $HA_LOG > $outf
+	else
+		getstampproc=`find_getstampproc < $HA_LOG`
+		if [ "$getstampproc" ]; then
+			export getstampproc # used by linetime
+			dumplogset $HA_LOG $FROM_TIME $TO_TIME > $outf &&
+				loginfo $HA_LOG > $outf.info ||
+				fatal "disk full"
+		else
+			warning "could not figure out the log format of $HA_LOG"
+		fi
+	fi
+}
+collect_journal() {
+	local from_time to_time outf
+	from_time="$1"
+	to_time="$2"
+	outf="$3"
+	if which journalctl > /dev/null 2>&1; then
+		if isnumber "$from_time" && [ $from_time -eq 0 ]; then
+			from_time=$(date "+%Y-%m-%d %H:%M")
+		elif isnumber "$from_time"; then
+			from_time=$(echo "$from_time" | awk '{ print strftime("%Y-%m-%d %H:%M", $1); }')
+		fi
+		if isnumber "$to_time" && [ $to_time -eq 0 ]; then
+			to_time=$(date "+%Y-%m-%d %H:%M")
+		elif isnumber "$to_time"; then
+			to_time=$(echo "$to_time" | awk '{ print strftime("%Y-%m-%d %H:%M", $1); }')
+		fi
+		if [ -f $outf ]; then
+			warning "$outf already exists"
+		fi
+		debug "journalctl from: '$1' until: '$2' from_time '$from_time' to_time: '$to_time' > $outf"
+		journalctl --since "$from_time" --until "$to_time" --no-pager | tail -n +2 > $outf
+	fi
+}
+#
+# get all other info (config, stats, etc)
+#
+collect_info() {
+	local l
+	sys_info > $WORKDIR/$SYSINFO_F 2>&1 &
+	sys_stats > $WORKDIR/$SYSSTATS_F 2>&1 &
+	getconfig $WORKDIR
+	getpeinputs $FROM_TIME $TO_TIME $WORKDIR &
+	crmconfig $WORKDIR &
+	skip_lvl 1 || touch_DC_if_dc $WORKDIR &
+	getbacktraces $FROM_TIME $TO_TIME $WORKDIR/$BT_F
+	getconfigurations $WORKDIR
+	check_perms > $WORKDIR/$PERMISSIONS_F 2>&1
+	dlm_dump > $WORKDIR/$DLM_DUMP_F 2>&1
+	time_status > $WORKDIR/$TIME_F 2>&1
+	corosync_blackbox $FROM_TIME $TO_TIME $WORKDIR/$COROSYNC_RECORDER_F
+	getratraces $FROM_TIME $TO_TIME $WORKDIR
+	wait
+	skip_lvl 1 || sanitize $WORKDIR
+
+	for l in $EXTRA_LOGS; do
+		[ "$NO_str2time" ] && break
+		[ ! -f "$l" ] && continue
+		if [ "$l" = "$HA_LOG" -a "$l" != "$HALOG_F" ]; then
+			ln -s $HALOG_F $WORKDIR/`basename $l`
+			continue
+		fi
+		getstampproc=`find_getstampproc < $l`
+		if [ "$getstampproc" ]; then
+			export getstampproc # used by linetime
+			dumplogset $l $FROM_TIME $TO_TIME > $WORKDIR/`basename $l` &&
+				loginfo $l > $WORKDIR/`basename $l`.info ||
+				fatal "disk full"
+		else
+			warning "could not figure out the log format of $l"
+		fi
+	done
+}
+finalword() {
+	if [ "$COMPRESS" = "1" ]; then
+		echo "The report is saved in $DESTDIR/$DEST.tar$COMPRESS_EXT"
+	else
+		echo "The report is saved in $DESTDIR/$DEST"
+	fi
+	echo " "
+	echo "Thank you for taking time to create this report."
+}
+
+[ $# -eq 0 ] && usage
+
+# check for the major prereq for a) parameter parsing and b)
+# parsing logs
+#
+NO_str2time=""
+t=`str2time "12:00"`
+if [ "$t" = "" ]; then
+	NO_str2time=1
+	is_collector ||
+		fatal "please install the perl Date::Parse module"
+fi
+
+which which >/dev/null 2>&1 ||
+	fatal "please install the which(1) program"
+
+WE=`uname -n`  # who am i?
+tmpdir=`mktemp -t -d .hb_report.workdir.XXXXXX` ||
+	fatal "disk full"
+add_tmpfiles $tmpdir
+WORKDIR=$tmpdir
+
+#
+# part 1: get and check options; and the destination
+#
+if ! is_collector; then
+	setvarsanddefaults
+	userargs="$@"
+	DESTDIR=.
+	DEST="hb_report-"`date +"%a-%d-%b-%Y"`
+	while getopts f:t:l:u:X:p:L:e:E:n:MSDCZAVsvhdQ o; do
+		case "$o" in
+			h) usage;;
+			V) version;;
+			f)
+				if echo "$OPTARG" | grep -qs '^cts:'; then
+					FROM_TIME=0 # to be calculated later
+					CTS=`echo "$OPTARG" | sed 's/cts://'`
+					DEST="cts-$CTS-"`date +"%a-%d-%b-%Y"`
+				else
+					FROM_TIME=`str2time "$OPTARG"`
+					chktime "$FROM_TIME" "$OPTARG"
+				fi
+			;;
+			t) TO_TIME=`str2time "$OPTARG"`
+				chktime "$TO_TIME" "$OPTARG"
+			;;
+			n) NODES_SOURCE=user
+				USER_NODES="$USER_NODES $OPTARG"
+			;;
+			u) SSH_USER="$OPTARG";;
+			X) SSH_OPTS="$SSH_OPTS $OPTARG";;
+			l) HA_LOG="$OPTARG";;
+			e) EDITOR="$OPTARG";;
+			p) SANITIZE="$SANITIZE $OPTARG";;
+			s) DO_SANITIZE="1";;
+			Q) SKIP_LVL=$((SKIP_LVL + 1));;
+			L) LOG_PATTERNS="$LOG_PATTERNS $OPTARG";;
+			S) NO_SSH=1;;
+			D) NO_DESCRIPTION=1;;
+			C) : ;;
+			Z) FORCE_REMOVE_DEST=1;;
+			M) EXTRA_LOGS="";;
+			E) EXTRA_LOGS="$EXTRA_LOGS $OPTARG";;
+			A) USER_CLUSTER_TYPE="openais";;
+			v) VERBOSITY=$((VERBOSITY + 1));;
+			d) COMPRESS="";;
+			[?]) usage short;;
+		esac
+	done
+	shift $(($OPTIND-1))
+	[ $# -gt 1 ] && usage short
+	set_dest $*
+	[ "$FROM_TIME" ] || usage short
+	WORKDIR=$tmpdir/$DEST
+else
+	WORKDIR=$tmpdir/$DEST/$WE
+fi
+
+mkdir -p $WORKDIR
+[ -d $WORKDIR ] || no_dir
+
+if is_collector; then
+	cat > $WORKDIR/.env
+	. $WORKDIR/.env
+fi
+
+[ $VERBOSITY -gt 1 ] && {
+	is_collector || {
+		info "high debug level, please read debug.out"
+	}
+	PS4='+ `date +"%T"`: ${FUNCNAME[0]:+${FUNCNAME[0]}:}${LINENO}: '
+	if echo "$SHELL" | grep bash > /dev/null &&
+			[ ${BASH_VERSINFO[0]} = "4" ]; then
+		exec 3>>$WORKDIR/debug.out
+		BASH_XTRACEFD=3
+	else
+		exec 2>>$WORKDIR/debug.out
+	fi
+	set -x
+}
+
+compatibility_pcmk
+
+# allow user to enforce the cluster type
+# if not, then it is found out on _all_ nodes
+if [ -z "$USER_CLUSTER_TYPE" ]; then
+	CLUSTER_TYPE=`get_cluster_type`
+else
+	CLUSTER_TYPE=$USER_CLUSTER_TYPE
+fi
+
+# the very first thing we must figure out is which cluster
+# stack is used
+CORES_DIRS="`2>/dev/null ls -d $HA_VARLIB/cores $PCMK_LIB/cores | uniq`"
+PACKAGES="pacemaker libpacemaker3 
+pacemaker-pygui pacemaker-pymgmt pymgmt-client
+openais libopenais2 libopenais3 corosync libcorosync4
+resource-agents cluster-glue libglue2 ldirectord
+heartbeat heartbeat-common heartbeat-resources libheartbeat2
+ocfs2-tools ocfs2-tools-o2cb ocfs2console
+ocfs2-kmp-default ocfs2-kmp-pae ocfs2-kmp-xen ocfs2-kmp-debug ocfs2-kmp-trace
+drbd drbd-kmp-xen drbd-kmp-pae drbd-kmp-default drbd-kmp-debug drbd-kmp-trace
+drbd-heartbeat drbd-pacemaker drbd-utils drbd-bash-completion drbd-xen
+lvm2 lvm2-clvm cmirrord
+libdlm libdlm2 libdlm3
+hawk ruby lighttpd
+kernel-default kernel-pae kernel-xen
+glibc
+"
+case "$CLUSTER_TYPE" in
+openais)
+	CONF=/etc/corosync/corosync.conf # corosync?
+	if test -f $CONF; then
+		CORES_DIRS="$CORES_DIRS /var/lib/corosync"
+	else
+		CONF=/etc/ais/openais.conf
+		CORES_DIRS="$CORES_DIRS /var/lib/openais"
+	fi
+	CF_SUPPORT=$HA_NOARCHBIN/openais_conf_support.sh
+	MEMBERSHIP_TOOL_OPTS=""
+	unset HOSTCACHE HB_UUID_F
+	;;
+heartbeat)
+	CONF=$HA_CF
+	CF_SUPPORT=$HA_NOARCHBIN/ha_cf_support.sh
+	MEMBERSHIP_TOOL_OPTS="-H"
+	;;
+esac
+B_CONF=`basename $CONF`
+
+if test -f "$CF_SUPPORT"; then
+	. $CF_SUPPORT
+else
+	fatal "no stack specific support: $CF_SUPPORT"
+fi
+
+if [ "x$CTS" = "x" ] || is_collector; then
+	getlogvars
+	debug "log settings: facility=$HA_LOGFACILITY logfile=$HA_LOGFILE debugfile=$HA_DEBUGFILE"
+elif ! is_collector; then
+	ctslog=`findmsg "CTS: Stack:" | awk '{print $1}'`
+	debug "Using CTS control file: $ctslog"
+	USER_NODES=`grep CTS: $ctslog | grep -v debug: | grep " \* " | sed s:.*\\\*::g | sort -u  | tr '\\n' ' '`
+	HA_LOGFACILITY=`findmsg "CTS:.*Environment.SyslogFacility" | awk '{print $NF}'`
+	NODES_SOURCE=user
+fi
+
+# the goods
+ANALYSIS_F=analysis.txt
+DESCRIPTION_F=description.txt
+HALOG_F=ha-log.txt
+JOURNAL_F=journal.log
+BT_F=backtraces.txt
+SYSINFO_F=sysinfo.txt
+SYSSTATS_F=sysstats.txt
+DLM_DUMP_F=dlm_dump.txt
+TIME_F=time.txt
+export ANALYSIS_F DESCRIPTION_F HALOG_F JOURNAL_F BT_F SYSINFO_F SYSSTATS_F DLM_DUMP_F TIME_F
+CRM_MON_F=crm_mon.txt
+MEMBERSHIP_F=members.txt
+HB_UUID_F=hb_uuid.txt
+HOSTCACHE=hostcache
+CRM_VERIFY_F=crm_verify.txt
+PERMISSIONS_F=permissions.txt
+CIB_F=cib.xml
+CIB_TXT_F=cib.txt
+COROSYNC_RECORDER_F=fdata.txt
+export CRM_MON_F MEMBERSHIP_F CRM_VERIFY_F CIB_F CIB_TXT_F HB_UUID_F PERMISSIONS_F
+export COROSYNC_RECORDER_F
+CONFIGURATIONS="/etc/drbd.conf /etc/drbd.d /etc/booth/booth.conf"
+export CONFIGURATIONS
+
+THIS_IS_NODE=""
+if ! is_collector; then
+	MASTER_NODE=$WE
+	NODES=`getnodes`
+	debug "nodes: `echo $NODES`"
+fi
+NODECNT=`echo $NODES | wc -w`
+if [ "$NODECNT" = 0 ]; then
+	fatal "could not figure out a list of nodes; is this a cluster node?"
+fi
+if echo $NODES | grep -wqs $WE; then # are we a node?
+	THIS_IS_NODE=1
+fi
+
+# this only on master
+if ! is_collector; then
+
+	# if this is not a node, then some things afterwards might
+	# make no sense (not work)
+	if ! is_node && [ "$NODES_SOURCE" != user ]; then
+		warning "this is not a node and you didn't specify a list of nodes using -n"
+	fi
+#
+# part 2: ssh business
+#
+	# find out if ssh works
+	if [ -z "$NO_SSH" ]; then
+		# if the ssh user was supplied, consider that it
+		# works; helps reduce the number of ssh invocations
+		findsshuser
+		if [ -n "$SSH_USER" ]; then
+			SSH_OPTS="$SSH_OPTS -o User=$SSH_USER"
+		fi
+	fi
+	# assume that only root can collect data
+	SUDO=""
+	if [ -z "$SSH_USER" -a `id -u` != 0 ] ||
+			[ -n "$SSH_USER" -a "$SSH_USER" != root ]; then
+		debug "ssh user other than root, use sudo"
+		SUDO="sudo -u root"
+	fi
+	LOCAL_SUDO=""
+	if [ `id -u` != 0 ]; then
+		debug "local user other than root, use sudo"
+		LOCAL_SUDO="sudo -u root"
+	fi
+fi
+
+if is_collector && [ "$HA_LOGFACILITY" ]; then
+	logmark $HA_LOGFACILITY.$HA_LOGLEVEL $UNIQUE_MSG
+	# allow for the log message to get (hopefully) written to the
+	# log file
+	sleep 1
+fi
+
+#
+# part 4: find the logs and cut out the segment for the period
+#
+
+# if the master is also a node, getlog is going to be invoked
+# from the collector
+(is_master && is_node) ||
+	getlog
+
+if ! is_collector; then
+	for node in $NODES; do
+		if node_needs_pwd $node; then
+			info "Please provide password for `say_ssh_user` at $node"
+			info "Note that collecting data will take a while."
+			start_slave_collector $node
+		else
+			start_slave_collector $node &
+			SLAVEPIDS="$SLAVEPIDS $!"
+		fi
+	done
+fi
+
+#
+# part 5: endgame:
+#         slaves tar their results to stdout, the master waits
+#         for them, analyses results, asks the user to edit the
+#         problem description template, and prints final notes
+#
+if is_collector; then
+	collect_info
+	(cd $WORKDIR/.. && tar -h -cf - $WE)
+else
+	if [ -n "$SLAVEPIDS" ]; then
+		wait $SLAVEPIDS
+	fi
+	analyze $WORKDIR > $WORKDIR/$ANALYSIS_F &
+	events $WORKDIR &
+	mktemplate > $WORKDIR/$DESCRIPTION_F
+	[ "$NO_DESCRIPTION" ] || {
+		echo press enter to edit the problem description...
+		read junk
+		edittemplate $WORKDIR/$DESCRIPTION_F
+	}
+	wait
+	if [ "$COMPRESS" = "1" ]; then
+		pickcompress
+		(cd $WORKDIR/.. && tar cf - $DEST) | $COMPRESS_PROG > $DESTDIR/$DEST.tar$COMPRESS_EXT
+	else
+		mv $WORKDIR $DESTDIR
+	fi
+	finalword
+fi
diff --git a/hb_report/openais_conf_support.sh b/hb_report/openais_conf_support.sh
new file mode 100644
index 0000000..b96d1aa
--- /dev/null
+++ b/hb_report/openais_conf_support.sh
@@ -0,0 +1,97 @@
+ # Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+ # 
+ # 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.1 of the License, or (at your option) any later version.
+ # 
+ # This software 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 library; if not, write to the Free Software
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ #
+
+#
+# Stack specific part (openais)
+# openais.conf/logd.cf parsing
+#
+# cut out a stanza
+getstanza() {
+	awk -v name="$1" '
+	!in_stanza && NF==2 && /^[a-z][a-z]*[[:space:]]*{/ { # stanza start
+		if ($1 == name)
+			in_stanza = 1
+	}
+	in_stanza { print }
+	in_stanza && NF==1 && $1 == "}" { exit }
+	'
+}
+# supply stanza in $1 and variable name in $2
+# (stanza is optional)
+getcfvar() {
+	[ -f "$CONF" ] || return
+	sed 's/#.*//' < $CONF |
+		if [ $# -eq 2 ]; then
+			getstanza "$1"
+			shift 1
+		else
+			cat
+		fi |
+		awk -v varname="$1" '
+		NF==2 && match($1,varname":$")==1 { print $2; exit; }
+		'
+}
+iscfvarset() {
+	test "`getcfvar $1`"
+}
+iscfvartrue() {
+	getcfvar $1 $2 |
+		egrep -qsi "^(true|y|yes|on|1)"
+}
+uselogd() {
+	iscfvartrue use_logd
+}
+get_ais_logvars() {
+	if iscfvartrue to_file; then
+		HA_LOGFILE=`getcfvar logfile`
+		HA_LOGFILE=${HA_LOGFILE:-"syslog"}
+		HA_DEBUGFILE=$HA_LOGFILE
+	elif iscfvartrue to_syslog; then
+		HA_LOGFACILITY=`getcfvar syslog_facility`
+		HA_LOGFACILITY=${HA_LOGFACILITY:-"daemon"}
+	fi
+}
+getlogvars() {
+	HA_LOGFACILITY=${HA_LOGFACILITY:-$DEFAULT_HA_LOGFACILITY}
+	HA_LOGLEVEL="info"
+	iscfvartrue debug && # prefer debug level if set
+		HA_LOGLEVEL="debug"
+	if uselogd; then
+		[ -f "$LOGD_CF" ] || {
+			debug "logd used but logd.cf not found: using defaults"
+			return  # no configuration: use defaults
+		}
+		debug "reading log settings from $LOGD_CF"
+		get_logd_logvars
+	else
+		debug "reading log settings from $CONF"
+		get_ais_logvars
+	fi
+}
+cluster_info() {
+	: echo "openais version: how?"
+	if [ "$CONF" = /etc/corosync/corosync.conf ]; then
+		/usr/sbin/corosync -v
+	fi
+}
+essential_files() {
+	cat<<EOF
+d $PCMK_LIB 0750 hacluster haclient
+d $PE_STATE_DIR 0750 hacluster haclient
+d $CIB_DIR 0750 hacluster haclient
+EOF
+}
diff --git a/hb_report/utillib.sh b/hb_report/utillib.sh
new file mode 100644
index 0000000..0fcab80
--- /dev/null
+++ b/hb_report/utillib.sh
@@ -0,0 +1,752 @@
+ # Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+ # 
+ # 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.1 of the License, or (at your option) any later version.
+ # 
+ # This software 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 library; if not, write to the Free Software
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ #
+
+#
+# figure out the cluster type, depending on the process list
+# and existence of configuration files
+#
+get_cluster_type() {
+	if ps -ef | egrep -qs '[a]isexec|[c]orosync' ||
+			[ -f /etc/ais/openais.conf -a ! -f "$HA_CF" ] ||
+			[ -f /etc/corosync/corosync.conf -a ! -f "$HA_CF" ]
+	then
+		debug "this is OpenAIS cluster stack"
+		echo "openais"
+	else
+		debug "this is Heartbeat cluster stack"
+		echo "heartbeat"
+	fi
+}
+#
+# find out which membership tool is installed
+#
+echo_membership_tool() {
+	local f membership_tools
+	membership_tools="ccm_tool crm_node"
+	for f in $membership_tools; do
+		which $f 2>/dev/null && break
+	done
+}
+# find out if ptest or crm_simulate
+#
+echo_ptest_tool() {
+	local f ptest_progs
+	ptest_progs="crm_simulate ptest"
+	for f in $ptest_progs; do
+		which $f 2>/dev/null && break
+	done
+}
+#
+# find nodes for this cluster
+#
+getnodes() {
+	# 1. set by user?
+	if [ "$USER_NODES" ]; then
+		echo $USER_NODES
+	# 2. running crm
+	elif iscrmrunning; then
+		debug "querying CRM for nodes"
+		get_crm_nodes
+	# 3. hostcache
+	elif [ -f $HA_VARLIB/hostcache ]; then
+		debug "reading nodes from $HA_VARLIB/hostcache"
+		awk '{print $1}' $HA_VARLIB/hostcache
+	# 4. ha.cf
+	elif [ "$CLUSTER_TYPE" = heartbeat ]; then
+		debug "reading nodes from ha.cf"
+		getcfvar node
+	# 5. if the cluster's stopped, try the CIB
+	elif [ -f $CIB_DIR/$CIB_F ]; then
+		debug "reading nodes from the archived $CIB_DIR/$CIB_F"
+		(CIB_file=$CIB_DIR/$CIB_F get_crm_nodes)
+	fi
+}
+
+logd_getcfvar() {
+	sed 's/#.*//' < $LOGD_CF |
+		grep -w "^$1" |
+		sed 's/^[^[:space:]]*[[:space:]]*//'
+}
+get_logd_logvars() {
+	# unless logfacility is set to none, heartbeat/ha_logd are
+	# going to log through syslog
+	HA_LOGFACILITY=`logd_getcfvar logfacility`
+	[ "" = "$HA_LOGFACILITY" ] && HA_LOGFACILITY=$DEFAULT_HA_LOGFACILITY
+	[ none = "$HA_LOGFACILITY" ] && HA_LOGFACILITY=""
+	HA_LOGFILE=`logd_getcfvar logfile`
+	HA_DEBUGFILE=`logd_getcfvar debugfile`
+}
+findlogdcf() {
+	local f
+	for f in \
+		`test -x $HA_BIN/ha_logd &&
+			which strings > /dev/null 2>&1 &&
+			strings $HA_BIN/ha_logd | grep 'logd\.cf'` \
+		`for d; do echo $d/logd.cf $d/ha_logd.cf; done`
+	do
+		if [ -f "$f" ]; then
+			echo $f
+			debug "found logd.cf at $f"
+			return 0
+		fi
+	done
+	debug "no logd.cf"
+	return 1
+}
+#
+# logging
+#
+syslogmsg() {
+	local severity logtag
+	severity=$1
+	shift 1
+	logtag=""
+	[ "$HA_LOGTAG" ] && logtag="-t $HA_LOGTAG"
+	logger -p ${HA_LOGFACILITY:-$DEFAULT_HA_LOGFACILITY}.$severity $logtag $*
+}
+
+#
+# find log destination
+#
+findmsg() {
+	local d syslogdirs favourites mark log
+	# this is tricky, we try a few directories
+	syslogdirs="/var/log /var/logs /var/syslog /var/adm
+	/var/log/ha /var/log/cluster /var/log/pacemaker
+	/var/log/heartbeat /var/log/crm /var/log/corosync /var/log/openais"
+	favourites="ha-*"
+	mark=$1
+	log=""
+	for d in $syslogdirs; do
+		[ -d $d ] || continue
+		log=`grep -l -e "$mark" $d/$favourites` && break
+		test "$log" && break
+		log=`grep -l -e "$mark" $d/*` && break
+		test "$log" && break
+	done 2>/dev/null
+	[ "$log" ] &&
+		ls -t $log | tr '\n' ' '
+	[ "$log" ] &&
+		debug "found HA log at `ls -t $log | tr '\n' ' '`" ||
+		debug "no HA log found in $syslogdirs"
+}
+
+#
+# print a segment of a log file
+#
+str2time() {
+	perl -e "\$time='$*';" -e '
+	$unix_tm = 0;
+	eval "use Date::Parse";
+	if (!$@) {
+		$unix_tm = str2time($time);
+	} else {
+		eval "use Date::Manip";
+		if (!$@) {
+			$unit_tm = UnixDate(ParseDateString($time), "%s");
+		}
+	}
+	if ($unix_tm != "") {
+		$unix_tm = int($unix_tm);
+	}
+	print $unix_tm;
+	'
+}
+getstamp_syslog() {
+	awk '{print $1,$2,$3}'
+}
+getstamp_legacy() {
+	awk '{print $2}' | sed 's/_/ /'
+}
+getstamp_rfc5424() {
+	awk '{print $1}'
+}
+get_ts() {
+	local l="$1" ts
+	ts=$(str2time `echo "$l" | $getstampproc`)
+	if [ -z "$ts" ]; then
+		local fmt
+		for fmt in rfc5424 syslog legacy; do
+			[ "getstamp_$fmt" = "$getstampproc" ] && continue
+			ts=$(str2time `echo "$l" | getstamp_$fmt`)
+			[ -n "$ts" ] && break
+		done
+	fi
+	echo $ts
+}
+linetime() {
+	get_ts "`tail -n +$2 $1 | head -1`"
+}
+find_getstampproc() {
+	local t l func trycnt
+	t=0 l="" func=""
+	trycnt=10
+	while [ $trycnt -gt 0 ] && read l; do
+		t=$(str2time `echo $l | getstamp_syslog`)
+		if [ "$t" ]; then
+			func="getstamp_syslog"
+			debug "the log file is in the syslog format"
+			break
+		fi
+		t=$(str2time `echo $l | getstamp_rfc5424`)
+		if [ "$t" ]; then
+			func="getstamp_rfc5424"
+			debug "the log file is in the rfc5424 format"
+			break
+		fi
+		t=$(str2time `echo $l | getstamp_legacy`)
+		if [ "$t" ]; then
+			func="getstamp_legacy"
+			debug "the log file is in the legacy format (please consider switching to syslog format)"
+			break
+		fi
+		trycnt=$(($trycnt-1))
+	done
+	echo $func
+}
+find_first_ts() {
+	local l ts
+	while read l; do
+		ts=`get_ts "$l"`
+		[ "$ts" ] && break
+		warning "cannot extract time: |$l|; will try the next one"
+	done
+	echo $ts
+}
+findln_by_time() {
+	local logf=$1
+	local tm=$2
+	local first=1
+	local last=`wc -l < $logf`
+	local tmid mid trycnt
+	while [ $first -le $last ]; do
+		mid=$((($last+$first)/2))
+		trycnt=10
+		while [ $trycnt -gt 0 ]; do
+			tmid=`linetime $logf $mid`
+			[ "$tmid" ] && break
+			warning "cannot extract time: $logf:$mid; will try the next one"
+			trycnt=$(($trycnt-1))
+			# shift the whole first-last segment
+			first=$(($first-1))
+			last=$(($last-1))
+			mid=$((($last+$first)/2))
+		done
+		if [ -z "$tmid" ]; then
+			warning "giving up on log..."
+			return
+		fi
+		if [ $tmid -gt $tm ]; then
+			last=$(($mid-1))
+		elif [ $tmid -lt $tm ]; then
+			first=$(($mid+1))
+		else
+			break
+		fi
+	done
+	echo $mid
+}
+
+dumplog() {
+	local logf=$1
+	local from_line=$2
+	local to_line=$3
+	[ "$from_line" ] ||
+		return
+	tail -n +$from_line $logf |
+		if [ "$to_line" ]; then
+			head -$(($to_line-$from_line+1))
+		else
+			cat
+		fi
+}
+
+#
+# find files newer than a and older than b
+#
+isnumber() {
+	echo "$*" | grep -qs '^[0-9][0-9]*$'
+}
+touchfile() {
+	local t
+	t=`mktemp` &&
+	perl -e "\$file=\"$t\"; \$tm=$1;" -e 'utime $tm, $tm, $file;' &&
+	echo $t
+}
+find_files() {
+	local dirs from_time to_time
+	local from_stamp to_stamp findexp
+	dirs=$1
+	from_time=$2
+	to_time=$3
+	isnumber "$from_time" && [ "$from_time" -gt 0 ] || {
+		warning "sorry, can't find files based on time if you don't supply time"
+		return
+	}
+	if ! from_stamp=`touchfile $from_time`; then
+		warning "can't create temporary files"
+		return
+	fi
+	add_tmpfiles $from_stamp
+	findexp="-newer $from_stamp"
+	if isnumber "$to_time" && [ "$to_time" -gt 0 ]; then
+		if ! to_stamp=`touchfile $to_time`; then
+			warning "can't create temporary files"
+			return
+		fi
+		add_tmpfiles $to_stamp
+		findexp="$findexp ! -newer $to_stamp"
+	fi
+	find $dirs -type f $findexp
+}
+
+#
+# check permissions of files/dirs
+#
+pl_checkperms() {
+perl -e '
+# check permissions and ownership
+# uid and gid are numeric
+# everything must match exactly
+# no error checking! (file should exist, etc)
+($filename, $perms, $in_uid, $in_gid) = @ARGV;
+($mode,$uid,$gid) = (stat($filename))[2,4,5];
+$p=sprintf("%04o", $mode & 07777);
+$p ne $perms and exit(1);
+$uid ne $in_uid and exit(1);
+$gid ne $in_gid and exit(1);
+' $*
+}
+num_id() {
+	getent $1 $2 | awk -F: '{print $3}'
+}
+chk_id() {
+	[ "$2" ] && return 0
+	echo "$1: id not found"
+	return 1
+}
+check_perms() {
+	local f p uid gid n_uid n_gid
+	essential_files |
+	while read type f p uid gid; do
+		[ -$type $f ] || {
+			echo "$f wrong type or doesn't exist"
+			continue
+		}
+		n_uid=`num_id passwd $uid`
+		chk_id "$uid" "$n_uid" || continue
+		n_gid=`num_id group $gid`
+		chk_id "$gid" "$n_gid" || continue
+		pl_checkperms $f $p $n_uid $n_gid || {
+			echo "wrong permissions or ownership for $f:"
+			ls -ld $f
+		}
+	done
+}
+
+#
+# coredumps
+#
+pkg_mgr_list() {
+# list of:
+# regex pkg_mgr
+# no spaces allowed in regex
+	cat<<EOF
+zypper.install zypper
+EOF
+}
+listpkg_zypper() {
+	local bins
+	local binary=$1 core=$2
+	gdb $binary $core </dev/null 2>&1 |
+	awk '
+	# this zypper version dumps all packages on a single line
+	/Missing separate debuginfos.*zypper.install/ {
+		sub(".*zypper.install ",""); print
+		exit}
+	n>0 && /^Try: zypper install/ {gsub("\"",""); print $NF}
+	n>0 {n=0}
+	/Missing separate debuginfo/ {n=1}
+	' | sort -u
+}
+fetchpkg_zypper() {
+	local pkg
+	debug "get debuginfo packages using zypper: $@"
+	zypper -qn ref > /dev/null
+	for pkg in $@; do
+		zypper -qn install -C $pkg >/dev/null
+	done
+}
+find_pkgmgr() {
+	local binary=$1 core=$2
+	local regex pkg_mgr
+	pkg_mgr_list |
+	while read regex pkg_mgr; do
+		if gdb $binary $core </dev/null 2>&1 |
+				grep "$regex" > /dev/null; then
+			echo $pkg_mgr
+			break
+		fi
+	done
+}
+get_debuginfo() {
+	local binary=$1 core=$2
+	local pkg_mgr pkgs
+	gdb $binary $core </dev/null 2>/dev/null |
+		egrep 'Missing.*debuginfo|no debugging symbols found' > /dev/null ||
+		return  # no missing debuginfo
+	pkg_mgr=`find_pkgmgr $binary $core`
+	if [ -z "$pkg_mgr" ]; then
+		warning "found core for $binary but there is no debuginfo and we don't know how to get it on this platform"
+		return
+	fi
+	pkgs=`listpkg_$pkg_mgr $binary $core`
+	[ -n "$pkgs" ] &&
+		fetchpkg_$pkg_mgr $pkgs
+}
+findbinary() {
+	local random_binary binary fullpath
+	random_binary=`which cat 2>/dev/null` # suppose we are lucky
+	binary=`gdb $random_binary $1 < /dev/null 2>/dev/null |
+		grep 'Core was generated' | awk '{print $5}' |
+		sed "s/^.//;s/[.':]*$//"`
+	if [ x = x"$binary" ]; then
+		debug "could not detect the program name for core $1 from the gdb output; will try with file(1)"
+		binary=$(file $1 | awk '/from/{
+			for( i=1; i<=NF; i++ )
+				if( $i == "from" ) {
+					print $(i+1)
+					break
+				}
+			}')
+		binary=`echo $binary | tr -d "'"`
+		binary=$(echo $binary | tr -d '`')
+		if [ "$binary" ]; then
+			binary=`which $binary 2>/dev/null`
+		fi
+	fi
+	if [ x = x"$binary" ]; then
+		warning "could not find the program path for core $1"
+		return
+	fi
+	fullpath=`which $binary 2>/dev/null`
+	if [ x = x"$fullpath" ]; then
+		for d in $HA_BIN $CRM_DAEMON_DIR; do
+			if [ -x $d/$binary ]; then
+				echo $d/$binary
+				debug "found the program at $d/$binary for core $1"
+			else
+				warning "could not find the program path for core $1"
+			fi
+		done
+	else
+		echo $fullpath
+		debug "found the program at $fullpath for core $1"
+	fi
+}
+getbt() {
+	local corefile absbinpath
+	which gdb > /dev/null 2>&1 || {
+		warning "please install gdb to get backtraces"
+		return
+	}
+	for corefile; do
+		absbinpath=`findbinary $corefile`
+		[ x = x"$absbinpath" ] && continue
+		get_debuginfo $absbinpath $corefile
+		echo "====================== start backtrace ======================"
+		ls -l $corefile
+		gdb -batch -n -quiet -ex ${BT_OPTS:-"thread apply all bt full"} -ex quit \
+			$absbinpath $corefile 2>/dev/null
+		echo "======================= end backtrace ======================="
+	done
+}
+
+#
+# heartbeat configuration/status
+#
+iscrmrunning() {
+	local pid maxwait
+	ps -ef | grep -qs [c]rmd || return 1
+	crmadmin -D >/dev/null 2>&1 &
+	pid=$!
+	maxwait=100
+	while kill -0 $pid 2>/dev/null && [ $maxwait -gt 0 ]; do
+		sleep 0.1
+		maxwait=$(($maxwait-1))
+	done
+	if kill -0 $pid 2>/dev/null; then
+		kill $pid
+		false
+	else
+		wait $pid
+	fi
+}
+dumpstate() {
+	crm_mon -1 | grep -v '^Last upd' > $1/$CRM_MON_F
+	cibadmin -Ql > $1/$CIB_F
+	`echo_membership_tool` $MEMBERSHIP_TOOL_OPTS -p > $1/$MEMBERSHIP_F 2>&1
+}
+getconfig() {
+	[ -f "$CONF" ] &&
+		cp -p $CONF $1/
+	[ -f "$LOGD_CF" ] &&
+		cp -p $LOGD_CF $1/
+	if iscrmrunning; then
+		dumpstate $1
+		touch $1/RUNNING
+	else
+		cp -p $CIB_DIR/$CIB_F $1/ 2>/dev/null
+		touch $1/STOPPED
+	fi
+	[ "$HOSTCACHE" ] &&
+		cp -p $HA_VARLIB/hostcache $1/$HOSTCACHE 2>/dev/null
+	[ "$HB_UUID_F" ] &&
+		crm_uuid -r > $1/$HB_UUID_F 2>&1
+	[ -f "$1/$CIB_F" ] &&
+		crm_verify -V -x $1/$CIB_F >$1/$CRM_VERIFY_F 2>&1
+}
+crmconfig() {
+	[ -f "$1/$CIB_F" ] && which crm >/dev/null 2>&1 &&
+		CIB_file=$1/$CIB_F crm configure show >$1/$CIB_TXT_F 2>&1
+}
+get_crm_nodes() {
+	cibadmin -Ql -o nodes |
+	awk '
+	/<node / {
+		for( i=1; i<=NF; i++ )
+			if( $i~/^uname=/ ) {
+				sub("uname=.","",$i);
+				sub("\".*","",$i);
+				print $i;
+				next;
+			}
+	}
+	'
+}
+get_live_nodes() {
+	if [ `id -u` = 0 ] && which fping >/dev/null 2>&1; then
+		fping -a $@ 2>/dev/null
+	else
+		local h
+		for h; do ping -c 2 -q $h >/dev/null 2>&1 && echo $h; done
+	fi
+}
+
+#
+# remove values of sensitive attributes
+#
+# this is not proper xml parsing, but it will work under the
+# circumstances
+is_sensitive_xml() {
+	local patt epatt
+	epatt=""
+	for patt in $SANITIZE; do
+		epatt="$epatt|$patt"
+	done
+	epatt="`echo $epatt|sed 's/.//'`"
+	egrep -qs "name=\"$epatt\""
+}
+test_sensitive_one() {
+	local file compress decompress
+	file=$1
+	compress=""
+	echo $file | grep -qs 'gz$' && compress=gzip
+	echo $file | grep -qs 'bz2$' && compress=bzip2
+	if [ "$compress" ]; then
+		decompress="$compress -dc"
+	else
+		compress=cat
+		decompress=cat
+	fi
+	$decompress < $file | is_sensitive_xml
+}
+sanitize_xml_attrs() {
+	local patt
+	sed $(
+	for patt in $SANITIZE; do
+		echo "-e /name=\"$patt\"/s/value=\"[^\"]*\"/value=\"****\"/"
+	done
+	)
+}
+sanitize_hacf() {
+	awk '
+	$1=="stonith_host"{ for( i=5; i<=NF; i++ ) $i="****"; }
+	{print}
+	'
+}
+sanitize_one() {
+	local file compress decompress tmp ref
+	file=$1
+	compress=""
+	echo $file | grep -qs 'gz$' && compress=gzip
+	echo $file | grep -qs 'bz2$' && compress=bzip2
+	if [ "$compress" ]; then
+		decompress="$compress -dc"
+	else
+		compress=cat
+		decompress=cat
+	fi
+	tmp=`mktemp`
+	ref=`mktemp`
+	add_tmpfiles $tmp $ref
+	if [ -z "$tmp" -o -z "$ref" ]; then
+		fatal "cannot create temporary files"
+	fi
+	touch -r $file $ref  # save the mtime
+	if [ "`basename $file`" = ha.cf ]; then
+		sanitize_hacf
+	else
+		$decompress | sanitize_xml_attrs | $compress
+	fi < $file > $tmp
+	mv $tmp $file
+	touch -r $ref $file
+}
+
+#
+# keep the user posted
+#
+fatal() {
+	echo "`uname -n`: ERROR: $*" >&2
+	exit 1
+}
+warning() {
+	echo "`uname -n`: WARN: $*" >&2
+}
+info() {
+	echo "`uname -n`: INFO: $*" >&2
+}
+debug() {
+	[ "$VERBOSITY" ] && [ $VERBOSITY -gt 0 ] &&
+	echo "`uname -n`: DEBUG: $*" >&2
+	return 0
+}
+pickfirst() {
+	for x; do
+		which $x >/dev/null 2>&1 && {
+			echo $x
+			return 0
+		}
+	done
+	return 1
+}
+
+# tmp files business
+drop_tmpfiles() {
+	trap 'rm -rf `cat $__TMPFLIST`; rm $__TMPFLIST' EXIT
+}
+init_tmpfiles() {
+	if __TMPFLIST=`mktemp`; then
+		drop_tmpfiles
+	else
+		# this is really bad, let's just leave
+		fatal "eek, mktemp cannot create temporary files"
+	fi
+}
+add_tmpfiles() {
+	test -f "$__TMPFLIST" || return
+	echo $* >> $__TMPFLIST
+}
+
+#
+# get some system info
+#
+distro() {
+	local relf f
+	which lsb_release >/dev/null 2>&1 && {
+		lsb_release -d
+		debug "using lsb_release for distribution info"
+		return
+	}
+	relf=`ls /etc/debian_version 2>/dev/null` ||
+	relf=`ls /etc/slackware-version 2>/dev/null` ||
+	relf=`ls -d /etc/*-release 2>/dev/null` && {
+		for f in $relf; do
+			test -f $f && {
+				echo "`ls $f` `cat $f`"
+				debug "found $relf distribution release file"
+				return
+			}
+		done
+	}
+	warning "no lsb_release, no /etc/*-release, no /etc/debian_version: no distro information"
+}
+
+pkg_ver_deb() {
+	dpkg-query -f '${Name} ${Version}' -W $* 2>/dev/null
+}
+pkg_ver_rpm() {
+	rpm -q --qf '%{name} %{version}-%{release} - %{distribution} %{arch}\n' $* 2>&1 |
+		grep -v 'not installed'
+}
+pkg_ver_pkg_info() {
+	for pkg; do
+		pkg_info | grep $pkg
+	done
+}
+pkg_ver_pkginfo() {
+	for pkg; do
+		pkginfo $pkg | awk '{print $3}'  # format?
+	done
+}
+verify_deb() {
+	debsums -s $* 2>/dev/null
+}
+verify_rpm() {
+	rpm --verify $* 2>&1 | grep -v 'not installed'
+}
+verify_pkg_info() {
+	:
+}
+verify_pkginfo() {
+	:
+}
+
+get_pkg_mgr() {
+	local pkg_mgr
+	if which dpkg >/dev/null 2>&1 ; then
+		pkg_mgr="deb"
+	elif which rpm >/dev/null 2>&1 ; then
+		pkg_mgr="rpm"
+	elif which pkg_info >/dev/null 2>&1 ; then 
+		pkg_mgr="pkg_info"
+	elif which pkginfo >/dev/null 2>&1 ; then 
+		pkg_mgr="pkginfo"
+	else
+		warning "Unknown package manager!"
+		return
+	fi
+	echo $pkg_mgr
+}
+
+pkg_versions() {
+	local pkg_mgr=`get_pkg_mgr`
+	[ -z "$pkg_mgr" ] &&
+		return
+	debug "the package manager is $pkg_mgr"
+	pkg_ver_$pkg_mgr $*
+}
+verify_packages() {
+	local pkg_mgr=`get_pkg_mgr`
+	[ -z "$pkg_mgr" ] &&
+		return
+	verify_$pkg_mgr $*
+}
+
+crm_info() {
+	$CRM_DAEMON_DIR/crmd version 2>&1
+}
diff --git a/modules/Makefile.am b/modules/Makefile.am
new file mode 100644
index 0000000..f190be1
--- /dev/null
+++ b/modules/Makefile.am
@@ -0,0 +1,81 @@
+#
+# doc: Pacemaker code
+#
+# Copyright (C) 2008 Andrew Beekhof
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+modules =	__init__.py \
+			cache.py \
+			cibconfig.py \
+			cibstatus.py \
+			cibverify.py \
+			clidisplay.py \
+			cliformat.py \
+			cmd_status.py \
+			command.py \
+			completers.py \
+			config.py \
+			corosync.py \
+			crm_gv.py \
+			crm_pssh.py \
+			help.py \
+			idmgmt.py \
+			log_patterns_118.py \
+			log_patterns.py \
+			main.py \
+			msg.py \
+			options.py \
+			ordereddict.py \
+			orderedset.py \
+			pacemaker.py \
+			parse.py \
+			ra.py \
+			report.py \
+			rsctest.py \
+			schema.py \
+			scripts.py \
+			template.py \
+			term.py \
+			tmpfiles.py \
+			ui_assist.py \
+			ui_cib.py \
+			ui_cibstatus.py \
+			ui_cluster.py \
+			ui_configure.py \
+			ui_context.py \
+			ui_corosync.py \
+			ui_history.py \
+			ui_node.py \
+			ui_options.py \
+			ui_ra.py \
+			ui_report.py \
+			ui_resource.py \
+			ui_root.py \
+			ui_script.py \
+			ui_site.py \
+			ui_template.py \
+			ui_utils.py \
+			userdir.py \
+			utils.py \
+			constants.py \
+			xmlbuilder.py \
+			xmlutil.py
+
+shelllibdir	= $(pyexecdir)/crmsh
+
+shelllib_PYTHON = $(modules)
diff --git a/modules/__init__.py b/modules/__init__.py
new file mode 100644
index 0000000..feff2bb
--- /dev/null
+++ b/modules/__init__.py
@@ -0,0 +1,2 @@
+# This file is required for python packages.
+# It is intentionally empty.
diff --git a/modules/cache.py b/modules/cache.py
new file mode 100644
index 0000000..53f4a9b
--- /dev/null
+++ b/modules/cache.py
@@ -0,0 +1,52 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import time
+
+"Cache stuff. A naive implementation."
+
+
+_max_cache_age = 600  # seconds
+_stamp = time.time()
+_lists = {}
+
+
+def _clear():
+    global _stamp
+    global _lists
+    _stamp = time.time()
+    _lists = {}
+
+
+def is_cached(name):
+    if time.time() - _stamp > _max_cache_age:
+        _clear()
+    return name in _lists
+
+
+def store(name, lst):
+    _lists[name] = lst
+    return lst
+
+
+def retrieve(name):
+    if is_cached(name):
+        return _lists[name]
+    return None
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/cibconfig.py b/modules/cibconfig.py
new file mode 100644
index 0000000..ceeb68d
--- /dev/null
+++ b/modules/cibconfig.py
@@ -0,0 +1,3594 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import copy
+from lxml import etree
+import os
+import sys
+import re
+import fnmatch
+import time
+import config
+import options
+import constants
+import tmpfiles
+from parse import CliParser
+import clidisplay
+from cibstatus import cib_status
+import idmgmt
+from ra import get_ra, get_properties_list, get_pe_meta
+import schema
+from crm_gv import gv_types
+from msg import common_warn, common_err, common_debug, common_info, err_buf
+from msg import common_error, constraint_norefobj_err, cib_parse_err, no_object_err
+from msg import missing_obj_err, common_warning, update_err, unsupported_err, empty_cib_err
+from msg import invalid_id_err, cib_ver_unsupported_err
+import utils
+from utils import ext_cmd, safe_open_w, pipe_string, safe_close_w, crm_msec
+from utils import ask, lines2cli, olist
+from utils import page_string, cibadmin_can_patch, str2tmp
+from utils import run_ptest, is_id_valid, edit_file, get_boolean, filter_string
+from ordereddict import odict
+from orderedset import oset
+from xmlutil import is_child_rsc, rsc_constraint, sanitize_cib, rename_id, get_interesting_nodes
+from xmlutil import is_pref_location, get_topnode, new_cib, get_rscop_defaults_meta_node
+from xmlutil import rename_rscref, is_ms, silly_constraint, is_container, fix_comments
+from xmlutil import sanity_check_nvpairs, merge_nodes, op2list, mk_rsc_type, is_resource
+from xmlutil import stuff_comments, is_comment, is_constraint, read_cib, processing_sort_cli
+from xmlutil import find_operation, get_rsc_children_ids, is_primitive, referenced_resources
+from xmlutil import cibdump2elem, processing_sort, get_rsc_ref_ids, merge_tmpl_into_prim
+from xmlutil import remove_id_used_attributes, get_top_cib_nodes
+from xmlutil import merge_attributes, is_cib_element, sanity_check_meta
+from xmlutil import is_simpleconstraint, is_template, rmnode, is_defaults, is_live_cib
+from xmlutil import get_rsc_operations, delete_rscref, xml_equals, lookup_node, RscState
+from xmlutil import cibtext2elem
+from cliformat import get_score, nvpairs2list, abs_pos_score, cli_acl_roleref, nvpair_format
+from cliformat import cli_nvpair, cli_acl_rule, rsc_set_constraint, get_kind, head_id_format
+from cliformat import cli_operations, simple_rsc_constraint, cli_rule, cli_format
+from cliformat import cli_acl_role, cli_acl_permission
+import cibverify
+
+
+def show_unrecognized_elems(cib_elem):
+    try:
+        conf = cib_elem.findall("configuration")[0]
+    except IndexError:
+        common_warn("CIB has no configuration element")
+        return False
+    rc = True
+    for topnode in conf.iterchildren():
+        if is_defaults(topnode) or topnode.tag == "fencing-topology":
+            continue
+        for c in topnode.iterchildren():
+            if not c.tag in cib_object_map:
+                common_warn("unrecognized CIB element %s" % c.tag)
+                rc = False
+    return rc
+
+
+#
+# object sets (enables operations on sets of elements)
+#
+def mkset_obj(*args):
+    if not cib_factory.is_cib_sane():
+        raise ValueError("CIB is not valid")
+    if args and args[0] == "xml":
+        return CibObjectSetRaw(*args[1:])
+    return CibObjectSetCli(*args)
+
+
+def set_graph_attrs(gv_obj, obj_type):
+    try:
+        for attr, attr_v in constants.graph['*'].iteritems():
+            gv_obj.new_graph_attr(attr, attr_v)
+    except KeyError:
+        pass
+    try:
+        for attr, attr_v in constants.graph[obj_type].iteritems():
+            gv_obj.new_graph_attr(attr, attr_v)
+    except KeyError:
+        pass
+
+
+def set_obj_attrs(gv_obj, obj_id, obj_type):
+    try:
+        for attr, attr_v in constants.graph['*'].iteritems():
+            gv_obj.new_attr(obj_id, attr, attr_v)
+    except KeyError:
+        pass
+    try:
+        for attr, attr_v in constants.graph[obj_type].iteritems():
+            gv_obj.new_attr(obj_id, attr, attr_v)
+    except KeyError:
+        pass
+
+
+def set_edge_attrs(gv_obj, edge_id, obj_type):
+    try:
+        for attr, attr_v in constants.graph[obj_type].iteritems():
+            gv_obj.new_edge_attr(edge_id, attr, attr_v)
+    except KeyError:
+        pass
+
+
+def fill_nvpairs(name, node, attrs, id_hint):
+    '''
+    Fill the container node with attrs:
+    name: name of container
+    node: container Element
+    attrs: dict containing values
+    id_hint: used to generate unique ids for nvpairs
+    '''
+    subpfx = constants.subpfx_list.get(name, '')
+    subpfx = "%s_%s" % (id_hint, subpfx) if subpfx else id_hint
+    nvpair_pfx = node.get("id") or subpfx
+    for n, v in attrs.iteritems():
+        nvpair = etree.SubElement(node, "nvpair", name=n)
+        if v is not None:
+            nvpair.set("value", v)
+        idmgmt.set(nvpair, None, nvpair_pfx)
+    return node
+
+
+def mkxmlnvpairs(name, attrs, id_hint):
+    '''
+    name: Name of the element.
+    attrs: dict containing a set of nvpairs.
+    hint: Used to generate ids.
+
+    Example: instance_attributes, {name: value...}, <hint>
+
+    Notes:
+
+      Other tags not containing nvpairs are fine if the dict is empty.
+
+      cluster_property_set and defaults have nvpairs as direct children.
+      In that case, use the id_hint directly as id.
+      This is important in case there are multiple sets.
+
+    '''
+    xml_node_type = name in constants.defaults_tags and "meta_attributes" or name
+    node = etree.Element(xml_node_type)
+    notops = name != "operations"
+
+    if (name == "cluster_property_set" or name in constants.defaults_tags) and id_hint:
+        node.set("id", id_hint)
+    id_ref = attrs.get("$id-ref")
+    if id_ref:
+        id_ref_2 = cib_factory.resolve_id_ref(name, id_ref)
+        node.set("id-ref", id_ref_2)
+        if notops:
+            return node  # id_ref is the only attribute (if not operations)
+        if '$id-ref' in attrs:
+            del attrs['$id-ref']
+    v = attrs.get('$id')
+    if v:
+        node.set("id", v)
+        del attrs['$id']
+    elif name in constants.nvset_cli_names:
+        node.set("id", id_hint)
+    else:
+        # operations don't need no id
+        idmgmt.set(node, None, id_hint, id_required=notops)
+    return fill_nvpairs(name, node, attrs, id_hint)
+
+
+def copy_nvpair(nvpairs, nvp, id_hint=None):
+    """
+    Copies the given nvpair into the given tag containing nvpairs
+    """
+    common_debug("copy_nvpair: %s" % (etree.tostring(nvp)))
+    if 'value' not in nvp.attrib:
+        nvpairs.append(copy.deepcopy(nvp))
+        return
+    n = nvp.get('name')
+    if id_hint is None:
+        id_hint = n
+    for nvp2 in nvpairs:
+        if nvp2.get('name') == n:
+            nvp2.set('value', nvp.get('value'))
+            break
+    else:
+        m = copy.deepcopy(nvp)
+        nvpairs.append(m)
+        if 'id' not in m.attrib:
+            m.set('id', idmgmt.new(m, id_hint))
+
+
+def copy_nvpairs(tonode, fromnode):
+    """
+    copy nvpairs from fromnode to tonode.
+    things to copy can be nvpairs, comments or rules.
+    """
+    def copy_comment(cnode):
+        for nvp2 in tonode:
+            if is_comment(nvp2) and nvp2.text == cnode.text:
+                break  # no need to copy
+        else:
+            tonode.append(copy.deepcopy(cnode))
+
+    def copy_id(node):
+        nid = node.get('id')
+        for nvp2 in tonode:
+            if nvp2.get('id') == nid:
+                tonode.replace(nvp2, copy.deepcopy(node))
+                break
+        else:
+            tonode.append(copy.deepcopy(node))
+
+    common_debug("copy_nvpairs: %s -> %s" % (etree.tostring(fromnode), etree.tostring(tonode)))
+    id_hint = tonode.get('id')
+    for c in fromnode:
+        if is_comment(c):
+            copy_comment(c)
+        elif c.tag == "nvpair":
+            copy_nvpair(tonode, c, id_hint=id_hint)
+        elif 'id' in c.attrib:  # ok, it has an id, we can work with this
+            copy_id(c)
+        else:  # no idea what this is, just copy it
+            tonode.append(copy.deepcopy(c))
+
+
+class CibObjectSet(object):
+    '''
+    Edit or display a set of cib objects.
+    repr() for objects representation and
+    save() used to store objects into internal structures
+    are defined in subclasses.
+    '''
+    def __init__(self, *args):
+        self.args = args
+        self._initialize()
+
+    def _initialize(self):
+        rc, self.obj_set = cib_factory.mkobj_set(*self.args)
+        self.search_rc = rc
+        self.all_set = cib_factory.get_all_obj_set()
+        self.obj_ids = oset([o.obj_id for o in self.obj_set])
+        self.all_ids = oset([o.obj_id for o in self.all_set])
+        self.locked_ids = self.all_ids - self.obj_ids
+
+    def _open_url(self, src):
+        if src == "-":
+            return sys.stdin
+        import urllib2
+        try:
+            ret = urllib2.urlopen(src)
+            return ret
+        except (urllib2.URLError, ValueError):
+            pass
+        try:
+            ret = open(src)
+            return ret
+        except IOError, e:
+            common_err("could not open %s: %s" % (src, e))
+        return False
+
+    def _pre_edit(self, s):
+        '''Extra processing of the string to be editted'''
+        return s
+
+    def _post_edit(self, s):
+        '''Extra processing after editing'''
+        return s
+
+    def _edit_save(self, s):
+        '''
+        Save string s to a tmp file. Invoke editor to edit it.
+        Parse/save the resulting file. In case of syntax error,
+        allow user to reedit.
+        If no changes are done, return silently.
+        '''
+        s = self._pre_edit(s)
+        tmp = str2tmp(s)
+        if not tmp:
+            return False
+        filehash = hash(s)
+        rc = False
+        while True:
+            if edit_file(tmp) != 0:
+                break
+            try:
+                f = open(tmp, 'r')
+            except IOError, msg:
+                common_err(msg)
+                break
+            s = ''.join(f)
+            f.close()
+            if hash(s) == filehash:  # file unchanged
+                rc = True
+                break
+            if not self.save(self._post_edit(s)):
+                if ask("Edit or discard changes (yes to edit, no to discard)?"):
+                    continue
+            rc = True
+            break
+        try:
+            os.unlink(tmp)
+        except OSError:
+            pass
+        return rc
+
+    def edit(self):
+        if options.batch:
+            common_info("edit not allowed in batch mode")
+            return False
+        clidisplay.disable_pretty()
+        s = self.repr()
+        clidisplay.enable_pretty()
+        # don't allow edit if one or more elements were not
+        # found
+        if not self.search_rc:
+            return self.search_rc
+        return self._edit_save(s)
+
+    def _filter_save(self, filter, s):
+        '''
+        Pipe string s through a filter. Parse/save the output.
+        If no changes are done, return silently.
+        '''
+        rc, outp = filter_string(filter, s)
+        if rc != 0:
+            return False
+        if hash(outp) == hash(s):
+            return True
+        return self.save(outp)
+
+    def filter(self, filter):
+        clidisplay.disable_pretty()
+        s = self.repr(format=-1)
+        clidisplay.enable_pretty()
+        # don't allow filter if one or more elements were not
+        # found
+        if not self.search_rc:
+            return self.search_rc
+        return self._filter_save(filter, s)
+
+    def save_to_file(self, fname):
+        f = safe_open_w(fname)
+        if not f:
+            return False
+        rc = True
+        clidisplay.disable_pretty()
+        s = self.repr()
+        clidisplay.enable_pretty()
+        if s:
+            f.write(s)
+            f.write('\n')
+        elif self.obj_set:
+            rc = False
+        safe_close_w(f)
+        return rc
+
+    def _get_gv_obj(self, gtype):
+        if not self.obj_set:
+            return True, None
+        if gtype not in gv_types:
+            common_err("graphviz type %s is not supported" % gtype)
+            return False, None
+        gv_obj = gv_types[gtype]()
+        set_graph_attrs(gv_obj, ".")
+        return True, gv_obj
+
+    def _graph_repr(self, gv_obj):
+        '''Let CIB elements produce graph elements.
+        '''
+        for obj in processing_sort_cli(list(self.obj_set)):
+            obj.repr_gv(gv_obj, from_grp=False)
+
+    def show_graph(self, gtype):
+        '''Display graph using dotty'''
+        rc, gv_obj = self._get_gv_obj(gtype)
+        if not rc or not gv_obj:
+            return rc
+        self._graph_repr(gv_obj)
+        return gv_obj.display()
+
+    def graph_img(self, gtype, outf, img_type):
+        '''Render graph to image and save it to a file (done by
+        dot(1))'''
+        rc, gv_obj = self._get_gv_obj(gtype)
+        if not rc or not gv_obj:
+            return rc
+        self._graph_repr(gv_obj)
+        return gv_obj.image(img_type, outf)
+
+    def save_graph(self, gtype, outf):
+        '''Save graph to a file'''
+        rc, gv_obj = self._get_gv_obj(gtype)
+        if not rc or not gv_obj:
+            return rc
+        self._graph_repr(gv_obj)
+        return gv_obj.save(outf)
+
+    def show(self):
+        s = self.repr()
+        if not s:
+            return self.search_rc
+        page_string(s)
+        return self.search_rc
+
+    def import_file(self, method, fname):
+        '''
+        method: update or replace
+        '''
+        if not cib_factory.is_cib_sane():
+            return False
+        f = self._open_url(fname)
+        if not f:
+            return False
+        s = ''.join(f)
+        if f != sys.stdin:
+            f.close()
+        return self.save(s, no_remove=True, method=method)
+
+    def repr(self, format=format):
+        '''
+        Return a string with objects's representations (either
+        CLI or XML).
+        '''
+        return ''
+
+    def save(self, s, no_remove=False, method='replace'):
+        '''
+        For each object:
+            - try to find a corresponding object in obj_set
+            - if (update and not found) or found:
+              replace the object in the obj_set with
+              the new object
+            - if not found: create new
+        See below for specific implementations.
+        '''
+        pass
+
+    def __check_unique_clash(self, set_obj_all):
+        'Check whether resource parameters with attribute "unique" clash'
+        def process_primitive(prim, clash_dict):
+            '''
+            Update dict clash_dict with
+            (ra_class, ra_provider, ra_type, name, value) -> [ resourcename ]
+            if parameter "name" should be unique
+            '''
+            ra_id = prim.get("id")
+            r_node = reduce_primitive(prim)
+            if r_node is None:
+                return  # template not defined yet
+            ra_type = node.get("type")
+            ra_class = node.get("class")
+            ra_provider = node.get("provider")
+            ra = get_ra(r_node)
+            if ra.mk_ra_node() is None:  # no RA found?
+                return
+            ra_params = ra.params()
+            for a in r_node.iterchildren("instance_attributes"):
+                for p in a.iterchildren("nvpair"):
+                    name = p.get("name")
+                    value = p.get("value")
+                    # don't fail if the meta-data doesn't contain the
+                    # expected attributes
+                    if value is not None:
+                        try:
+                            if ra_params[name].get("unique") == "1":
+                                k = (ra_class, ra_provider, ra_type, name, value)
+                                try:
+                                    clash_dict[k].append(ra_id)
+                                except KeyError:
+                                    clash_dict[k] = [ra_id]
+                        except KeyError:
+                            pass
+            return
+        # we check the whole CIB for clashes as a clash may originate between
+        # an object already committed and a new one
+        check_set = set([o.obj_id
+                         for o in self.obj_set
+                         if o.obj_type == "primitive"])
+        if not check_set:
+            return 0
+        clash_dict = {}
+        for obj in set_obj_all.obj_set:
+            node = obj.node
+            if is_primitive(node):
+                process_primitive(node, clash_dict)
+        # but we only warn if a 'new' object is involved
+        rc = 0
+        for param, resources in clash_dict.items():
+            # at least one new object must be involved
+            if len(resources) > 1 and len(set(resources) & check_set) > 0:
+                rc = 2
+                msg = 'Resources %s violate uniqueness for parameter "%s": "%s"' % (
+                    ",".join(sorted(resources)), param[3], param[4])
+                common_warning(msg)
+        return rc
+
+    def semantic_check(self, set_obj_all):
+        '''
+        Test objects for sanity. This is about semantics.
+        '''
+        rc = self.__check_unique_clash(set_obj_all)
+        for obj in self.obj_set:
+            rc |= obj.check_sanity()
+        return rc
+
+    def is_edit_valid(self, id_set):
+        '''
+        1. Cannot name any elements as those which exist but
+        were not picked for editing.
+        2. Cannot remove running resources.
+        '''
+        rc = True
+        not_allowed = id_set & self.locked_ids
+        rscstat = RscState()
+        if not_allowed:
+            common_err("Elements %s already exist" %
+                       ', '.join(list(not_allowed)))
+            rc = False
+        delete_set = self.obj_ids - id_set
+        cannot_delete = [x for x in delete_set
+                         if not rscstat.can_delete(x)]
+        if cannot_delete:
+            common_err("Cannot delete running resources: %s" %
+                       ', '.join(cannot_delete))
+            rc = False
+        return rc
+
+
+class CibObjectSetCli(CibObjectSet):
+    '''
+    Edit or display a set of cib objects (using cli notation).
+    '''
+    vim_stx_str = "# vim: set filetype=pcmk:\n"
+
+    def __init__(self, *args):
+        CibObjectSet.__init__(self, *args)
+
+    def repr_nopretty(self, format=1):
+        clidisplay.disable_pretty()
+        s = self.repr(format=format)
+        clidisplay.enable_pretty()
+        return s
+
+    def repr(self, format=1):
+        "Return a string containing cli format of all objects."
+        if not self.obj_set:
+            return ''
+        return '\n'.join(obj.repr_cli(format=format)
+                         for obj in processing_sort_cli(list(self.obj_set)))
+
+    def _pre_edit(self, s):
+        '''Extra processing of the string to be edited'''
+        if config.core.editor.startswith("vi"):
+            return "%s\n%s" % (s, self.vim_stx_str)
+        return s
+
+    def _post_edit(self, s):
+        if config.core.editor.startswith("vi"):
+            return s.replace(self.vim_stx_str, "")
+        return s
+
+    def _get_id(self, node):
+        '''
+        Get the id from a CLI representation. Normally, it should
+        be value of the id attribute, but sometimes the
+        attribute is missing.
+        '''
+        if node.tag == 'fencing-topology':
+            return 'fencing_topology'
+        if node.tag in constants.defaults_tags:
+            return node[0].get('id')
+        return node.get('id')
+
+    def save(self, s, no_remove=False, method='replace'):
+        '''
+        Save a user supplied cli format configuration.
+        On errors user is typically asked to review the
+        configuration (for instance on editting).
+
+        On errors, the user is asked to edit again (if we're
+        coming from edit). The original CIB is preserved and no
+        changes are made.
+        '''
+        edit_d = {}
+        id_set = oset()
+        del_set = oset()
+        rc = True
+        err_buf.start_tmp_lineno()
+        cp = CliParser()
+        for cli_text in lines2cli(s):
+            err_buf.incr_lineno()
+            node = cp.parse(cli_text)
+            if node not in (False, None):
+                obj_id = id_for_node(node)
+                if obj_id is None:
+                    common_err("element %s has no id!" %
+                               etree.tostring(node, pretty_print=True))
+                    rc = False
+                elif obj_id in id_set:
+                    common_err("duplicate element %s" % obj_id)
+                    rc = False
+                else:
+                    id_set.add(obj_id)
+                    edit_d[obj_id] = node
+            elif node is False:
+                rc = False
+        err_buf.stop_tmp_lineno()
+        # we can't proceed if there was a syntax error, but we
+        # can ask the user to fix problems
+        if not no_remove:
+            rc &= self.is_edit_valid(id_set)
+            del_set = self.obj_ids - id_set
+        if not rc:
+            return rc
+        mk_set = id_set - self.obj_ids
+        upd_set = id_set & self.obj_ids
+
+        rc = cib_factory.set_update(edit_d, mk_set, upd_set, del_set,
+                                    upd_type="cli", method=method)
+        if not rc:
+            self._initialize()
+        return rc
+
+
+class CibObjectSetRaw(CibObjectSet):
+    '''
+    Edit or display one or more CIB objects (XML).
+    '''
+    def __init__(self, *args):
+        CibObjectSet.__init__(self, *args)
+
+    def repr(self, format="ignored"):
+        "Return a string containing xml of all objects."
+        cib_elem = cib_factory.obj_set2cib(self.obj_set)
+        s = etree.tostring(cib_elem, pretty_print=True)
+        return '<?xml version="1.0" ?>\n' + s
+
+    def _get_id(self, node):
+        if node.tag == "fencing-topology":
+            return "fencing_topology"
+        return node.get("id")
+
+    def save(self, s, no_remove=False, method='replace'):
+        try:
+            cib_elem = etree.fromstring(s)
+        except etree.ParseError, msg:
+            cib_parse_err(msg, s)
+            return False
+        sanitize_cib(cib_elem)
+        if not show_unrecognized_elems(cib_elem):
+            return False
+        rc = True
+        id_set = oset()
+        del_set = oset()
+        edit_d = {}
+        for node in get_top_cib_nodes(cib_elem, []):
+            id = self._get_id(node)
+            if id is None:
+                common_err("element %s has no id!" %
+                           etree.tostring(node, pretty_print=True))
+                rc = False
+            elif id in id_set:
+                common_err("duplicate element %s" % id)
+                rc = False
+            else:
+                id_set.add(id)
+                edit_d[id] = node
+        if not no_remove:
+            rc &= self.is_edit_valid(id_set)
+            del_set = self.obj_ids - id_set
+        if not rc:
+            return rc
+        mk_set = id_set - self.obj_ids
+        upd_set = id_set & self.obj_ids
+        rc = cib_factory.set_update(edit_d, mk_set, upd_set, del_set, "xml", method)
+        if not rc:
+            self._initialize()
+        return rc
+
+    def verify(self):
+        if not self.obj_set:
+            return True
+        clidisplay.disable_pretty()
+        cib = self.repr(format=-1)
+        clidisplay.enable_pretty()
+        rc = cibverify.verify(cib)
+
+        if rc not in (0, 1):
+            common_debug("verify (rc=%s): %s" % (rc, cib))
+        return rc in (0, 1)
+
+    def ptest(self, nograph, scores, utilization, actions, verbosity):
+        if not cib_factory.is_cib_sane():
+            return False
+        cib_elem = cib_factory.obj_set2cib(self.obj_set)
+        status = cib_status.get_status()
+        if status is None:
+            common_err("no status section found")
+            return False
+        cib_elem.append(copy.deepcopy(status))
+        graph_s = etree.tostring(cib_elem)
+        return run_ptest(graph_s, nograph, scores, utilization, actions, verbosity)
+
+
+def find_comment_nodes(node):
+    return [c for c in node.iterchildren() if is_comment(c)]
+
+
+def fix_node_ids(node, oldnode):
+    """
+    Fills in missing ids, getting ids from oldnode
+    as much as possible. Tries to generate reasonable
+    ids as well.
+    """
+    hint_map = {
+        'node': 'node',
+        'primitive': 'rsc',
+        'template': 'rsc',
+        'master': 'grp',
+        'group': 'grp',
+        'clone': 'grp',
+        'rsc_location': 'location',
+        'fencing-topology': 'fencing',
+        'tags': 'tag',
+        }
+
+    idless = set(['operations', 'fencing-topology'])
+    isref = set(['resource_ref', 'obj_ref', 'crmsh-ref'])
+
+    def needs_id(node):
+        a = node.attrib
+        if node.tag in isref:
+            return False
+        return 'id-ref' not in a and node.tag not in idless
+
+    def next_prefix(node, refnode, prefix):
+        if node.tag == 'node' and 'uname' in node.attrib:
+            return node.get('uname')
+        if 'id' in node.attrib:
+            return node.get('id')
+        return prefix
+
+    def recurse(node, oldnode, prefix):
+        refnode = lookup_node(node, oldnode)
+        if needs_id(node):
+            idmgmt.set(node, refnode, prefix, id_required=(node.tag not in idless))
+        prefix = next_prefix(node, refnode, prefix)
+        for c in node.iterchildren():
+            if not is_comment(c):
+                recurse(c, refnode if refnode is not None else oldnode, prefix)
+
+    recurse(node, oldnode, hint_map.get(node.tag, ''))
+
+
+def resolve_idref(node):
+    """
+    resolve id-ref references that refer
+    to object ids, not attribute lists
+    """
+    id_ref = node.get('id-ref')
+    attr_list_type = node.tag
+    obj = cib_factory.find_object(id_ref)
+    if obj:
+        nodes = obj.node.xpath(".//%s" % attr_list_type)
+        if len(nodes) > 1:
+            common_warn("%s contains more than one %s, using first" %
+                        (obj.obj_id, attr_list_type))
+        if len(nodes) > 0:
+            node_id = nodes[0].get("id")
+            if node_id:
+                return node_id
+    target = cib_factory.get_cib().xpath('.//*[@id="%s"]' % (id_ref))
+    if len(target) == 0:
+        common_err("Reference not found: %s" % id_ref)
+    elif len(target) > 1:
+        common_err("Ambiguous reference to %s" % id_ref)
+    return id_ref
+
+
+def resolve_references(node):
+    """
+    In the output from parse(), there are
+    possible references to other nodes in
+    the CIB. This resolves those references.
+    """
+    idrefnodes = node.xpath('.//*[@id-ref]')
+    if 'id-ref' in node.attrib:
+        idrefnodes += [node]
+    for ref in idrefnodes:
+        ref.set('id-ref', resolve_idref(ref))
+    for ref in node.iterchildren('crmsh-ref'):
+        child_id = ref.get('id')
+        obj = cib_factory.find_object(child_id)
+        common_debug("resolve_references: %s -> %s" % (child_id, obj))
+        if obj is not None:
+            newnode = copy.deepcopy(obj.node)
+            node.replace(ref, newnode)
+        else:
+            node.remove(ref)
+            common_err("%s refers to missing object %s" % (node.get('id'),
+                                                           child_id))
+
+
+def id_for_node(node, id_hint=None):
+    "find id for unprocessed node"
+    root = node
+    if node.tag in constants.defaults_tags:
+        node = node[0]
+    if node.tag == 'fencing-topology':
+        obj_id = 'fencing_topology'
+    else:
+        obj_id = node.get('id') or node.get('uname')
+    if obj_id is None:
+        if node.tag == 'op':
+            if id_hint is None:
+                id_hint = node.get("rsc")
+            idmgmt.set(node, None, id_hint)
+            obj_id = node.get('id')
+        else:
+            defid = default_id_for_tag(root.tag)
+            if defid is not None:
+                try:
+                    node.set('id', defid)
+                except TypeError, e:
+                    raise ValueError('Internal error: %s (%s)' % (e, etree.tostring(node)))
+                obj_id = node.get('id')
+                idmgmt.save(obj_id)
+    if root.tag != "node" and obj_id and not is_id_valid(obj_id):
+        invalid_id_err(obj_id)
+        return None
+    return obj_id
+
+
+def postprocess_cli(node, oldnode=None, id_hint=None):
+    """
+    input: unprocessed but parsed XML
+    output: XML, obj_type, obj_id
+    """
+    if node.tag == 'op':
+        obj_type = 'op'
+    else:
+        obj_type = cib_object_map[node.tag][0]
+    obj_id = id_for_node(node, id_hint=id_hint)
+    if obj_id is None:
+        if obj_type == 'op':
+            # In this case, we need to delay postprocessing
+            # until we know where to insert the op
+            return node, obj_type, None
+        common_err("No ID found for %s: %s" % (obj_type, etree.tostring(node)))
+        return None, None, None
+    if node.tag in constants.defaults_tags:
+        node = node[0]
+    fix_node_ids(node, oldnode)
+    resolve_references(node)
+    if oldnode is not None:
+        remove_id_used_attributes(oldnode)
+    return node, obj_type, obj_id
+
+
+def parse_cli_to_xml(cli, oldnode=None, validation=None):
+    """
+    input: CLI text
+    output: XML, obj_type, obj_id
+    """
+    parser = CliParser()
+    if validation is not None:
+        for p in parser.parsers.values():
+            p.validation = validation
+    node = None
+    if isinstance(cli, basestring):
+        for s in lines2cli(cli):
+            node = parser.parse(s)
+    else:  # should be a pre-tokenized list
+        node = parser.parse(cli)
+    if node is False:
+        return None, None, None
+    elif node is None:
+        return None, None, None
+    return postprocess_cli(node, oldnode)
+
+#
+# cib element classes (CibObject the parent class)
+#
+class CibObject(object):
+    '''
+    The top level object of the CIB. Resources and constraints.
+    '''
+    state_fmt = "%16s %-8s%-8s%-8s%-4s"
+    set_names = {}
+
+    def __init__(self, xml_obj_type):
+        if not xml_obj_type in cib_object_map:
+            unsupported_err(xml_obj_type)
+            return
+        self.obj_type = cib_object_map[xml_obj_type][0]
+        self.parent_type = cib_object_map[xml_obj_type][2]
+        self.xml_obj_type = xml_obj_type
+        self.origin = ""        # where did it originally come from?
+        self.nocli = False      # we don't support this one
+        self.nocli_warn = True  # don't issue warnings all the time
+        self.updated = False    # was the object updated
+        self.parent = None      # object superior (group/clone/ms)
+        self.children = []      # objects inferior
+        self.obj_id = None
+        self.node = None
+
+    def __str__(self):
+        return "%s:%s" % (self.obj_type, self.obj_id)
+
+    def set_updated(self):
+        self.updated = True
+        self.propagate_updated()
+
+    def _dump_state(self):
+        'Print object status'
+        print self.state_fmt % (self.obj_id,
+                                self.origin,
+                                self.updated,
+                                self.parent and self.parent.obj_id or "",
+                                len(self.children))
+
+    def _repr_cli_xml(self, format):
+        if format < 0:
+            clidisplay.disable_pretty()
+        try:
+            h = clidisplay.keyword("xml")
+            l = etree.tostring(self.node, pretty_print=True).split('\n')
+            l = [x for x in l if x]  # drop empty lines
+            return "%s %s" % (h, cli_format(l, break_lines=(format > 0), xml=True))
+        finally:
+            if format < 0:
+                clidisplay.enable_pretty()
+
+    def _gv_rsc_id(self):
+        if self.parent and self.parent.obj_type in constants.clonems_tags:
+            return "%s:%s" % (self.parent.obj_type, self.obj_id)
+        return self.obj_id
+
+    def _set_gv_attrs(self, gv_obj, obj_type=None):
+        if not obj_type:
+            obj_type = self.obj_type
+        obj_id = self.node.get("uname") or self.obj_id
+        set_obj_attrs(gv_obj, obj_id, obj_type)
+
+    def _set_sg_attrs(self, sg_obj, obj_type=None):
+        if not obj_type:
+            obj_type = self.obj_type
+        set_graph_attrs(sg_obj, obj_type)
+
+    def _set_edge_attrs(self, gv_obj, e_id, obj_type=None):
+        if not obj_type:
+            obj_type = self.obj_type
+        set_edge_attrs(gv_obj, e_id, obj_type)
+
+    def repr_gv(self, gv_obj, from_grp=False):
+        '''
+        Add some graphviz elements to gv_obj.
+        '''
+        pass
+
+    def _repr_cli_head(self, format):
+        'implemented in subclasses'
+        pass
+
+    def repr_cli(self, format=1):
+        '''
+        CLI representation for the node.
+        _repr_cli_head and _repr_cli_child in subclasess.
+        '''
+        if self.nocli:
+            return self._repr_cli_xml(format)
+        l = []
+        if format < 0:
+            clidisplay.disable_pretty()
+        try:
+            head_s = self._repr_cli_head(format)
+            # everybody must have a head
+            if not head_s:
+                return None
+            comments = []
+            l.append(head_s)
+            desc = self.node.get("description")
+            if desc:
+                l.append(nvpair_format("description", desc))
+            for c in self.node.iterchildren():
+                if is_comment(c):
+                    comments.append(c.text)
+                    continue
+                s = self._repr_cli_child(c, format)
+                if s:
+                    l.append(s)
+            return self._cli_format_and_comment(l, comments, break_lines=(format > 0))
+        finally:
+            if format < 0:
+                clidisplay.enable_pretty()
+
+    def _attr_set_str(self, node):
+        '''
+        Add $id=<id> if the set id is referenced by another
+        element.
+
+        also show rule expressions if found
+        '''
+
+        has_nvpairs = len(node.xpath('.//nvpair')) > 0
+        idref = node.get('id-ref')
+
+        # don't skip empty sets: skipping these breaks
+        # patching
+        # empty set
+        # if not (has_nvpairs or idref is not None):
+        #    return ''
+
+        ret = "%s " % (clidisplay.keyword(self.set_names[node.tag]))
+        node_id = node.get("id")
+        if node_id is not None and cib_factory.is_id_refd(node.tag, node_id):
+            ret += "%s " % (nvpair_format("$id", node_id))
+        elif idref is not None:
+            ret += "%s " % (nvpair_format("$id-ref", idref))
+
+        score = node.get("score")
+        if score:
+            ret += "%s: " % (clidisplay.score(score))
+
+        for c in node.iterchildren():
+            if c.tag == "rule":
+                ret += "%s %s " % (clidisplay.keyword("rule"), cli_rule(c))
+        for c in node.iterchildren():
+            if c.tag == "nvpair":
+                ret += "%s " % (cli_nvpair(c))
+        if ret[-1] == ' ':
+            ret = ret[:-1]
+        return ret
+
+    def _repr_cli_child(self, c, format):
+        if c.tag in self.set_names:
+            return self._attr_set_str(c)
+
+    def _get_oldnode(self):
+        '''
+        Used to retrieve sub id's.
+        '''
+        if self.obj_type == "property":
+            return get_topnode(cib_factory.get_cib(), self.parent_type)
+        elif self.obj_type in constants.defaults_tags:
+            return self.node.getparent()
+        return self.node
+
+    def set_id(self, obj_id=None):
+        if obj_id is None and self.node is not None:
+            obj_id = self.node.get("id") or self.node.get('uname')
+            if obj_id is None:
+                m = cib_object_map.get(self.node.tag)
+                if m and len(m) > 3:
+                    obj_id = m[3]
+        self.obj_id = obj_id
+
+    def set_nodeid(self):
+        if self.node is not None and self.obj_id:
+            self.node.set("id", self.obj_id)
+
+    def cli2node(self, cli):
+        '''
+        Convert CLI representation to a DOM node.
+        '''
+        oldnode = self._get_oldnode()
+        node, obj_type, obj_id = parse_cli_to_xml(cli, oldnode)
+        return node
+
+    def set_node(self, node, oldnode=None):
+        self.node = node
+        self.set_id()
+        return self.node
+
+    def _cli_format_and_comment(self, l, comments, break_lines):
+        '''
+        Format and add comment (if any).
+        '''
+        s = cli_format(l, break_lines=break_lines)
+        cs = '\n'.join(comments)
+        return (comments and format >= 0) and '\n'.join([cs, s]) or s
+
+    def move_comments(self):
+        '''
+        Move comments to the top of the node.
+        '''
+        l = []
+        firstelem = None
+        for n in self.node.iterchildren():
+            if is_comment(n):
+                if firstelem:
+                    l.append(n)
+            else:
+                if not firstelem:
+                    firstelem = self.node.index(n)
+        for comm_node in l:
+            self.node.remove(comm_node)
+            self.node.insert(firstelem, comm_node)
+            firstelem += 1
+
+    def mknode(self, obj_id):
+        if self.xml_obj_type in constants.defaults_tags:
+            tag = "meta_attributes"
+        else:
+            tag = self.xml_obj_type
+        self.node = etree.Element(tag)
+        self.set_id(obj_id)
+        self.set_nodeid()
+        self.origin = "user"
+        return True
+
+    def can_be_renamed(self):
+        '''
+        Return False if this object can't be renamed.
+        '''
+        if self.obj_id is None:
+            return False
+        rscstat = RscState()
+        if not rscstat.can_delete(self.obj_id):
+            common_err("cannot rename a running resource (%s)" % self.obj_id)
+            return False
+        if not is_live_cib() and self.node.tag == "node":
+            common_err("cannot rename nodes")
+            return False
+        return True
+
+    def cli_use_validate(self):
+        '''
+        Check validity of the object, as we know it. It may
+        happen that we don't recognize a construct, but that the
+        object is still valid for the CRM. In that case, the
+        object is marked as "CLI read only", i.e. we will neither
+        convert it to CLI nor try to edit it in that format.
+
+        The validation procedure:
+        we convert xml to cli and then back to xml. If the two
+        xml representations match then we can understand the xml.
+
+        Complication:
+        There are valid variations of the XML where the CLI syntax
+        cannot express the difference. For example, sub-tags in a
+        <primitive> are not ordered, but the CLI syntax can only express
+        one specific ordering.
+
+        This is usually not a problem unless mixing pcs and crmsh.
+        '''
+        if self.node is None:
+            return True
+        clidisplay.disable_pretty()
+        cli_text = self.repr_cli(format=0)
+        clidisplay.enable_pretty()
+        if not cli_text:
+            common_debug("validation failed: %s" % (etree.tostring(self.node)))
+            return False
+        xml2 = self.cli2node(cli_text)
+        if xml2 is None:
+            common_debug("validation failed: %s -> %s" % (
+                etree.tostring(self.node),
+                cli_text))
+            return False
+        if not xml_equals(self.node, xml2, show=True):
+            common_debug("validation failed: %s -> %s -> %s" % (
+                etree.tostring(self.node),
+                cli_text,
+                etree.tostring(xml2)))
+            return False
+        return True
+
+    def _verify_op_attributes(self, op_node):
+        '''
+        Check if all operation attributes are supported by the
+        schema.
+        '''
+        rc = True
+        op_id = op_node.get("name")
+        for name in op_node.keys():
+            vals = schema.rng_attr_values(op_node.tag, name)
+            if not vals:
+                continue
+            v = op_node.get(name)
+            if v not in vals:
+                common_warn("%s: op '%s' attribute '%s' value '%s' not recognized" %
+                            (self.obj_id, op_id, name, v))
+                rc = False
+        return rc
+
+    def _check_ops_attributes(self):
+        '''
+        Check if operation attributes settings are valid.
+        '''
+        rc = True
+        if self.node is None:
+            return rc
+        for op_node in self.node.xpath("operations/op"):
+            rc |= self._verify_op_attributes(op_node)
+        return rc
+
+    def check_sanity(self):
+        '''
+        Right now, this is only for primitives.
+        And groups/clones/ms and cluster properties.
+        '''
+        return 0
+
+    def reset_updated(self):
+        self.updated = False
+        for child in self.children:
+            child.reset_updated()
+
+    def propagate_updated(self):
+        if self.parent:
+            self.parent.updated = self.updated
+            self.parent.propagate_updated()
+
+    def top_parent(self):
+        '''Return the top parent or self'''
+        if self.parent:
+            return self.parent.top_parent()
+        else:
+            return self
+
+    def find_child_in_node(self, child):
+        for c in self.node.iterchildren():
+            if c.tag == child.obj_type and \
+                    c.get("id") == child.obj_id:
+                return c
+        return None
+
+
+def gv_first_prim(node):
+    if node.tag != "primitive":
+        for c in node.iterchildren():
+            if is_child_rsc(c):
+                return gv_first_prim(c)
+    return node.get("id")
+
+
+def gv_first_rsc(rsc_id):
+    rsc_obj = cib_factory.find_object(rsc_id)
+    if not rsc_obj:
+        return rsc_id
+    return gv_first_prim(rsc_obj.node)
+
+
+def gv_last_prim(node):
+    if node.tag != "primitive":
+        for c in node.iterchildren(reversed=True):
+            if is_child_rsc(c):
+                return gv_last_prim(c)
+    return node.get("id")
+
+
+def gv_last_rsc(rsc_id):
+    rsc_obj = cib_factory.find_object(rsc_id)
+    if not rsc_obj:
+        return rsc_id
+    return gv_last_prim(rsc_obj.node)
+
+
+def gv_edge_score_label(gv_obj, e_id, node):
+    score = get_score(node) or get_kind(node)
+    if abs_pos_score(score):
+        gv_obj.new_edge_attr(e_id, 'style', 'solid')
+        return
+    elif re.match("-?([0-9]+|inf)$", score):
+        lbl = score
+    elif score in schema.rng_attr_values('rsc_order', 'kind'):
+        lbl = score
+    elif not score:
+        lbl = 'Adv'
+    else:
+        lbl = "attr:%s" % score
+    gv_obj.new_edge_attr(e_id, 'label', lbl)
+
+
+class CibNode(CibObject):
+    '''
+    Node and node's attributes.
+    '''
+    set_names = {
+        "instance_attributes": "attributes",
+        "utilization": "utilization",
+    }
+
+    def _repr_cli_head(self, format):
+        uname = self.node.get("uname")
+        s = clidisplay.keyword(self.obj_type)
+        if self.obj_id != uname:
+            if utils.noquotes(self.obj_id):
+                s = "%s %s:" % (s, self.obj_id)
+            else:
+                s = '%s $id="%s"' % (s, self.obj_id)
+        s = '%s %s' % (s, clidisplay.id(uname))
+        type = self.node.get("type")
+        if type and type != constants.node_default_type:
+            s = '%s:%s' % (s, type)
+        return s
+
+    def repr_gv(self, gv_obj, from_grp=False):
+        '''
+        Create a gv node. The label consists of the ID.
+        Nodes are square.
+        '''
+        uname = self.node.get("uname")
+        if not uname:
+            uname = self.obj_id
+        gv_obj.new_node(uname, top_node=True)
+        gv_obj.new_attr(uname, 'label', uname)
+        self._set_gv_attrs(gv_obj)
+
+
+def reduce_primitive(node):
+    '''
+    A primitive may reference template. If so, put the two
+    together.
+    Returns:
+        - if no template reference, node itself
+        - if template reference, but no template found, None
+        - return merged primitive node into template node
+    '''
+    template = node.get("template")
+    if not template:
+        return node
+    template_obj = cib_factory.find_object(template)
+    if not template_obj:
+        return None
+    return merge_tmpl_into_prim(node, template_obj.node)
+
+
+class Op(object):
+    '''
+    Operations.
+    '''
+    elem_type = "op"
+
+    def __init__(self, op_name, prim, node=None):
+        self.prim = prim
+        self.node = node
+        self.attr_d = odict()
+        self.attr_d["name"] = op_name
+        if self.node is not None:
+            self.xml2dict()
+
+    def set_attr(self, n, v):
+        self.attr_d[n] = v
+
+    def get_attr(self, n):
+        try:
+            return self.attr_d[n]
+        except KeyError:
+            return None
+
+    def del_attr(self, n):
+        try:
+            del self.attr_d[n]
+        except KeyError:
+            pass
+
+    def xml2dict(self):
+        for name in self.node.keys():
+            if name != "id":  # skip the id
+                self.set_attr(name, self.node.get(name))
+        for p in self.node.xpath("instance_attributes/nvpair"):
+            n = p.get("name")
+            v = p.get("value")
+            if n is not None and v is not None:
+                self.set_attr(n, v)
+
+    def mkxml(self):
+        # create an xml node
+        if self.node is not None:
+            if self.node.getparent() is not None:
+                self.node.getparent().remove(self.node)
+            idmgmt.remove_xml(self.node)
+        self.node = etree.Element(self.elem_type)
+        inst_attr = {}
+        valid_attrs = olist(schema.get('attr', 'op', 'a'))
+        for n, v in self.attr_d.iteritems():
+            if n in valid_attrs:
+                self.node.set(n, v)
+            else:
+                inst_attr[n] = v
+        idmgmt.set(self.node, None, self.prim)
+        if inst_attr:
+            nia = mkxmlnvpairs("instance_attributes", inst_attr, self.node.get("id"))
+            self.node.append(nia)
+        return self.node
+
+
+class CibPrimitive(CibObject):
+    '''
+    Primitives.
+    '''
+
+    set_names = {
+        "instance_attributes": "params",
+        "meta_attributes": "meta",
+        "utilization": "utilization",
+    }
+
+    def _repr_cli_head(self, format):
+        if self.obj_type == "primitive":
+            template_ref = self.node.get("template")
+        else:
+            template_ref = None
+        if template_ref:
+            rsc_spec = "@%s" % clidisplay.idref(template_ref)
+        else:
+            rsc_spec = mk_rsc_type(self.node)
+        s = clidisplay.keyword(self.obj_type)
+        id = clidisplay.id(self.obj_id)
+        return "%s %s %s" % (s, id, rsc_spec)
+
+    def _repr_cli_child(self, c, format):
+        if c.tag in self.set_names:
+            return self._attr_set_str(c)
+        elif c.tag == "operations":
+            return cli_operations(c, break_lines=(format > 0))
+
+    def _append_op(self, op_node):
+        try:
+            ops_node = self.node.findall("operations")[0]
+        except IndexError:
+            ops_node = etree.SubElement(self.node, "operations")
+        ops_node.append(op_node)
+
+    def add_operation(self, node):
+        # check if there is already an op with the same interval
+        name = node.get("name")
+        interval = node.get("interval")
+        if find_operation(self.node, name, interval) is not None:
+            common_err("%s already has a %s op with interval %s" %
+                       (self.obj_id, name, interval))
+            return None
+        # create an xml node
+        if 'id' not in node.attrib:
+            idmgmt.set(node, None, self.obj_id)
+        valid_attrs = olist(schema.get('attr', 'op', 'a'))
+        inst_attr = {}
+        for attr in node.attrib.keys():
+            if attr not in valid_attrs:
+                inst_attr[attr] = node.attrib[attr]
+                del node.attrib[attr]
+        if inst_attr:
+            attr_nodes = node.xpath('./instance_attributes')
+            if len(attr_nodes) == 1:
+                fill_nvpairs("instance_attributes", attr_nodes[0], inst_attr, node.get("id"))
+            else:
+                nia = mkxmlnvpairs("instance_attributes", inst_attr, node.get("id"))
+                node.append(nia)
+
+        self._append_op(node)
+        comments = find_comment_nodes(node)
+        for comment in comments:
+            node.remove(comment)
+        if comments and self.node is not None:
+            stuff_comments(self.node, [c.text for c in comments])
+        self.set_updated()
+        return self
+
+    def del_operation(self, op_node):
+        if op_node.getparent() is None:
+            return
+        ops_node = op_node.getparent()
+        op_node.getparent().remove(op_node)
+        idmgmt.remove_xml(op_node)
+        if len(ops_node) == 0:
+            rmnode(ops_node)
+        self.set_updated()
+
+    def is_dummy_operation(self, op_node):
+        '''If the op has just name, id, and interval=0, then it's
+        not of much use.'''
+        interval = op_node.get("interval")
+        if len(op_node) == 0 and crm_msec(interval) == 0:
+            attr_names = set(op_node.keys())
+            basic_attr_names = set(["id", "name", "interval"])
+            if len(attr_names ^ basic_attr_names) == 0:
+                return True
+        return False
+
+    def set_op_attr(self, op_node, attr_n, attr_v):
+        name = op_node.get("name")
+        op_obj = Op(name, self.obj_id, op_node)
+        op_obj.set_attr(attr_n, attr_v)
+        new_op_node = op_obj.mkxml()
+        self._append_op(new_op_node)
+        # the resource is updated
+        self.set_updated()
+        return new_op_node
+
+    def del_op_attr(self, op_node, attr_n):
+        name = op_node.get("name")
+        op_obj = Op(name, self.obj_id, op_node)
+        op_obj.del_attr(attr_n)
+        new_op_node = op_obj.mkxml()
+        self._append_op(new_op_node)
+        self.set_updated()
+        return new_op_node
+
+    def check_sanity(self):
+        '''
+        Check operation timeouts and if all required parameters
+        are defined.
+        '''
+        if self.node is None:  # eh?
+            common_err("%s: no xml (strange)" % self.obj_id)
+            return utils.get_check_rc()
+        rc3 = sanity_check_meta(self.obj_id, self.node, constants.rsc_meta_attributes)
+        if self.obj_type == "primitive":
+            r_node = reduce_primitive(self.node)
+            if r_node is None:
+                common_err("%s: no such resource template" % self.node.get("template"))
+                return utils.get_check_rc()
+        else:
+            r_node = self.node
+        ra = get_ra(r_node)
+        if ra.mk_ra_node() is None:  # no RA found?
+            if cib_factory.is_asymm_cluster():
+                return rc3
+            if config.core.ignore_missing_metadata:
+                return rc3
+            ra.error("no such resource agent")
+            return utils.get_check_rc()
+        actions = get_rsc_operations(r_node)
+        default_timeout = get_default_timeout()
+        rc2 = ra.sanity_check_ops(self.obj_id, actions, default_timeout)
+        rc4 = self._check_ops_attributes()
+        params = []
+        for c in r_node.iterchildren("instance_attributes"):
+            params += nvpairs2list(c)
+        rc1 = ra.sanity_check_params(self.obj_id,
+                                     params,
+                                     existence_only=(self.obj_type != "primitive"))
+        return rc1 | rc2 | rc3 | rc4
+
+    def repr_gv(self, gv_obj, from_grp=False):
+        '''
+        Create a gv node. The label consists of the ID and the
+        RA type.
+        '''
+        if self.obj_type == "primitive":
+            # if we belong to a group, but were not called with
+            # from_grp=True, then skip
+            if not from_grp and self.parent and self.parent.obj_type == "group":
+                return
+            n = reduce_primitive(self.node)
+            if n is None:
+                raise ValueError("Referenced template not found")
+            ra_class = n.get("class")
+            ra_type = n.get("type")
+            lbl_top = self._gv_rsc_id()
+            if ra_class in ("ocf", "stonith"):
+                lbl_bottom = ra_type
+            else:
+                lbl_bottom = "%s:%s" % (ra_class, ra_type)
+            gv_obj.new_node(self.obj_id, norank=(ra_class == "stonith"))
+            gv_obj.new_attr(self.obj_id, 'label', '%s\\n%s' % (lbl_top, lbl_bottom))
+            self._set_gv_attrs(gv_obj)
+            self._set_gv_attrs(gv_obj, "class:%s" % ra_class)
+            # if it's clone/ms, then get parent graph attributes
+            if self.parent and self.parent.obj_type in constants.clonems_tags:
+                self._set_gv_attrs(gv_obj, self.parent.obj_type)
+
+            template_ref = self.node.get("template")
+            if template_ref:
+                e = [template_ref, self.obj_id]
+                e_id = gv_obj.new_edge(e)
+                self._set_edge_attrs(gv_obj, e_id, 'template:edge')
+
+        elif self.obj_type == "rsc_template":
+            n = reduce_primitive(self.node)
+            if n is None:
+                raise ValueError("Referenced template not found")
+            ra_class = n.get("class")
+            ra_type = n.get("type")
+            lbl_top = self._gv_rsc_id()
+            if ra_class in ("ocf", "stonith"):
+                lbl_bottom = ra_type
+            else:
+                lbl_bottom = "%s:%s" % (ra_class, ra_type)
+            gv_obj.new_node(self.obj_id, norank=(ra_class == "stonith"))
+            gv_obj.new_attr(self.obj_id, 'label', '%s\\n%s' % (lbl_top, lbl_bottom))
+            self._set_gv_attrs(gv_obj)
+            self._set_gv_attrs(gv_obj, "class:%s" % ra_class)
+            # if it's clone/ms, then get parent graph attributes
+            if self.parent and self.parent.obj_type in constants.clonems_tags:
+                self._set_gv_attrs(gv_obj, self.parent.obj_type)
+
+
+class CibContainer(CibObject):
+    '''
+    Groups and clones and ms.
+    '''
+    set_names = {
+        "instance_attributes": "params",
+        "meta_attributes": "meta",
+    }
+
+    def _repr_cli_head(self, format):
+        children = []
+        for c in self.node.iterchildren():
+            if (self.obj_type == "group" and is_primitive(c)) or \
+                    is_child_rsc(c):
+                children.append(clidisplay.rscref(c.get("id")))
+            elif self.obj_type in constants.clonems_tags and is_child_rsc(c):
+                children.append(clidisplay.rscref(c.get("id")))
+        s = clidisplay.keyword(self.obj_type)
+        id = clidisplay.id(self.obj_id)
+        return "%s %s %s" % (s, id, ' '.join(children))
+
+    def check_sanity(self):
+        '''
+        Check meta attributes.
+        '''
+        if self.node is None:  # eh?
+            common_err("%s: no xml (strange)" % self.obj_id)
+            return utils.get_check_rc()
+        l = constants.rsc_meta_attributes
+        if self.obj_type == "clone":
+            l += constants.clone_meta_attributes
+        elif self.obj_type == "ms":
+            l += constants.clone_meta_attributes + constants.ms_meta_attributes
+        elif self.obj_type == "group":
+            l += constants.group_meta_attributes
+        rc = sanity_check_meta(self.obj_id, self.node, l)
+        return rc
+
+    def repr_gv(self, gv_obj, from_grp=False):
+        '''
+        A group is a subgraph.
+        Clones and ms just get different attributes.
+        '''
+        if self.obj_type != "group":
+            return
+        sg_obj = gv_obj.group([x.obj_id for x in self.children],
+                              "cluster_%s" % self.obj_id)
+        sg_obj.new_graph_attr('label', self._gv_rsc_id())
+        self._set_sg_attrs(sg_obj, self.obj_type)
+        if self.parent and self.parent.obj_type in constants.clonems_tags:
+            self._set_sg_attrs(sg_obj, self.parent.obj_type)
+        for child_rsc in self.children:
+            child_rsc.repr_gv(sg_obj, from_grp=True)
+
+
+class CibLocation(CibObject):
+    '''
+    Location constraint.
+    '''
+
+    def _repr_cli_head(self, format):
+        rsc = None
+        if "rsc" in self.node.keys():
+            rsc = self.node.get("rsc")
+        elif "rsc-pattern" in self.node.keys():
+            rsc = '/%s/' % (self.node.get("rsc-pattern"))
+        if rsc is not None:
+            rsc = clidisplay.rscref(rsc)
+        elif self.node.find("resource_set") is not None:
+            rsc = '{ %s }' % (' '.join(rsc_set_constraint(self.node, self.obj_type)))
+        else:
+            common_err("%s: unknown rsc_location format" % self.obj_id)
+            return None
+        s = clidisplay.keyword(self.obj_type)
+        id = clidisplay.id(self.obj_id)
+        s = "%s %s %s" % (s, id, rsc)
+
+        known_attrs = ['role', 'resource-discovery']
+        for attr in known_attrs:
+            val = self.node.get(attr)
+            if val is not None:
+                s += " %s=%s" % (attr, val)
+
+        pref_node = self.node.get("node")
+        score = clidisplay.score(get_score(self.node))
+        if pref_node is not None:
+            s = "%s %s: %s" % (s, score, pref_node)
+        return s
+
+    def _repr_cli_child(self, c, format):
+        if c.tag == "rule":
+            return "%s %s" % \
+                (clidisplay.keyword("rule"), cli_rule(c))
+
+    def check_sanity(self):
+        '''
+        Check if node references match existing nodes.
+        '''
+        if self.node is None:  # eh?
+            common_err("%s: no xml (strange)" % self.obj_id)
+            return utils.get_check_rc()
+        rc = 0
+        uname = self.node.get("node")
+        if uname and uname.lower() not in [id.lower() for id in cib_factory.node_id_list()]:
+            common_warn("%s: referenced node %s does not exist" % (self.obj_id, uname))
+            rc = 1
+        pattern = self.node.get("rsc-pattern")
+        if pattern:
+            try:
+                re.compile(pattern)
+            except IndexError, e:
+                common_warn("%s: '%s' may not be a valid regular expression (%s)" %
+                            (self.obj_id, pattern, e))
+                rc = 1
+            except re.error, e:
+                common_warn("%s: '%s' may not be a valid regular expression (%s)" %
+                            (self.obj_id, pattern, e))
+                rc = 1
+        for enode in self.node.xpath("rule/expression"):
+            if enode.get("attribute") == "#uname":
+                uname = enode.get("value")
+                ids = [i.lower() for i in cib_factory.node_id_list()]
+                if uname and uname.lower() not in ids:
+                    common_warn("%s: referenced node %s does not exist" % (self.obj_id, uname))
+                    rc = 1
+        return rc
+
+    def repr_gv(self, gv_obj, from_grp=False):
+        '''
+        What to do with the location constraint?
+        '''
+        pref_node = self.node.get("node")
+        if pref_node is not None:
+            score_n = self.node
+            # otherwise, it's too complex to render
+        elif is_pref_location(self.node):
+            score_n = self.node.findall("rule")[0]
+            exp = self.node.xpath("rule/expression")[0]
+            pref_node = exp.get("value")
+        else:
+            return
+        rsc_id = gv_first_rsc(self.node.get("rsc"))
+        e = [pref_node, rsc_id]
+        e_id = gv_obj.new_edge(e)
+        self._set_edge_attrs(gv_obj, e_id)
+        gv_edge_score_label(gv_obj, e_id, score_n)
+
+
+def _opt_set_name(n):
+    return "cluster%s" % n.get("id")
+
+
+def rsc_set_gv_edges(node, gv_obj):
+    def traverse_set(cum, st):
+        e = []
+        for i, elem in enumerate(cum):
+            if isinstance(elem, list):
+                for rsc in elem:
+                    cum2 = copy.copy(cum)
+                    cum2[i] = rsc
+                    traverse_set(cum2, st)
+                return
+            else:
+                e.append(elem)
+        st.append(e)
+
+    cum = []
+    for n in node.iterchildren("resource_set"):
+        sequential = get_boolean(n.get("sequential"), True)
+        require_all = get_boolean(n.get("require-all"), True)
+        l = get_rsc_ref_ids(n)
+        if not require_all and len(l) > 1:
+            sg_name = _opt_set_name(n)
+            cum.append('[%s]%s' % (sg_name, l[0]))
+        elif not sequential and len(l) > 1:
+            cum.append(l)
+        else:
+            cum += l
+    st = []
+    # deliver only 2-edges
+    for i, lvl in enumerate(cum):
+        if i == len(cum)-1:
+            break
+        traverse_set([cum[i], cum[i+1]], st)
+    return st
+
+
+class CibSimpleConstraint(CibObject):
+    '''
+    Colocation and order constraints.
+    '''
+
+    def _repr_cli_head(self, format):
+        s = clidisplay.keyword(self.obj_type)
+        id = clidisplay.id(self.obj_id)
+        score = get_score(self.node) or get_kind(self.node)
+        if self.node.find("resource_set") is not None:
+            col = rsc_set_constraint(self.node, self.obj_type)
+        else:
+            col = simple_rsc_constraint(self.node, self.obj_type)
+        if not col:
+            return None
+        if self.obj_type == "order":
+            symm = self.node.get("symmetrical")
+            if symm:
+                col.append("symmetrical=%s" % symm)
+        elif self.obj_type == "colocation":
+            node_attr = self.node.get("node-attribute")
+            if node_attr:
+                col.append("node-attribute=%s" % node_attr)
+        s = "%s %s " % (s, id)
+        if score != '':
+            s += "%s: " % (clidisplay.score(score))
+        return s + ' '.join(col)
+
+    def _mk_optional_set(self, gv_obj, n):
+        '''
+        Put optional resource set in a box.
+        '''
+        members = get_rsc_ref_ids(n)
+        sg_name = _opt_set_name(n)
+        sg_obj = gv_obj.optional_set(members, sg_name)
+        self._set_sg_attrs(sg_obj, "optional_set")
+
+    def _mk_one_edge(self, gv_obj, e):
+        '''
+        Create an edge between two resources (used for resource
+        sets). If the first resource name starts with '[', it's
+        an optional resource set which is later put into a subgraph.
+        The edge then goes from the subgraph to the resource
+        which follows. An expensive exception.
+        '''
+        optional_rsc = False
+        r = re.match(r'\[(.*)\]', e[0])
+        if r:
+            optional_rsc = True
+            sg_name = r.group(1)
+        e = [re.sub(r'\[(.*)\]', '', x) for x in e]
+        e = [gv_last_rsc(e[0]), gv_first_rsc(e[1])]
+        e_id = gv_obj.new_edge(e)
+        gv_edge_score_label(gv_obj, e_id, self.node)
+        if optional_rsc:
+            self._set_edge_attrs(gv_obj, e_id, 'optional_set')
+            gv_obj.new_edge_attr(e_id, 'ltail', gv_obj.gv_id(sg_name))
+
+    def repr_gv(self, gv_obj, from_grp=False):
+        '''
+        What to do with the collocation constraint?
+        '''
+        if self.obj_type != "order":
+            return
+        if self.node.find("resource_set") is not None:
+            for e in rsc_set_gv_edges(self.node, gv_obj):
+                self._mk_one_edge(gv_obj, e)
+            for n in self.node.iterchildren("resource_set"):
+                if not get_boolean(n.get("require-all"), True):
+                    self._mk_optional_set(gv_obj, n)
+        else:
+            self._mk_one_edge(gv_obj, [
+                self.node.get("first"),
+                self.node.get("then")])
+
+
+class CibRscTicket(CibSimpleConstraint):
+    '''
+    rsc_ticket constraint.
+    '''
+
+    def _repr_cli_head(self, format):
+        s = clidisplay.keyword(self.obj_type)
+        id = clidisplay.id(self.obj_id)
+        ticket = clidisplay.ticket(self.node.get("ticket"))
+        if self.node.find("resource_set") is not None:
+            col = rsc_set_constraint(self.node, self.obj_type)
+        else:
+            col = simple_rsc_constraint(self.node, self.obj_type)
+        if not col:
+            return None
+        a = self.node.get("loss-policy")
+        if a:
+            col.append("loss-policy=%s" % a)
+        return "%s %s %s: %s" % (s, id, ticket, ' '.join(col))
+
+
+class CibProperty(CibObject):
+    '''
+    Cluster properties.
+    '''
+
+    def _repr_cli_head(self, format):
+        return "%s %s" % (clidisplay.keyword(self.obj_type),
+                          head_id_format(self.obj_id))
+
+    def _repr_cli_child(self, c, format):
+        if c.tag == "rule":
+            return ' '.join((clidisplay.keyword("rule"),
+                             cli_rule(c)))
+        elif c.tag == "nvpair":
+            return cli_nvpair(c)
+        else:
+            return ''
+
+    def check_sanity(self):
+        '''
+        Match properties with PE metadata.
+        '''
+        if self.node is None:  # eh?
+            common_err("%s: no xml (strange)" % self.obj_id)
+            return utils.get_check_rc()
+        l = []
+        if self.obj_type == "property":
+            l = get_properties_list()
+            l += constants.extra_cluster_properties
+        elif self.obj_type == "op_defaults":
+            l = schema.get('attr', 'op', 'a')
+        elif self.obj_type == "rsc_defaults":
+            l = constants.rsc_meta_attributes
+        rc = sanity_check_nvpairs(self.obj_id, self.node, l)
+        return rc
+
+
+def is_stonith_rsc(xmlnode):
+    '''
+    True if resource is stonith or derived from stonith template.
+    '''
+    xmlnode = reduce_primitive(xmlnode)
+    if xmlnode is None:
+        return False
+    return xmlnode.get('class') == 'stonith'
+
+
+class CibFencingOrder(CibObject):
+    '''
+    Fencing order (fencing-topology).
+    '''
+
+    def set_id(self, obj_id=None):
+        self.obj_id = "fencing_topology"
+
+    def set_nodeid(self):
+        '''This id is not part of attributes'''
+        pass
+
+    def __str__(self):
+        return self.obj_id
+
+    def can_be_renamed(self):
+        ''' Cannot rename this one. '''
+        return False
+
+    def _repr_cli_head(self, format):
+        s = clidisplay.keyword(self.obj_type)
+        d = odict()
+        for c in self.node.iterchildren("fencing-level"):
+            target = c.get("target")
+            if target not in d:
+                d[target] = {}
+            d[target][c.get("index")] = c.get("devices")
+        dd = odict()
+        for target in d.keys():
+            sorted_keys = sorted([int(i) for i in d[target].keys()])
+            dd[target] = [d[target][str(x)] for x in sorted_keys]
+        d2 = {}
+        for target in dd.keys():
+            devs_s = ' '.join(dd[target])
+            d2[devs_s] = 1
+        if len(d2) == 1 and len(d) == len(cib_factory.node_id_list()):
+            return "%s %s" % (s, devs_s)
+        return cli_format([s] + ["%s: %s" % (x, ' '.join(dd[x]))
+                                 for x in dd.keys()],
+                          break_lines=(format > 0))
+
+    def _repr_cli_child(self, c, format):
+        pass  # no children here
+
+    def check_sanity(self):
+        '''
+        Targets are nodes and resource are stonith resources.
+        '''
+        if self.node is None:  # eh?
+            common_err("%s: no xml (strange)" % self.obj_id)
+            return utils.get_check_rc()
+        rc = 0
+        nl = self.node.findall("fencing-level")
+        for target in [x.get("target") for x in nl]:
+            if target.lower() not in [id.lower() for id in cib_factory.node_id_list()]:
+                common_warn("%s: target %s not a node" % (self.obj_id, target))
+                rc = 1
+        stonith_rsc_l = [x.obj_id for x in
+                         cib_factory.get_elems_on_type("type:primitive")
+                         if is_stonith_rsc(x.node)]
+        for devices in [x.get("devices") for x in nl]:
+            for dev in devices.split(","):
+                if not cib_factory.find_object(dev):
+                    common_warn("%s: resource %s does not exist" % (self.obj_id, dev))
+                    rc = 1
+                elif dev not in stonith_rsc_l:
+                    common_warn("%s: %s not a stonith resource" % (self.obj_id, dev))
+                    rc = 1
+        return rc
+
+
+class CibAcl(CibObject):
+    '''
+    User and role ACL.
+
+    Now with support for 1.1.12 style ACL rules.
+
+    '''
+
+    def _repr_cli_head(self, format):
+        s = clidisplay.keyword(self.obj_type)
+        id = clidisplay.id(self.obj_id)
+        return "%s %s" % (s, id)
+
+    def _repr_cli_child(self, c, format):
+        if c.tag in constants.acl_rule_names:
+            return cli_acl_rule(c, format)
+        elif c.tag == "role_ref":
+            return cli_acl_roleref(c, format)
+        elif c.tag == "role":
+            return cli_acl_role(c)
+        elif c.tag == "acl_permission":
+            return cli_acl_permission(c)
+
+
+class CibTag(CibObject):
+    '''
+    Tag objects
+
+    TODO: check_sanity, repr_gv
+
+    '''
+
+    def _repr_cli_head(self, fmt):
+        s = clidisplay.keyword(self.obj_type)
+        id_ = clidisplay.id(self.obj_id)
+        sub = ' '.join(clidisplay.rscref(c.get('id'))
+                       for c in self.node.iterchildren() if not is_comment(c))
+        return "%s %s: %s" % (s, id_, sub)
+
+
+#
+################################################################
+
+
+#
+# cib factory
+#
+cib_piped = "cibadmin -p"
+
+
+def get_default_timeout():
+    t = cib_factory.get_op_default("timeout")
+    if t:
+        return t
+    t = cib_factory.get_property("default-action-timeout")
+    if t:
+        return t
+    try:
+        return get_pe_meta().param_default("default-action-timeout")
+    except:
+        return 0
+
+# xml -> cli translations (and classes)
+cib_object_map = {
+    # xml_tag: ( cli_name, element class, parent element tag, id hint )
+    "node": ("node", CibNode, "nodes"),
+    "primitive": ("primitive", CibPrimitive, "resources"),
+    "group": ("group", CibContainer, "resources"),
+    "clone": ("clone", CibContainer, "resources"),
+    "master": ("ms", CibContainer, "resources"),
+    "template": ("rsc_template", CibPrimitive, "resources"),
+    "rsc_location": ("location", CibLocation, "constraints"),
+    "rsc_colocation": ("colocation", CibSimpleConstraint, "constraints"),
+    "rsc_order": ("order", CibSimpleConstraint, "constraints"),
+    "rsc_ticket": ("rsc_ticket", CibRscTicket, "constraints"),
+    "cluster_property_set": ("property", CibProperty, "crm_config", "cib-bootstrap-options"),
+    "rsc_defaults": ("rsc_defaults", CibProperty, "rsc_defaults", "rsc-options"),
+    "op_defaults": ("op_defaults", CibProperty, "op_defaults", "op-options"),
+    "fencing-topology": ("fencing_topology", CibFencingOrder, "configuration"),
+    "acl_role": ("role", CibAcl, "acls"),
+    "acl_user": ("user", CibAcl, "acls"),
+    "acl_target": ("acl_target", CibAcl, "acls"),
+    "acl_group": ("acl_group", CibAcl, "acls"),
+    "tag": ("tag", CibTag, "tags"),
+}
+
+
+# generate a translation cli -> tag
+backtrans = odict((item[0], key) for key, item in cib_object_map.iteritems())
+
+
+def default_id_for_tag(tag):
+    "Get default id for XML tag"
+    m = cib_object_map.get(tag, tuple())
+    return m[3] if len(m) > 3 else None
+
+
+def default_id_for_obj(obj_type):
+    "Get default id for object type"
+    return default_id_for_tag(backtrans.get(obj_type))
+
+
+def can_migrate(node):
+    return 'true' in node.xpath('.//nvpair[@name="allow-migrate"]/@value')
+
+
+cib_upgrade = "cibadmin --upgrade --force"
+
+
+class CibFactory(object):
+    '''
+    Juggle with CIB objects.
+    See check_structure below for details on the internal cib
+    representation.
+    '''
+
+    def __init__(self):
+        self._init_vars()
+        self.regtest = options.regression_tests
+        self.last_commit_time = 0
+        # internal (just not to produce silly messages)
+        self._no_constraint_rm_msg = False
+        # FIXME
+        self.supported_cib_re = "^pacemaker-[12][.][0123]$"
+        self._crm_diff_cmd = None
+
+    def is_cib_sane(self):
+        # try to initialize
+        if self.cib_elem is None:
+            self.initialize()
+            if self.cib_elem is None:
+                empty_cib_err()
+                return False
+        return True
+
+    def get_cib(self):
+        if not self.is_cib_sane():
+            return None
+        return self.cib_elem
+    #
+    # check internal structures
+    #
+
+    def _check_parent(self, obj, parent):
+        if not obj in parent.children:
+            common_err("object %s does not reference its child %s" %
+                       (parent.obj_id, obj.obj_id))
+            return False
+        if parent.node != obj.node.getparent():
+            if obj.node.getparent() is None:
+                common_err("object %s node is not a child of its parent %s" %
+                           (obj.obj_id, parent.obj_id))
+            else:
+                common_err("object %s node is not a child of its parent %s, but %s:%s" %
+                           (obj.obj_id,
+                            parent.obj_id,
+                            obj.node.getparent().tag,
+                            obj.node.getparent().get("id")))
+            return False
+        return True
+
+    def check_structure(self):
+        if not self.is_cib_sane():
+            return False
+        rc = True
+        for obj in self.cib_objects:
+            if obj.parent:
+                if not self._check_parent(obj, obj.parent):
+                    common_debug("check_parent failed: %s %s" % (obj.obj_id, obj.parent))
+                    rc = False
+            for child in obj.children:
+                if not child.parent:
+                    common_err("child %s does not reference its parent %s" %
+                               (child.obj_id, obj.obj_id))
+                    rc = False
+        return rc
+
+    def regression_testing(self, param):
+        # provide some help for regression testing
+        # in particular by trying to provide output which is
+        # easier to predict
+        if param == "off":
+            self.regtest = False
+        elif param == "on":
+            self.regtest = True
+        else:
+            common_warn("bad parameter for regtest: %s" % param)
+
+    def get_schema(self):
+        return self.cib_attrs["validate-with"]
+
+    def change_schema(self, schema_st):
+        'Use another schema'
+        if schema_st == self.get_schema():
+            common_info("already using schema %s" % schema_st)
+            return True
+        if not re.match(self.supported_cib_re, schema_st):
+            common_err("schema %s not supported by the shell" % schema_st)
+            return False
+        self.cib_elem.set("validate-with", schema_st)
+        if not schema.test_schema(self.cib_elem):
+            self.cib_elem.set("validate-with", self.get_schema())
+            common_err("schema %s does not exist" % schema_st)
+            return False
+        schema.init_schema(self.cib_elem)
+        rc = True
+        for obj in self.cib_objects:
+            if schema.get('sub', obj.node.tag, 'a') is None:
+                common_err("Element '%s' is not supported by the RNG schema %s" %
+                           (obj.node.tag, schema_st))
+                common_debug("Offending object: %s" % (etree.tostring(obj.node)))
+                rc = False
+        if not rc:
+            # revert, as some elements won't validate
+            self.cib_elem.set("validate-with", self.get_schema())
+            schema.init_schema(self.cib_elem)
+            common_err("current configuration not valid with %s, cannot change schema" % schema_st)
+            return 4
+        self.cib_attrs["validate-with"] = schema_st
+        self.new_schema = True
+        return 0
+
+    def is_elem_supported(self, obj_type):
+        'Do we support this element?'
+        try:
+            if schema.get('sub', backtrans[obj_type], 'a') is None:
+                return False
+        except KeyError:
+            pass
+        return True
+
+    def is_cib_supported(self):
+        'Do we support this CIB?'
+        req = self.cib_elem.get("crm_feature_set")
+        validator = self.cib_elem.get("validate-with")
+        if validator and re.match(self.supported_cib_re, validator):
+            return True
+        cib_ver_unsupported_err(validator, req)
+        return False
+
+    def upgrade_cib_06to10(self, force=False):
+        'Upgrade the CIB from 0.6 to 1.0.'
+        if not self.is_cib_sane():
+            return False
+        validator = self.cib_elem.get("validate-with")
+        if force or not validator or re.match("0[.]6", validator):
+            return ext_cmd(cib_upgrade) == 0
+
+    def _import_cib(self, cib_elem):
+        'Parse the current CIB (from cibadmin -Q).'
+        self.cib_elem = cib_elem
+        if self.cib_elem is None:
+            return False
+        if not self.is_cib_supported():
+            self.reset()
+            return False
+        self._get_cib_attributes(self.cib_elem)
+        schema.init_schema(self.cib_elem)
+        return True
+
+    #
+    # create a doc from the list of objects
+    # (used by CibObjectSetRaw)
+    #
+    def bump_epoch(self):
+        try:
+            self.cib_attrs["epoch"] = str(int(self.cib_attrs["epoch"])+1)
+        except:
+            self.cib_attrs["epoch"] = "1"
+        common_debug("Bump epoch to %s" % (self.cib_attrs["epoch"]))
+
+    def _get_cib_attributes(self, cib):
+        for attr in cib.keys():
+            self.cib_attrs[attr] = cib.get(attr)
+
+    def _set_cib_attributes(self, cib):
+        for attr in self.cib_attrs:
+            cib.set(attr, self.cib_attrs[attr])
+
+    def _copy_cib_attributes(self, src_cib, cib):
+        """
+        Copy CIB attributes from src_cib to cib.
+        Also updates self.cib_attrs.
+        Preserves attributes that may be modified by
+        the user (for example validate-with).
+        """
+        attrs = ((attr, src_cib.get(attr))
+                 for attr in self.cib_attrs
+                 if attr not in constants.cib_user_attrs)
+        for attr, value in attrs:
+            self.cib_attrs[attr] = value
+            cib.set(attr, value)
+
+    def obj_set2cib(self, obj_set, obj_filter=None):
+        '''
+        Return document containing objects in obj_set.
+        Must remove all children from the object list, because
+        printing xml of parents will include them.
+        Optional filter to sieve objects.
+        '''
+        cib_elem = new_cib()
+        # get only top parents for the objects in the list
+        # e.g. if we get a primitive which is part of a clone,
+        # then the clone gets in, not the primitive
+        # dict will weed out duplicates
+        d = {}
+        for obj in obj_set:
+            if obj_filter and not obj_filter(obj):
+                continue
+            d[obj.top_parent()] = 1
+        for obj in d:
+            get_topnode(cib_elem, obj.parent_type).append(copy.deepcopy(obj.node))
+        self._set_cib_attributes(cib_elem)
+        return cib_elem
+
+    #
+    # commit changed objects to the CIB
+    #
+    def _attr_match(self, c, a):
+        'Does attribute match?'
+        return c.get(a) == self.cib_attrs.get(a)
+
+    def is_current_cib_equal(self, silent=False):
+        cib_elem = read_cib(cibdump2elem)
+        if cib_elem is None:
+            return False
+        rc = self._attr_match(cib_elem, 'epoch') and \
+            self._attr_match(cib_elem, 'admin_epoch')
+        if not silent and not rc:
+            common_warn("CIB changed in the meantime: won't touch it!")
+        return rc
+
+    def _state_header(self):
+        'Print object status header'
+        print CibObject.state_fmt % \
+            ("", "origin", "updated", "parent", "children")
+
+    def showobjects(self):
+        self._state_header()
+        for obj in self.cib_objects:
+            obj._dump_state()
+        if self.remove_queue:
+            print "Remove queue:"
+            for obj in self.remove_queue:
+                obj._dump_state()
+
+    def commit(self, force=False):
+        'Commit the configuration to the CIB.'
+        if not self.is_cib_sane():
+            return False
+        if cibadmin_can_patch():
+            rc = self._patch_cib(force)
+        else:
+            rc = self._replace_cib(force)
+        if rc:
+            # reload the cib!
+            t = time.time()
+            common_debug("CIB commit successful at %s" % (t))
+            if is_live_cib():
+                self.last_commit_time = t
+            self.reset()
+        return rc
+
+    def _update_schema(self):
+        '''
+        Set the validate-with, if the schema changed.
+        '''
+        s = '<cib validate-with="%s"/>' % self.cib_attrs["validate-with"]
+        rc = pipe_string("%s -U" % cib_piped, s)
+        if rc != 0:
+            update_err("cib", "-U", s, rc)
+            return False
+        self.new_schema = False
+        return True
+
+    def _replace_cib(self, force):
+        try:
+            conf_el = self.cib_elem.findall("configuration")[0]
+        except IndexError:
+            common_error("cannot find the configuration element")
+            return False
+        if self.new_schema and not self._update_schema():
+            return False
+        cibadmin_opts = force and "-R --force" or "-R"
+        rc = pipe_string("%s %s" % (cib_piped, cibadmin_opts), etree.tostring(conf_el))
+        if rc != 0:
+            update_err("cib", cibadmin_opts, etree.tostring(conf_el), rc)
+            return False
+        return True
+
+    def _patch_cib(self, force):
+        # copy the epoch from the current cib to both the target
+        # cib and the original one (otherwise cibadmin won't want
+        # to apply the patch)
+        current_cib = read_cib(cibdump2elem)
+        if current_cib is None:
+            return False
+
+        # check if crm_diff supports --no-version
+        if self._crm_diff_cmd is None:
+            rc, out = utils.get_stdout("crm_diff --help")
+            if "--no-version" in out:
+                self._crm_diff_cmd = 'crm_diff --no-version'
+            else:
+                self._crm_diff_cmd = 'crm_diff'
+
+        self._copy_cib_attributes(current_cib, self.cib_orig)
+        current_cib = None  # don't need that anymore
+        # only bump epoch if we don't have support for --no-version
+        if not self._crm_diff_cmd.endswith('--no-version'):
+            # now increase the epoch by 1
+            self.bump_epoch()
+        self._set_cib_attributes(self.cib_elem)
+        cib_s = etree.tostring(self.cib_orig, pretty_print=True)
+        tmpf = str2tmp(cib_s, suffix=".xml")
+        if not tmpf:
+            return False
+        tmpfiles.add(tmpf)
+        cibadmin_opts = force and "-P --force" or "-P"
+
+        # produce a diff:
+        # dump_new_conf | crm_diff -o self.cib_orig -n -
+        common_debug("Input: %s" % (etree.tostring(self.cib_elem)))
+        rc, cib_diff = filter_string("%s -o %s -n -" %
+                                     (self._crm_diff_cmd, tmpf),
+                                     etree.tostring(self.cib_elem))
+        if not cib_diff and (rc == 0):
+            # no diff = no action
+            return True
+        elif not cib_diff:
+            common_err("crm_diff apparently failed to produce the diff (rc=%d)" % rc)
+            return False
+        if not self._crm_diff_cmd.endswith('--no-version'):
+            # skip the version information for source and target
+            # if we dont have support for --no-version
+            e = etree.fromstring(cib_diff)
+            for tag in e.xpath("./version/*[self::target or self::source]"):
+                tag.attrib.clear()
+            cib_diff = etree.tostring(e)
+        # for v1 diffs, fall back to non-patching if
+        # any containers are modified, else strip the digest
+        if "<diff" in cib_diff and "digest=" in cib_diff:
+            if not self.can_patch_v1():
+                return self._replace_cib(force)
+            e = etree.fromstring(cib_diff)
+            for tag in e.xpath("/diff"):
+                if "digest" in tag.attrib:
+                    del tag.attrib["digest"]
+            cib_diff = etree.tostring(e)
+        common_debug("Diff: %s" % (cib_diff))
+        rc = pipe_string("%s %s" % (cib_piped, cibadmin_opts),
+                         cib_diff)
+        if rc != 0:
+            update_err("cib", cibadmin_opts, cib_diff, rc)
+            return False
+        return True
+
+    def can_patch_v1(self):
+        """
+        The v1 patch format cannot handle reordering,
+        so if there are any changes to any containers
+        or acl tags, don't patch.
+        """
+        def group_changed():
+            for obj in self.cib_objects:
+                if not obj.updated:
+                    continue
+                if obj.obj_type in constants.container_tags:
+                    return True
+                if obj.obj_type in ('user', 'role', 'acl_target', 'acl_group'):
+                    return True
+            return False
+        return not group_changed()
+
+    #
+    # initialize cib_objects from CIB
+    #
+    def _create_object_from_cib(self, node, pnode=None):
+        '''
+        Need pnode (parent node) acrobacy because cluster
+        properties and rsc/op_defaults hold stuff in a
+        meta_attributes child.
+        '''
+        assert node is not None
+        if pnode is None:
+            pnode = node
+        obj = cib_object_map[pnode.tag][1](pnode.tag)
+        obj.origin = "cib"
+        obj.node = node
+        obj.set_id()
+        self.cib_objects.append(obj)
+        return obj
+
+    def _populate(self):
+        "Walk the cib and collect cib objects."
+        all_nodes = get_interesting_nodes(self.cib_elem, [])
+        if not all_nodes:
+            return
+        for node in processing_sort(all_nodes):
+            if is_defaults(node):
+                for c in node.xpath("./meta_attributes"):
+                    self._create_object_from_cib(c, node)
+            else:
+                self._create_object_from_cib(node)
+        for obj in self.cib_objects:
+            obj.move_comments()
+            fix_comments(obj.node)
+        self.cli_use_validate_all()
+        for obj in self.cib_objects:
+            self._update_links(obj)
+
+    def cli_use_validate_all(self):
+        for obj in self.cib_objects:
+            if not obj.cli_use_validate():
+                obj.nocli = True
+                obj.nocli_warn = False
+                # no need to warn, user can see the object displayed as XML
+                common_debug("object %s cannot be represented in the CLI notation" % (obj.obj_id))
+
+    def initialize(self, cib=None):
+        if self.cib_elem is not None:
+            return True
+        if cib is None:
+            cib = read_cib(cibdump2elem)
+        elif isinstance(cib, basestring):
+            cib = cibtext2elem(cib)
+        if not self._import_cib(cib):
+            return False
+        sanitize_cib(self.cib_elem)
+        if cibadmin_can_patch():
+            self.cib_orig = copy.deepcopy(self.cib_elem)
+        show_unrecognized_elems(self.cib_elem)
+        self._populate()
+        return self.check_structure()
+
+    def _init_vars(self):
+        self.cib_elem = None     # the cib
+        self.cib_orig = None     # the CIB which we loaded
+        self.cib_attrs = {}      # cib version dictionary
+        self.cib_objects = []    # a list of cib objects
+        self.remove_queue = []   # a list of cib objects to be removed
+        self.id_refs = {}        # dict of id-refs
+        self.new_schema = False  # schema changed
+        self._state = []
+
+    def _push_state(self):
+        '''
+        A rudimentary instance state backup. Just make copies of
+        all important variables.
+        idmgmt has to be backed up too.
+        '''
+        self._state.append([copy.deepcopy(x)
+                            for x in (self.cib_elem,
+                                      self.cib_attrs,
+                                      self.cib_objects,
+                                      self.remove_queue,
+                                      self.id_refs)])
+        idmgmt.push_state()
+
+    def _pop_state(self):
+        try:
+            common_debug("performing rollback from %s" % (self.cib_objects))
+            self.cib_elem, \
+                self.cib_attrs, self.cib_objects, \
+                self.remove_queue, self.id_refs = self._state.pop()
+        except KeyError:
+            return False
+        # need to get addresses of all new objects created by
+        # deepcopy
+        for obj in self.cib_objects:
+            obj.node = self.find_node(obj.xml_obj_type, obj.obj_id)
+            self._update_links(obj)
+        idmgmt.pop_state()
+        return self.check_structure()
+
+    def _drop_state(self):
+        try:
+            self._state.pop()
+        except KeyError:
+            pass
+        idmgmt.drop_state()
+
+    def _clean_state(self):
+        self._state = []
+        idmgmt.clean_state()
+
+    def reset(self):
+        if self.cib_elem is None:
+            return
+        self.cib_elem = None
+        self.cib_orig = None
+        self._init_vars()
+        self._clean_state()
+        idmgmt.clear()
+
+    def find_objects(self, obj_id):
+        "Find objects for id (can be a wildcard-glob)."
+        if not self.is_cib_sane() or obj_id is None:
+            return None
+        matchfn = lambda x: x and fnmatch.fnmatch(x, obj_id)
+        objs = []
+        for obj in self.cib_objects:
+            if matchfn(obj.obj_id):
+                objs.append(obj)
+            # special case for Heartbeat nodes which have id
+            # different from uname
+            elif obj.obj_type == "node" and matchfn(obj.node.get("uname")):
+                objs.append(obj)
+        return objs
+
+    def find_object(self, obj_id):
+        if not self.is_cib_sane():
+            return None
+        objs = self.find_objects(obj_id)
+        if objs is None:
+            return None
+        if len(objs) > 0:
+            return objs[0]
+        return None
+
+    #
+    # tab completion functions
+    #
+    def id_list(self):
+        "List of ids (for completion)."
+        return [x.obj_id for x in self.cib_objects]
+
+    def type_list(self):
+        "List of object types (for completion)"
+        return list(set([x.obj_type for x in self.cib_objects]))
+
+    def prim_id_list(self):
+        "List of primitives ids (for group completion)."
+        return [x.obj_id for x in self.cib_objects if x.obj_type == "primitive"]
+
+    def children_id_list(self):
+        "List of child ids (for clone/master completion)."
+        return [x.obj_id for x in self.cib_objects if x.obj_type in constants.children_tags]
+
+    def rsc_id_list(self):
+        "List of all resource ids."
+        return [x.obj_id for x in self.cib_objects
+                if x.obj_type in constants.resource_tags]
+
+    def top_rsc_id_list(self):
+        "List of top resource ids (for constraint completion)."
+        return [x.obj_id for x in self.cib_objects
+                if x.obj_type in constants.resource_tags and not x.parent]
+
+    def node_id_list(self):
+        "List of node ids."
+        return [x.node.get("uname") for x in self.cib_objects
+                if x.obj_type == "node"]
+
+    def f_prim_free_id_list(self):
+        "List of possible primitives ids (for group completion)."
+        return [x.obj_id for x in self.cib_objects
+                if x.obj_type == "primitive" and not x.parent]
+
+    def f_group_id_list(self):
+        "List of group ids."
+        return [x.obj_id for x in self.cib_objects
+                if x.obj_type == "group"]
+
+    def rsc_template_list(self):
+        "List of templates."
+        return [x.obj_id for x in self.cib_objects
+                if x.obj_type == "rsc_template"]
+
+    def f_children_id_list(self):
+        "List of possible child ids (for clone/master completion)."
+        return [x.obj_id for x in self.cib_objects
+                if x.obj_type in constants.children_tags and not x.parent]
+
+    #
+    # a few helper functions
+    #
+    def find_object_for_node(self, node):
+        "Find an object which matches a dom node."
+        for obj in self.cib_objects:
+            if node.tag == "fencing-topology" and \
+                    obj.xml_obj_type == "fencing-topology":
+                return obj
+            if node.get("id") == obj.obj_id:
+                return obj
+        return None
+
+    def find_node(self, tag, id, strict=True):
+        "Find a node of this type with this id."
+        try:
+            if tag in constants.defaults_tags:
+                expr = '//%s/meta_attributes[@id="%s"]' % (tag, id)
+            elif tag == 'fencing-topology':
+                expr = '//fencing-topology' % tag
+            else:
+                expr = '//%s[@id="%s"]' % (tag, id)
+            return self.cib_elem.xpath(expr)[0]
+        except IndexError:
+            if strict:
+                common_warn("strange, %s element %s not found" % (tag, id))
+            return None
+
+    #
+    # Element editing stuff.
+    #
+    def default_timeouts(self, *args):
+        '''
+        Set timeouts for operations from the defaults provided in
+        the meta-data.
+        '''
+        implied_actions = ["start", "stop"]
+        implied_ms_actions = ["promote", "demote"]
+        implied_migrate_actions = ["migrate_to", "migrate_from"]
+        other_actions = ("monitor",)
+        if not self.is_cib_sane():
+            return False
+        rc = True
+        for obj_id in args:
+            obj = self.find_object(obj_id)
+            if not obj:
+                no_object_err(obj_id)
+                rc = False
+                continue
+            if obj.obj_type != "primitive":
+                common_warn("element %s is not a primitive" % obj_id)
+                rc = False
+                continue
+            r_node = reduce_primitive(obj.node)
+            if r_node is None:
+                # cannot do anything without template defined
+                common_warn("template for %s not defined" % obj_id)
+                rc = False
+                continue
+            ra = get_ra(r_node)
+            if not ra.mk_ra_node():  # no RA found?
+                if not self.is_asymm_cluster():
+                    ra.error("no resource agent found for %s" % obj_id)
+                continue
+            obj_modified = False
+            for c in r_node.iterchildren():
+                if c.tag == "operations":
+                    for c2 in c.iterchildren():
+                        if not c2.tag == "op":
+                            continue
+                        op, pl = op2list(c2)
+                        if not op:
+                            continue
+                        if op in implied_actions:
+                            implied_actions.remove(op)
+                        elif can_migrate(r_node) and op in implied_migrate_actions:
+                            implied_migrate_actions.remove(op)
+                        elif is_ms(obj.node.getparent()) and op in implied_ms_actions:
+                            implied_ms_actions.remove(op)
+                        elif op not in other_actions:
+                            continue
+                        adv_timeout = ra.get_adv_timeout(op, c2)
+                        if adv_timeout:
+                            c2.set("timeout", adv_timeout)
+                            obj_modified = True
+            l = implied_actions
+            if can_migrate(r_node):
+                l += implied_migrate_actions
+            if is_ms(obj.node.getparent()):
+                l += implied_ms_actions
+            for op in l:
+                adv_timeout = ra.get_adv_timeout(op)
+                if not adv_timeout:
+                    continue
+                n = etree.Element('op')
+                n.set('name', op)
+                n.set('timeout', adv_timeout)
+                n.set('interval', '0')
+                if not obj.add_operation(n):
+                    rc = False
+                else:
+                    obj_modified = True
+            if obj_modified:
+                obj.set_updated()
+        return rc
+
+    def is_id_refd(self, attr_list_type, id):
+        '''
+        Is this ID referenced anywhere?
+        Used from cliformat
+        '''
+        try:
+            return self.id_refs[id] == attr_list_type
+        except KeyError:
+            return False
+
+    def resolve_id_ref(self, attr_list_type, id_ref):
+        '''
+        User is allowed to specify id_ref either as a an object
+        id or as attributes id. Here we try to figure out which
+        one, i.e. if the former is the case to find the right
+        id to reference.
+        '''
+        self.id_refs[id_ref] = attr_list_type
+        obj = self.find_object(id_ref)
+        if obj:
+            nodes = obj.node.xpath(".//%s" % attr_list_type)
+            if len(nodes) > 1:
+                common_warn("%s contains more than one %s, using first" %
+                            (obj.obj_id, attr_list_type))
+            if len(nodes) > 0:
+                node_id = nodes[0].get("id")
+                if node_id:
+                    return node_id
+        target = self.cib_elem.xpath('.//*[@id="%s"]' % (id_ref))
+        if len(target) == 0:
+            common_err("Reference not found: %s" % id_ref)
+        elif len(target) > 1:
+            common_err("Ambiguous reference to %s" % id_ref)
+        return id_ref
+
+    def _get_attr_value(self, obj_type, attr):
+        if not self.is_cib_sane():
+            return None
+        for obj in self.cib_objects:
+            if obj.obj_type == obj_type and obj.node is not None:
+                for n in nvpairs2list(obj.node):
+                    if n.get('name') == attr:
+                        return n.get('value')
+        return None
+
+    def get_property(self, property):
+        '''
+        Get the value of the given cluster property.
+        '''
+        return self._get_attr_value("property", property)
+
+    def get_op_default(self, attr):
+        '''
+        Get the value of the attribute from op_defaults.
+        '''
+        return self._get_attr_value("op_defaults", attr)
+
+    def is_asymm_cluster(self):
+        symm = self.get_property("symmetric-cluster")
+        return symm and symm != "true"
+
+    def new_object(self, obj_type, obj_id):
+        "Create a new object of type obj_type."
+        common_debug("new_object: %s:%s" % (obj_type, obj_id))
+        xml_obj_type = backtrans.get(obj_type)
+        v = cib_object_map.get(xml_obj_type)
+        if v is None:
+            return None
+        obj = v[1](xml_obj_type)
+        obj.obj_type = obj_type
+        obj.set_id(obj_id)
+        obj.node = None
+        obj.origin = "user"
+        return obj
+
+    def modified_elems(self):
+        return [x for x in self.cib_objects
+                if x.updated or x.origin == "user"]
+
+    def get_elems_on_type(self, spec):
+        if not spec.startswith("type:"):
+            return []
+        t = spec[5:]
+        return [x for x in self.cib_objects if x.obj_type == t]
+
+    def get_elems_on_tag(self, spec):
+        if not spec.startswith("tag:"):
+            return []
+        t = spec[4:]
+        matching_tags = [x for x in self.cib_objects if x.obj_type == 'tag' and x.obj_id == t]
+        ret = []
+        for mt in matching_tags:
+            matches = [cib_factory.find_object(o) for o in mt.node.xpath('./obj_ref/@id')]
+            ret += [m for m in matches if m is not None]
+        return ret
+
+    def mkobj_set(self, *args):
+        if not args:
+            return True, copy.copy(self.cib_objects)
+        if args[0] == "NOOBJ":
+            return True, []
+        rc = True
+        obj_set = oset([])
+        for spec in args:
+            if spec == "changed":
+                obj_set |= oset(self.modified_elems())
+            elif spec.startswith("type:"):
+                obj_set |= oset(self.get_elems_on_type(spec))
+            elif spec.startswith("tag:"):
+                obj_set |= oset(self.get_elems_on_tag(spec))
+            else:
+                objs = self.find_objects(spec) or []
+                for obj in objs:
+                    obj_set.add(obj)
+                if len(objs) == 0:
+                    no_object_err(spec)
+                    rc = False
+        return rc, obj_set
+
+    def get_all_obj_set(self):
+        return set(self.cib_objects)
+
+    def has_no_primitives(self):
+        return not self.get_elems_on_type("type:primitive")
+
+    def has_cib_changed(self):
+        if not self.is_cib_sane():
+            return False
+        return self.modified_elems() or self.remove_queue
+
+    def _verify_constraints(self, node):
+        '''
+        Check if all resources referenced in a constraint exist
+        '''
+        rc = True
+        constraint_id = node.get("id")
+        for obj_id in referenced_resources(node):
+            if not self.find_object(obj_id):
+                constraint_norefobj_err(constraint_id, obj_id)
+                rc = False
+        return rc
+
+    def _verify_rsc_children(self, obj):
+        '''
+        Check prerequisites:
+          a) all children must exist
+          b) no child may have more than one parent
+          c) there may not be duplicate children
+        '''
+        obj_id = obj.obj_id
+        rc = True
+        c_dict = {}
+        for c in obj.node.iterchildren():
+            if not is_cib_element(c):
+                continue
+            child_id = c.get("id")
+            if not self._verify_child(child_id, obj.node.tag, obj_id):
+                rc = False
+            if child_id in c_dict:
+                common_err("in group %s child %s listed more than once" %
+                           (obj_id, child_id))
+                rc = False
+            c_dict[child_id] = 1
+        for other in [x for x in self.cib_objects
+                      if x != obj and is_container(x.node)]:
+            shared_obj = set(obj.children) & set(other.children)
+            if shared_obj:
+                common_err("%s contained in both %s and %s" %
+                           (','.join([x.obj_id for x in shared_obj]),
+                            obj_id, other.obj_id))
+                rc = False
+        return rc
+
+    def _verify_child(self, child_id, parent_tag, obj_id):
+        'Check if child exists and obj_id is (or may become) its parent.'
+        child = self.find_object(child_id)
+        if not child:
+            no_object_err(child_id)
+            return False
+        if parent_tag == "group" and child.obj_type != "primitive":
+            common_err("a group may contain only primitives; %s is %s" %
+                       (child_id, child.obj_type))
+            return False
+        if child.parent and child.parent.obj_id != obj_id:
+            common_err("%s already in use at %s" % (child_id, child.parent.obj_id))
+            return False
+        if not child.obj_type in constants.children_tags:
+            common_err("%s may contain a primitive or a group; %s is %s" %
+                       (parent_tag, child_id, child.obj_type))
+            return False
+        return True
+
+    def _verify_element(self, obj):
+        '''
+        Can we create this object given its CLI representation.
+        This is not about syntax, we're past that, but about
+        semantics.
+        Right now we check if the children, if any, are fit for
+        the parent. And if this is a constraint, if all
+        referenced resources are present.
+        '''
+        rc = True
+        node = obj.node
+        obj_id = obj.obj_id
+        try:
+            cib_object_map[node.tag][0]
+        except KeyError:
+            common_err("element %s (%s) not recognized" % (node.tag, obj_id))
+            return False
+        if is_container(node):
+            rc &= self._verify_rsc_children(obj)
+        elif is_constraint(node):
+            rc &= self._verify_constraints(node)
+        return rc
+
+    def create_object(self, *args):
+        if not self.is_cib_sane():
+            return False
+        return self.create_from_cli(list(args)) is not None
+
+    def set_property_cli(self, obj_type, node):
+        pset_id = node.get('id') or default_id_for_obj(obj_type)
+        obj = self.find_object(pset_id)
+        if not obj:
+            if not is_id_valid(pset_id):
+                invalid_id_err(pset_id)
+                return None
+            obj = self.new_object(obj_type, pset_id)
+            if not obj:
+                return None
+            topnode = get_topnode(self.cib_elem, obj.parent_type)
+            obj.node = etree.SubElement(topnode, node.tag)
+            obj.origin = "user"
+            obj.node.set('id', pset_id)
+            topnode.append(obj.node)
+            self.cib_objects.append(obj)
+        copy_nvpairs(obj.node, node)
+        obj.set_updated()
+        return obj
+
+    def add_op(self, node):
+        '''Add an op to a primitive.'''
+        # does the referenced primitive exist
+        rsc_id = node.get('rsc')
+        rsc_obj = self.find_object(rsc_id)
+        if not rsc_obj:
+            no_object_err(rsc_id)
+            return None
+        if rsc_obj.obj_type != "primitive":
+            common_err("%s is not a primitive" % rsc_id)
+            return None
+
+        # the given node is not postprocessed
+        node, obj_type, obj_id = postprocess_cli(node, id_hint=rsc_obj.obj_id)
+
+        del node.attrib['rsc']
+        return rsc_obj.add_operation(node)
+
+    def create_from_cli(self, cli):
+        'Create a new cib object from the cli representation.'
+        if not self.is_cib_sane():
+            common_debug("create_from_cli (%s): is_cib_sane() failed" % (cli))
+            return None
+        if isinstance(cli, basestring) or isinstance(cli, list):
+            elem, obj_type, obj_id = parse_cli_to_xml(cli)
+        else:
+            elem, obj_type, obj_id = postprocess_cli(cli)
+        if elem is None:
+            # FIXME: raise error?
+            common_debug("create_from_cli (%s): failed" % (cli))
+            return None
+        common_debug("create_from_cli: %s, %s, %s" % (etree.tostring(elem), obj_type, obj_id))
+        if obj_type in olist(constants.nvset_cli_names):
+            return self.set_property_cli(obj_type, elem)
+        if obj_type == "op":
+            return self.add_op(elem)
+        if obj_type == "node":
+            obj = self.find_object(obj_id)
+            # make an exception and allow updating nodes
+            if obj:
+                self.merge_from_cli(obj, elem)
+                return obj
+        obj = self.new_object(obj_type, obj_id)
+        if not obj:
+            return None
+        return self._add_element(obj, elem)
+
+    def update_from_cli(self, obj, node, method):
+        '''
+        Replace element from the cli intermediate.
+        If this is an update and the element is properties, then
+        the new properties should be merged with the old.
+        Otherwise, users may be surprised.
+        '''
+        if method == 'update' and obj.obj_type in constants.nvset_cli_names:
+            return self.merge_from_cli(obj, node)
+        return self.update_element(obj, node)
+
+    def update_from_node(self, obj, node):
+        'Update element from a doc node.'
+        idmgmt.replace_xml(obj.node, node)
+        return self.update_element(obj, node)
+
+    def update_element(self, obj, newnode):
+        'Update element from a doc node.'
+        if newnode is None:
+            return False
+        if not self.is_cib_sane():
+            idmgmt.replace_xml(newnode, obj.node)
+            return False
+        oldnode = obj.node
+        if xml_equals(oldnode, newnode):
+            if newnode.getparent() is not None:
+                newnode.getparent().remove(newnode)
+            return True  # the new and the old versions are equal
+        obj.node = newnode
+        common_debug("update CIB element: %s" % str(obj))
+        if oldnode.getparent() is not None:
+            oldnode.getparent().replace(oldnode, newnode)
+        obj.nocli = False  # try again after update
+        if not self._adjust_children(obj):
+            return False
+        if not obj.cli_use_validate():
+            common_debug("update_element: validation failed (%s, %s)" % (obj, etree.tostring(newnode)))
+            obj.nocli_warn = True
+            obj.nocli = True
+        obj.set_updated()
+        return True
+
+    def merge_from_cli(self, obj, node):
+        common_debug("merge_from_cli: %s %s" % (obj.obj_type, etree.tostring(node)))
+        if obj.obj_type in constants.nvset_cli_names:
+            rc = merge_attributes(obj.node, node, "nvpair")
+        else:
+            rc = merge_nodes(obj.node, node)
+        if rc:
+            obj.set_updated()
+        return True
+
+    def _cli_set_update(self, edit_d, mk_set, upd_set, del_set, method):
+        '''
+        Create/update/remove elements.
+        edit_d is a dict with id keys and parsed xml values.
+        mk_set is a set of ids to be created.
+        upd_set is a set of ids to be updated (replaced).
+        del_set is a set to be removed.
+        method is either replace or update.
+        '''
+        common_debug("_cli_set_update: %s, %s, %s" % (mk_set, upd_set, del_set))
+        test_l = []
+
+        def obj_is_container(x):
+            obj = self.find_object(x)
+            return obj and is_container(obj.node)
+
+        del_containers = [x for x in del_set if obj_is_container(x)]
+        del_objs = [x for x in del_set if not obj_is_container(x)]
+
+        # delete containers first in case objects are moved elsewhere
+        if not self.delete(*del_containers):
+            common_debug("delete %s failed" % (list(del_set)))
+            return False
+
+        for cli in processing_sort([edit_d[x] for x in mk_set]):
+            obj = self.create_from_cli(cli)
+            if not obj:
+                common_debug("create_from_cli '%s' failed" %
+                             (etree.tostring(cli, pretty_print=True)))
+                return False
+            test_l.append(obj)
+
+        for id in upd_set:
+            obj = self.find_object(id)
+            if not obj:
+                common_debug("%s not found!" % (id))
+                return False
+            node, _, _ = postprocess_cli(edit_d[id], oldnode=obj.node)
+            if node is None:
+                common_debug("postprocess_cli failed: %s" % (id))
+                return False
+            if not self.update_from_cli(obj, node, method):
+                common_debug("update_from_cli failed: %s, %s, %s" %
+                             (obj, etree.tostring(node), method))
+                return False
+            test_l.append(obj)
+        if not self.delete(*del_objs):
+            common_debug("delete %s failed" % (list(del_set)))
+            return False
+        rc = True
+        for obj in test_l:
+            if not self.test_element(obj):
+                common_debug("test_element failed for %s" % (obj))
+                rc = False
+        return rc & self.check_structure()
+
+    def _xml_set_update(self, edit_d, mk_set, upd_set, del_set):
+        '''
+        Create/update/remove elements.
+        node_l is a list of elementtree elements.
+        mk_set is a set of ids to be created.
+        upd_set is a set of ids to be updated (replaced).
+        del_set is a set to be removed.
+        '''
+        common_debug("_xml_set_update: %s, %s, %s" % (mk_set, upd_set, del_set))
+        test_l = []
+        for el in processing_sort([edit_d[x] for x in mk_set]):
+            obj = self.create_from_node(el)
+            if not obj:
+                return False
+            test_l.append(obj)
+        for id in upd_set:
+            obj = self.find_object(id)
+            if not obj:
+                return False
+            if not self.update_from_node(obj, edit_d[id]):
+                return False
+            test_l.append(obj)
+        if not self.delete(*list(del_set)):
+            return False
+        rc = True
+        for obj in test_l:
+            if not self.test_element(obj):
+                rc = False
+        return rc & self.check_structure()
+
+    def _set_update(self, edit_d, mk_set, upd_set, del_set, upd_type, method):
+        if upd_type == "xml":
+            return self._xml_set_update(edit_d, mk_set, upd_set, del_set)
+        else:
+            return self._cli_set_update(edit_d, mk_set, upd_set, del_set, method)
+
+    def set_update(self, edit_d, mk_set, upd_set, del_set, upd_type="cli", method='replace'):
+        '''
+        Just a wrapper for _set_update() to allow for a
+        rollback.
+        '''
+        self._push_state()
+        if not self._set_update(edit_d, mk_set, upd_set, del_set, upd_type, method):
+            if not self._pop_state():
+                raise RuntimeError("this should never happen!")
+            return False
+        self._drop_state()
+        return True
+
+    def _adjust_children(self, obj):
+        '''
+        All stuff children related: manage the nodes of children,
+        update the list of children for the parent, update
+        parents in the children.
+        '''
+        new_children_ids = get_rsc_children_ids(obj.node)
+        if not new_children_ids:
+            return True
+        old_children = [x for x in obj.children if x.parent == obj]
+        new_children = [self.find_object(x) for x in new_children_ids]
+        new_children = [c for c in new_children if c is not None]
+        obj.children = new_children
+        # relink orphans to top
+        for child in set(old_children) - set(obj.children):
+            common_debug("relink child %s to top" % str(child))
+            self._relink_child_to_top(child)
+        if not self._are_children_orphans(obj):
+            return False
+        return self._update_children(obj)
+
+    def _relink_child_to_top(self, obj):
+        'Relink a child to the top node.'
+        get_topnode(self.cib_elem, obj.parent_type).append(obj.node)
+        obj.parent = None
+
+    def _are_children_orphans(self, obj):
+        """
+        Check if we're adding a container containing objects
+        we've already added to a different container
+        """
+        for child in obj.children:
+            if not child.parent:
+                continue
+            if child.parent == obj or child.parent.obj_id == obj.obj_id:
+                continue
+            if child.parent.obj_type in constants.container_tags:
+                common_err("Cannot create %s: Child %s already in %s" % (obj, child, child.parent))
+                return False
+        return True
+
+    def _update_children(self, obj):
+        '''For composite objects: update all children nodes.
+        '''
+        # unlink all and find them in the new node
+        for child in obj.children:
+            oldnode = child.node
+            newnode = obj.find_child_in_node(child)
+            if newnode is None:
+                common_err("Child found in children list but not in node: %s, %s" % (obj, child))
+                return False
+            child.node = newnode
+            if child.children:  # and children of children
+                if not self._update_children(child):
+                    return False
+            rmnode(oldnode)
+            if child.parent:
+                child.parent.updated = True
+            child.parent = obj
+        return True
+
+    def test_element(self, obj):
+        if not obj.xml_obj_type in constants.defaults_tags:
+            if not self._verify_element(obj):
+                return False
+        if utils.is_check_always() and obj.check_sanity() > 1:
+            return False
+        return True
+
+    def _update_links(self, obj):
+        '''
+        Update the structure links for the object (obj.children,
+        obj.parent). Update also the XML, if necessary.
+        '''
+        obj.children = []
+        if obj.obj_type not in constants.container_tags:
+            return
+        for c in obj.node.iterchildren():
+            if is_child_rsc(c):
+                child = self.find_object_for_node(c)
+                if not child:
+                    missing_obj_err(c)
+                    continue
+                child.parent = obj
+                obj.children.append(child)
+                if c != child.node:
+                    rmnode(child.node)
+                    child.node = c
+
+    def _add_element(self, obj, node):
+        assert node is not None
+        obj.node = node
+        obj.set_id()
+        pnode = get_topnode(self.cib_elem, obj.parent_type)
+        common_debug("_add_element: append child %s to %s" % (obj.obj_id, pnode.tag))
+        if not self._adjust_children(obj):
+            return None
+        pnode.append(node)
+        self._redirect_children_constraints(obj)
+        if not obj.cli_use_validate():
+            self.nocli_warn = True
+            obj.nocli = True
+        self._update_links(obj)
+        obj.origin = "user"
+        self.cib_objects.append(obj)
+        return obj
+
+    def _add_children(self, obj_type, node):
+        """
+        Called from create_from_node
+        In case this is a clone/group/master create from XML,
+        and the child node(s) haven't been added as a separate objects.
+        """
+        if obj_type not in constants.container_tags:
+            return True
+
+        for c in node.iterchildren('primitive'):
+            pid = c.get('id')
+            child_obj = self.find_object(pid)
+            if child_obj is None:
+                child_obj = self.create_from_node(copy.deepcopy(c))
+                if not child_obj:
+                    return False
+        return True
+
+    def create_from_node(self, node):
+        'Create a new cib object from a document node.'
+        if node is None:
+            common_debug("create_from_node: got None")
+            return None
+        try:
+            obj_type = cib_object_map[node.tag][0]
+        except KeyError:
+            common_debug("create_from_node: keyerror (%s)" % (node.tag))
+            return None
+        if is_defaults(node):
+            node = get_rscop_defaults_meta_node(node)
+            if node is None:
+                common_debug("create_from_node: get_rscop_defaults_meta_node failed")
+                return None
+
+        if not self._add_children(obj_type, node):
+            return None
+
+        obj = self.new_object(obj_type, node.get("id"))
+        if not obj:
+            return None
+        return self._add_element(obj, node)
+
+    def _remove_obj(self, obj):
+        "Remove a cib object."
+        common_debug("remove object %s" % str(obj))
+        for child in obj.children:
+            # just relink, don't remove children
+            self._relink_child_to_top(child)
+        if obj.parent:  # remove obj from its parent, if any
+            obj.parent.children.remove(obj)
+        idmgmt.remove_xml(obj.node)
+        rmnode(obj.node)
+        self._add_to_remove_queue(obj)
+        self.cib_objects.remove(obj)
+        for c_obj in self.related_constraints(obj):
+            if is_simpleconstraint(c_obj.node) and obj.children:
+                # the first child inherits constraints
+                rename_rscref(c_obj, obj.obj_id, obj.children[0].obj_id)
+            deleted = False
+            if delete_rscref(c_obj, obj.obj_id):
+                deleted = True
+            if silly_constraint(c_obj.node, obj.obj_id):
+                # remove invalid constraints
+                self._remove_obj(c_obj)
+                if not self._no_constraint_rm_msg:
+                    err_buf.info("hanging %s deleted" % str(c_obj))
+            elif deleted:
+                err_buf.info("constraint %s updated" % str(c_obj))
+
+    def related_constraints(self, obj):
+        if not is_resource(obj.node):
+            return []
+        c_list = []
+        for obj2 in self.cib_objects:
+            if not is_constraint(obj2.node):
+                continue
+            if rsc_constraint(obj.obj_id, obj2.node):
+                c_list.append(obj2)
+        return c_list
+
+    def _redirect_children_constraints(self, obj):
+        '''
+        Redirect constraints to the new parent
+        '''
+        for child in obj.children:
+            for c_obj in self.related_constraints(child):
+                rename_rscref(c_obj, child.obj_id, obj.obj_id)
+        # drop useless constraints which may have been created above
+        for c_obj in self.related_constraints(obj):
+            if silly_constraint(c_obj.node, obj.obj_id):
+                self._no_constraint_rm_msg = True
+                self._remove_obj(c_obj)
+                self._no_constraint_rm_msg = False
+
+    def template_primitives(self, obj):
+        if not is_template(obj.node):
+            return []
+        c_list = []
+        for obj2 in self.cib_objects:
+            if not is_primitive(obj2.node):
+                continue
+            if obj2.node.get("template") == obj.obj_id:
+                c_list.append(obj2)
+        return c_list
+
+    def _check_running_primitives(self, prim_l):
+        rscstat = RscState()
+        for prim in prim_l:
+            if not rscstat.can_delete(prim.obj_id):
+                common_err("resource %s is running, can't delete it" % prim.obj_id)
+                return False
+        return True
+
+    def _add_to_remove_queue(self, obj):
+        if obj.origin == "cib":
+            self.remove_queue.append(obj)
+
+    def _delete_1(self, obj):
+        '''
+        Remove an object and its parent in case the object is the
+        only child.
+        '''
+        if obj.parent and len(obj.parent.children) == 1:
+            self._delete_1(obj.parent)
+        if obj in self.cib_objects:  # don't remove parents twice
+            self._remove_obj(obj)
+
+    def delete(self, *args):
+        'Delete a cib object.'
+        if not self.is_cib_sane():
+            return False
+        rc = True
+        l = []
+        rscstat = RscState()
+        for obj_id in args:
+            obj = self.find_object(obj_id)
+            if not obj:
+                # If --force is set:
+                # Unless something more serious goes wrong here,
+                # don't return an error code if the object
+                # to remove doesn't exist. This should help scripted
+                # workflows without compromising an interactive
+                # use.
+                if not config.core.force:
+                    no_object_err(obj_id)
+                    rc = False
+                continue
+            if not rscstat.can_delete(obj_id):
+                common_err("resource %s is running, can't delete it" % obj_id)
+                rc = False
+                continue
+            if is_template(obj.node):
+                prim_l = self.template_primitives(obj)
+                prim_l = [x for x in prim_l
+                          if x not in l and x.obj_id not in args]
+                if not self._check_running_primitives(prim_l):
+                    rc = False
+                    continue
+                for prim in prim_l:
+                    common_info("hanging %s deleted" % str(prim))
+                    l.append(prim)
+            l.append(obj)
+        if l:
+            l = processing_sort_cli(l)
+            for obj in reversed(l):
+                self._delete_1(obj)
+        return rc
+
+    def rename(self, old_id, new_id):
+        '''
+        Rename a cib object.
+        - check if the resource (if it's a resource) is stopped
+        - check if the new id is not taken
+        - find the object with old id
+        - rename old id to new id in all related objects
+          (constraints)
+        - if the object came from the CIB, then it must be
+          deleted and the one with the new name created
+        - rename old id to new id in the object
+        '''
+        if not self.is_cib_sane() or not new_id:
+            return False
+        if idmgmt.id_in_use(new_id):
+            return False
+        obj = self.find_object(old_id)
+        if not obj:
+            no_object_err(old_id)
+            return False
+        if not obj.can_be_renamed():
+            return False
+        for c_obj in self.related_constraints(obj):
+            rename_rscref(c_obj, old_id, new_id)
+        rename_id(obj.node, old_id, new_id)
+        obj.obj_id = new_id
+        idmgmt.rename(old_id, new_id)
+        obj.set_updated()
+
+    def erase(self):
+        "Remove all cib objects."
+        # remove only bottom objects and no constraints
+        # the rest will automatically follow
+        if not self.is_cib_sane():
+            return False
+        erase_ok = True
+        l = []
+        rscstat = RscState()
+        for obj in [obj for obj in self.cib_objects
+                    if not obj.children and not is_constraint(obj.node)
+                    and obj.obj_type != "node"]:
+            if not rscstat.can_delete(obj.obj_id):
+                common_warn("resource %s is running, can't delete it" % obj.obj_id)
+                erase_ok = False
+            else:
+                l.append(obj)
+        if not erase_ok:
+            common_err("CIB erase aborted (nothing was deleted)")
+            return False
+        self._no_constraint_rm_msg = True
+        for obj in l:
+            self.delete(obj.obj_id)
+        self._no_constraint_rm_msg = False
+        remaining = 0
+        for obj in self.cib_objects:
+            if obj.obj_type != "node":
+                remaining += 1
+        if remaining > 0:
+            common_err("strange, but these objects remained:")
+            for obj in self.cib_objects:
+                if obj.obj_type != "node":
+                    print >> sys.stderr, str(obj)
+            self.cib_objects = []
+        return True
+
+    def erase_nodes(self):
+        "Remove nodes only."
+        if not self.is_cib_sane():
+            return False
+        l = [obj for obj in self.cib_objects if obj.obj_type == "node"]
+        for obj in l:
+            self.delete(obj.obj_id)
+
+    def refresh(self):
+        "Refresh from the CIB."
+        self.reset()
+        self.initialize()
+        return self.is_cib_sane()
+
+
+cib_factory = CibFactory()
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/cibstatus.py b/modules/cibstatus.py
new file mode 100644
index 0000000..f857a72
--- /dev/null
+++ b/modules/cibstatus.py
@@ -0,0 +1,403 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import os
+from lxml import etree
+import tmpfiles
+from tempfile import mkstemp
+from utils import ext_cmd, show_dot_graph, page_string
+from msg import common_err, common_info, common_warn
+import xmlutil
+import utils
+import config
+
+
+def get_tag_by_id(node, tag, id):
+    "Find a doc node which matches tag and id."
+    for n in node.xpath(".//%s" % tag):
+        if n.get("id") == id:
+            return n
+    return None
+
+
+def get_status_node_id(n):
+    try:
+        n = n.getparent()
+    except:
+        return None
+    if n.tag != "node_state":
+        return get_status_node_id(n)
+    return n.get("id")
+
+
+def get_status_node(status_node, node):
+    for n in status_node.iterchildren("node_state"):
+        if n.get("id") == node:
+            return n
+    return None
+
+
+def get_status_ops(status_node, rsc, op, interval, node=''):
+    '''
+    Find a doc node which matches the operation. interval set to
+    "-1" means to lookup an operation with non-zero interval (for
+    monitors). Empty interval means any interval is fine.
+    '''
+    l = []
+    for n in status_node.iterchildren("node_state"):
+        if node is not None and n.get("id") != node:
+            continue
+        for r in n.iterchildren("lrm_resource"):
+            if r.get("id") != rsc:
+                continue
+            for o in r.iterchildren("lrm_rsc_op"):
+                if o.get("operation") != op:
+                    continue
+                iv = o.get("interval")
+                if iv == interval or (interval == "-1" and iv != "0"):
+                    l.append(o)
+    return l
+
+
+def split_op(op):
+    if op == "probe":
+        return "monitor", "0"
+    elif op == "monitor":
+        return "monitor", "-1"
+    elif op[0:8] == "monitor:":
+        return "monitor", op[8:]
+    return op, "0"
+
+
+def cib_path(source):
+    return source[0:7] == "shadow:" and xmlutil.shadowfile(source[7:]) or source
+
+
+class CibStatus(object):
+    '''
+    CIB status management
+    '''
+    cmd_inject = "</dev/null >/dev/null 2>&1 crm_simulate -x %s -I %s"
+    cmd_run = "2>&1 crm_simulate -R -x %s"
+    cmd_simulate = "2>&1 crm_simulate -S -x %s"
+    node_ops = {
+        "online": "-u",
+        "offline": "-d",
+        "unclean": "-f",
+    }
+    ticket_ops = {
+        "grant": "-g",
+        "revoke": "-r",
+        "standby": "-b",
+        "activate": "-e",
+    }
+
+    def __init__(self):
+        self.origin = ""
+        self.backing_file = ""  # file to keep the live cib
+        self.status_node = None
+        self.cib = None
+        self.reset_state()
+
+    def _cib_path(self, source):
+        if source[0:7] == "shadow:":
+            return xmlutil.shadowfile(source[7:])
+        else:
+            return source
+
+    def _load_cib(self, source):
+        if source == "live":
+            if not self.backing_file:
+                self.backing_file = xmlutil.cibdump2tmp()
+                if not self.backing_file:
+                    return None
+                tmpfiles.add(self.backing_file)
+            else:
+                xmlutil.cibdump2file(self.backing_file)
+            f = self.backing_file
+        else:
+            f = cib_path(source)
+        return xmlutil.read_cib(xmlutil.file2cib_elem, f)
+
+    def _load(self, source):
+        cib = self._load_cib(source)
+        if cib is None:
+            return False
+        status = cib.find("status")
+        if status is None:
+            return False
+        self.cib = cib
+        self.status_node = status
+        self.reset_state()
+        return True
+
+    def reset_state(self):
+        self.modified = False
+        self.quorum = ''
+        self.node_changes = {}
+        self.op_changes = {}
+        self.ticket_changes = {}
+
+    def initialize(self):
+        src = utils.get_cib_in_use()
+        if not src:
+            src = "live"
+        else:
+            src = "shadow:" + src
+        if self._load(src):
+            self.origin = src
+
+    def source_file(self):
+        if self.origin == "live":
+            return self.backing_file
+        else:
+            return cib_path(self.origin)
+
+    def status_node_list(self):
+        st = self.get_status()
+        if st is None:
+            return
+        return [x.get("id") for x in st.xpath(".//node_state")]
+
+    def status_rsc_list(self):
+        st = self.get_status()
+        if st is None:
+            return
+        rsc_list = [x.get("id") for x in st.xpath(".//lrm_resource")]
+        # how to uniq?
+        d = {}
+        for e in rsc_list:
+            d[e] = 0
+        return d.keys()
+
+    def load(self, source):
+        '''
+        Load the status section from the given source. The source
+        may be cluster ("live"), shadow CIB, or CIB in a file.
+        '''
+        if self.backing_file:
+            os.unlink(self.backing_file)
+            self.backing_file = ""
+        if not self._load(source):
+            common_err("the cib contains no status")
+            return False
+        self.origin = source
+        return True
+
+    def save(self, dest=None):
+        '''
+        Save the modified status section to a file/shadow. If the
+        file exists, then it must be a cib file and the status
+        section is replaced with our status section. If the file
+        doesn't exist, then our section and some (?) configuration
+        is saved.
+        '''
+        if not self.modified:
+            common_info("apparently you didn't modify status")
+            return False
+        if (not dest and self.origin == "live") or dest == "live":
+            common_warn("cannot save status to the cluster")
+            return False
+        cib = self.cib
+        if dest:
+            dest_path = cib_path(dest)
+            if os.path.isfile(dest_path):
+                cib = self._load_cib(dest)
+                if cib is None:
+                    common_err("%s exists, but no cib inside" % dest)
+                    return False
+        else:
+            dest_path = cib_path(self.origin)
+        if cib != self.cib:
+            status = cib.find("status")
+            xmlutil.rmnode(status)
+            cib.append(self.status_node)
+        xml = etree.tostring(cib)
+        try:
+            f = open(dest_path, "w")
+        except IOError, msg:
+            common_err(msg)
+            return False
+        f.write(xml)
+        f.close()
+        return True
+
+    def _crm_simulate(self, cmd, nograph, scores, utilization, verbosity):
+        if not self.origin:
+            self.initialize()
+        if verbosity:
+            cmd = "%s -%s" % (cmd, verbosity.upper())
+        if scores:
+            cmd = "%s -s" % cmd
+        if utilization:
+            cmd = "%s -U" % cmd
+        if config.core.dotty and not nograph:
+            fd, dotfile = mkstemp()
+            cmd = "%s -D %s" % (cmd, dotfile)
+        else:
+            dotfile = None
+        rc = ext_cmd(cmd % self.source_file())
+        if dotfile:
+            show_dot_graph(dotfile)
+        return rc == 0
+
+    # actions is ignored
+    def run(self, nograph, scores, utilization, actions, verbosity):
+        return self._crm_simulate(self.cmd_run,
+                                  nograph, scores, utilization, verbosity)
+
+    # actions is ignored
+    def simulate(self, nograph, scores, utilization, actions, verbosity):
+        return self._crm_simulate(self.cmd_simulate,
+                                  nograph, scores, utilization, verbosity)
+
+    def get_status(self):
+        '''
+        Return the status section node.
+        '''
+        if not self.origin:
+            self.initialize()
+        if (self.status_node is None or
+            (self.origin == "live" and not self.modified)) \
+                and not self._load(self.origin):
+            return None
+        return self.status_node
+
+    def list_changes(self):
+        '''
+        Dump a set of changes done.
+        '''
+        if not self.modified:
+            return True
+        for node in self.node_changes:
+            print node, self.node_changes[node]
+        for op in self.op_changes:
+            print op, self.op_changes[op]
+        for ticket in self.ticket_changes:
+            print ticket, self.ticket_changes[ticket]
+        if self.quorum:
+            print "quorum:", self.quorum
+        return True
+
+    def show(self):
+        '''
+        Page the "pretty" XML of the status section.
+        '''
+        if self.get_status() is None:
+            return False
+        page_string(etree.tostring(self.status_node, pretty_print=True))
+        return True
+
+    def inject(self, opts):
+        return ext_cmd("%s %s" %
+                       (self.cmd_inject % (self.source_file(), self.source_file()), opts))
+
+    def set_quorum(self, v):
+        if not self.origin:
+            self.initialize()
+        rc = self.inject("--quorum=%s" % (v and "true" or "false"))
+        if rc != 0:
+            return False
+        self._load(self.origin)
+        self.quorum = v and "true" or "false"
+        self.modified = True
+        return True
+
+    def edit_node(self, node, state):
+        '''
+        Modify crmd, expected, and join attributes of node_state
+        to set the node's state to online, offline, or unclean.
+        '''
+        if self.get_status() is None:
+            return False
+        if state not in self.node_ops:
+            common_err("unknown state %s" % state)
+            return False
+        node_node = get_tag_by_id(self.status_node, "node_state", node)
+        if node_node is None:
+            common_info("node %s created" % node)
+            return False
+        rc = self.inject("%s %s" % (self.node_ops[state], node))
+        if rc != 0:
+            return False
+        self._load(self.origin)
+        self.node_changes[node] = state
+        self.modified = True
+        return True
+
+    def edit_ticket(self, ticket, subcmd):
+        '''
+        Modify ticket status.
+        '''
+        if self.get_status() is None:
+            return False
+        if subcmd not in self.ticket_ops:
+            common_err("unknown ticket command %s" % subcmd)
+            return False
+        rc = self.inject("%s %s" % (self.ticket_ops[subcmd], ticket))
+        if rc != 0:
+            return False
+        self._load(self.origin)
+        self.ticket_changes[ticket] = subcmd
+        self.modified = True
+        return True
+
+    def edit_op(self, op, rsc, rc_code, op_status, node=''):
+        '''
+        Set rc-code and op-status in the lrm_rsc_op status
+        section element.
+        '''
+        if self.get_status() is None:
+            return False
+        l_op, l_int = split_op(op)
+        op_nodes = get_status_ops(self.status_node, rsc, l_op, l_int, node)
+        if l_int == "-1" and len(op_nodes) != 1:
+            common_err("need interval for the monitor op")
+            return False
+        if node == '' and len(op_nodes) != 1:
+            if op_nodes:
+                nodelist = [get_status_node_id(x) for x in op_nodes]
+                common_err("operation %s found at %s" % (op, ' '.join(nodelist)))
+            else:
+                common_err("operation %s not found" % op)
+            return False
+        # either the op is fully specified (maybe not found)
+        # or we found exactly one op_node
+        if len(op_nodes) == 1:
+            op_node = op_nodes[0]
+            if not node:
+                node = get_status_node_id(op_node)
+            if not node:
+                common_err("node not found for the operation %s" % op)
+                return False
+            if l_int == "-1":
+                l_int = op_node.get("interval")
+        op_op = op_status == "0" and "-i" or "-F"
+        rc = self.inject("%s %s_%s_%s@%s=%s" %
+                         (op_op, rsc, l_op, l_int, node, rc_code))
+        if rc != 0:
+            return False
+        self.op_changes[node+":"+rsc+":"+op] = "rc="+rc_code
+        if op_status:
+            self.op_changes[node+":"+rsc+":"+op] += "," "op-status="+op_status
+        self._load(self.origin)
+        self.modified = True
+        return True
+
+cib_status = CibStatus()
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/cibverify.py b/modules/cibverify.py
new file mode 100644
index 0000000..9eb7f50
--- /dev/null
+++ b/modules/cibverify.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import re
+import utils
+from msg import err_buf
+
+
+cib_verify = "crm_verify --verbose -p"
+VALIDATE_RE = re.compile(r"^Entity: line (\d)+: element (\w+): " +
+                         r"Relax-NG validity error : (.+)$")
+
+
+def _prettify(line, indent=0):
+    m = VALIDATE_RE.match(line)
+    if m:
+        return "%s%s (%s): %s" % (indent*' ', m.group(2), m.group(1), m.group(3))
+    return line
+
+
+def verify(cib):
+    rc, _, stderr = utils.get_stdout_stderr(cib_verify, cib)
+    for i, line in enumerate(line for line in stderr.split('\n') if line):
+        line = _prettify(line, 0 if i == 0 else 7)
+        if i == 0:
+            err_buf.error(line)
+        else:
+            err_buf.writemsg(line)
+    return rc
diff --git a/modules/clidisplay.py b/modules/clidisplay.py
new file mode 100644
index 0000000..9e812e6
--- /dev/null
+++ b/modules/clidisplay.py
@@ -0,0 +1,130 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+"""
+Display output for various syntax elements.
+"""
+
+import config
+
+
+# Enable colors/upcasing
+_pretty = True
+
+
+def enable_pretty():
+    global _pretty
+    _pretty = True
+
+
+def disable_pretty():
+    global _pretty
+    _pretty = False
+
+
+def colors_enabled():
+    return 'color' in config.color.style and _pretty
+
+
+def _colorize(s, colors):
+    if colors_enabled():
+        return ''.join(('${%s}' % clr.upper()) for clr in colors) + s + '${NORMAL}'
+    return s
+
+
+def error(s):
+    return _colorize(s, config.color.error)
+
+
+def ok(s):
+    return _colorize(s, config.color.ok)
+
+
+def info(s):
+    return _colorize(s, config.color.info)
+
+
+def warn(s):
+    return _colorize(s, config.color.warn)
+
+
+def keyword(s):
+    if "uppercase" in config.color.style:
+        s = s.upper()
+    if "color" in config.color.style:
+        s = _colorize(s, config.color.keyword)
+    return s
+
+
+def prompt(s):
+    if colors_enabled():
+        s = "${RLIGNOREBEGIN}${GREEN}${BOLD}${RLIGNOREEND}" + s
+        return s + "${RLIGNOREBEGIN}${NORMAL}${RLIGNOREEND}"
+    return s
+
+
+def prompt_noreadline(s):
+    if colors_enabled():
+        return "${GREEN}${BOLD}" + s + "${NORMAL}"
+    return s
+
+
+def help_header(s):
+    return _colorize(s, config.color.help_header)
+
+
+def help_keyword(s):
+    return _colorize(s, config.color.help_keyword)
+
+
+def help_topic(s):
+    return _colorize(s, config.color.help_topic)
+
+
+def help_block(s):
+    return _colorize(s, config.color.help_block)
+
+
+def id(s):
+    return _colorize(s, config.color.identifier)
+
+
+def attr_name(s):
+    return _colorize(s, config.color.attr_name)
+
+
+def attr_value(s):
+    return _colorize(s, config.color.attr_value)
+
+
+def rscref(s):
+    return _colorize(s, config.color.resource_reference)
+
+
+def idref(s):
+    return _colorize(s, config.color.id_reference)
+
+
+def score(s):
+    return _colorize(s, config.color.score)
+
+
+def ticket(s):
+    return _colorize(s, config.color.ticket)
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/cliformat.py b/modules/cliformat.py
new file mode 100644
index 0000000..12f13c5
--- /dev/null
+++ b/modules/cliformat.py
@@ -0,0 +1,415 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import constants
+import clidisplay
+import utils
+import xmlutil
+
+
+#
+# CLI format generation utilities (from XML)
+#
+def cli_format(pl, break_lines=True, xml=False):
+    if break_lines and xml:
+        return ' \\\n'.join(pl)
+    elif break_lines:
+        return ' \\\n\t'.join(pl)
+    else:
+        return ' '.join(pl)
+
+
+def head_id_format(nodeid):
+    "Special format for property list / node id"
+    if utils.noquotes(nodeid):
+        return "%s:" % (clidisplay.id(nodeid))
+    return '%s="%s"' % (clidisplay.id('$id'),
+                        clidisplay.attr_value(nodeid))
+
+
+def quote_wrap(v):
+    if utils.noquotes(v):
+        return v
+    elif '"' in v:
+        return '"%s"' % v.replace('"', '\\"')
+    else:
+        return '"%s"' % v
+
+
+def nvpair_format(n, v):
+    if v is None:
+        return clidisplay.attr_name(n)
+    else:
+        return '='.join((clidisplay.attr_name(n),
+                         clidisplay.attr_value(quote_wrap(v))))
+
+
+def cli_operations(node, break_lines=True):
+    l = []
+    node_id = node.get("id")
+    s = ''
+    if node_id:
+        s = nvpair_format('$id', node_id)
+    idref = node.get("id-ref")
+    if idref:
+        s = '%s %s' % (s, nvpair_format('$id-ref', idref))
+    if s:
+        l.append("%s %s" % (clidisplay.keyword("operations"), s))
+    for c in node.iterchildren():
+        if c.tag == "op":
+            l.append(cli_op(c))
+    return cli_format(l, break_lines=break_lines)
+
+
+def cli_nvpair(nvp):
+    'Converts an nvpair tag or a (name, value) pair to CLI syntax'
+    from cibconfig import cib_factory
+    nodeid = nvp.get('id')
+    idref = nvp.get('id-ref')
+    name = nvp.get('name')
+    value = nvp.get('value')
+    if idref is not None:
+        if name is not None:
+            return '@%s:%s' % (idref, name)
+        return '@%s' % (idref)
+    elif nodeid is not None and cib_factory.is_id_refd(nvp.tag, nodeid):
+        return '$%s:%s' % (nodeid, nvpair_format(name, value))
+    return nvpair_format(name, value)
+
+
+def cli_nvpairs(nvplist):
+    'Return a string of name="value" pairs (passed in a list of nvpairs).'
+    return ' '.join([cli_nvpair(nvp) for nvp in nvplist])
+
+
+def nvpairs2list(node, add_id=False):
+    '''
+    Convert an attribute node to a list of nvpairs.
+    Also converts an id-ref or id into plain nvpairs.
+    The id attribute is normally skipped, since they tend to be
+    long and therefore obscure the relevant content. For some
+    elements, however, they are included (e.g. properties).
+    '''
+    import xmlbuilder
+
+    ret = []
+    if 'id-ref' in node:
+        ret.append(xmlbuilder.nvpair('$id-ref', node.get('id-ref')))
+    nvpairs = node.xpath('./nvpair | ./attributes/nvpair')
+    if 'id' in node and (add_id or len(nvpairs) == 0):
+        ret.append(xmlbuilder.nvpair('$id', node.get('id')))
+    ret.extend(nvpairs)
+    return ret
+
+
+def op_instattr(node):
+    """
+    Return nvpairs in <op><instance_attributes>...
+    """
+    pl = []
+    for c in node.xpath('./instance_attributes'):
+        pl.extend(nvpairs2list(c))
+    return pl
+
+
+def cli_op(node):
+    "CLI format for an <op> tag"
+    action, pl = xmlutil.op2list(node)
+    if not action:
+        return ""
+    ret = ["%s %s" % (clidisplay.keyword("op"), action)]
+    ret += [nvpair_format(n, v) for n, v in pl]
+    ret += [cli_nvpair(v) for v in op_instattr(node)]
+    return ' '.join(ret)
+
+
+def date_exp2cli(node):
+    kwmap = {'in_range': 'in', 'date_spec': 'spec'}
+    l = []
+    operation = node.get("operation")
+    l.append(clidisplay.keyword("date"))
+    l.append(clidisplay.keyword(kwmap.get(operation, operation)))
+    if operation in utils.olist(constants.simple_date_ops):
+        value = node.get(utils.keyword_cmp(operation, 'lt') and "end" or "start")
+        l.append(clidisplay.attr_value(quote_wrap(value)))
+    else:
+        if operation == 'in_range':
+            for name in constants.in_range_attrs:
+                if name in node.attrib:
+                    l.append(nvpair_format(name, node.attrib[name]))
+        for c in node.iterchildren():
+            if c.tag in ("duration", "date_spec"):
+                l.extend([nvpair_format(name, c.get(name))
+                          for name in c.keys() if name != 'id'])
+    return ' '.join(l)
+
+
+def binary_op_format(op):
+    l = op.split(':')
+    if len(l) == 2:
+        return "%s:%s" % (l[0], clidisplay.keyword(l[1]))
+    else:
+        return clidisplay.keyword(op)
+
+
+def exp2cli(node):
+    operation = node.get("operation")
+    type = node.get("type")
+    if type:
+        operation = "%s:%s" % (type, operation)
+    attribute = node.get("attribute")
+    value = node.get("value")
+    if not value:
+        return "%s %s" % (binary_op_format(operation), attribute)
+    else:
+        return "%s %s %s" % (attribute, binary_op_format(operation), value)
+
+
+def abs_pos_score(score):
+    return score in ("inf", "+inf", "Mandatory")
+
+
+def get_kind(node):
+    kind = node.get("kind")
+    if not kind:
+        kind = ""
+    return kind
+
+
+def get_score(node):
+    score = node.get("score")
+    if not score:
+        score = node.get("score-attribute")
+    else:
+        if score.find("INFINITY") >= 0:
+            score = score.replace("INFINITY", "inf")
+    if not score:
+        score = ""
+    return score
+
+
+def cli_rule_score(node):
+    score = node.get("score")
+    if score == "INFINITY":
+        return None
+    return get_score(node)
+
+
+def cli_exprs(node):
+    bool_op = node.get("boolean-op")
+    if not bool_op:
+        bool_op = "and"
+    exp = []
+    for c in node.iterchildren():
+        if c.tag == "date_expression":
+            exp.append(date_exp2cli(c))
+        elif c.tag == "expression":
+            exp.append(exp2cli(c))
+    return (" %s " % clidisplay.keyword(bool_op)).join(exp)
+
+
+def cli_rule(node):
+    from cibconfig import cib_factory
+    s = []
+    node_id = node.get("id")
+    if node_id and cib_factory.is_id_refd(node.tag, node_id):
+        s.append(nvpair_format('$id', node_id))
+    else:
+        idref = node.get("id-ref")
+        if idref:
+            return nvpair_format('$id-ref', idref)
+    rsc_role = node.get("role")
+    if rsc_role:
+        s.append(nvpair_format('$role', rsc_role))
+    score = cli_rule_score(node)
+    if score:
+        s.append("%s:" % (clidisplay.score(score)))
+    s.append(cli_exprs(node))
+    return ' '.join(s)
+
+
+def mkrscrole(node, n):
+    rsc = clidisplay.rscref(node.get(n))
+    rsc_role = node.get(n + "-role")
+    rsc_instance = node.get(n + "-instance")
+    if rsc_role:
+        return "%s:%s" % (rsc, rsc_role)
+    elif rsc_instance:
+        return "%s:%s" % (rsc, rsc_instance)
+    else:
+        return rsc
+
+
+def mkrscaction(node, n):
+    rsc = clidisplay.rscref(node.get(n))
+    rsc_action = node.get(n + "-action")
+    rsc_instance = node.get(n + "-instance")
+    if rsc_action:
+        return "%s:%s" % (rsc, rsc_action)
+    elif rsc_instance:
+        return "%s:%s" % (rsc, rsc_instance)
+    else:
+        return rsc
+
+
+def boolean_maybe(v):
+    "returns True/False or None"
+    if v is None:
+        return None
+    return utils.get_boolean(v)
+
+
+def rsc_set_constraint(node, obj_type):
+    col = []
+    cnt = 0
+    for n in node.findall("resource_set"):
+        sequential = boolean_maybe(n.get("sequential"))
+        require_all = boolean_maybe(n.get("require-all"))
+        if require_all is False:
+            col.append("[")
+        elif sequential is False:
+            col.append("(")
+        role = n.get("role")
+        action = n.get("action")
+        for r in n.findall("resource_ref"):
+            rsc = clidisplay.rscref(r.get("id"))
+            q = (obj_type == "order") and action or role
+            col.append(q and "%s:%s" % (rsc, q) or rsc)
+            cnt += 1
+        if require_all is False:
+            if sequential in (None, True):
+                col.append(nvpair_format('sequential', 'true'))
+            col.append("]")
+        elif sequential is False:
+            if require_all is False:
+                col.append(nvpair_format('require-all', 'false'))
+            col.append(")")
+    is_ticket = obj_type == 'rsc_ticket'
+    is_location = obj_type == 'location'
+    is_seq_all = sequential in (None, True) and require_all in (None, True)
+    if not is_location and ((is_seq_all and not is_ticket and cnt <= 2) or
+                            (is_ticket and cnt <= 1)):  # a degenerate thingie
+        col.insert(0, "_rsc_set_")
+    return col
+
+
+def simple_rsc_constraint(node, obj_type):
+    col = []
+    if obj_type == "colocation":
+        col.append(mkrscrole(node, "rsc"))
+        col.append(mkrscrole(node, "with-rsc"))
+    elif obj_type == "order":
+        col.append(mkrscaction(node, "first"))
+        col.append(mkrscaction(node, "then"))
+    else:  # rsc_ticket
+        col.append(mkrscrole(node, "rsc"))
+    return col
+
+
+# this pre (or post)-processing is oversimplified
+# but it will do for now
+# (a shortcut with more than one placeholder in a single expansion
+# cannot have more than one expansion)
+# ("...'@@'...'@@'...","...") <- that won't work
+def build_exp_re(exp_l):
+    return [x.replace(r'@@', r'([a-zA-Z_][a-zA-Z0-9_.-]*)') for x in exp_l]
+
+
+def match_acl_shortcut(xpath, re_l):
+    import re
+    for i in range(len(re_l)):
+        s = ''.join(re_l[0:i+1])
+        r = re.match(s + r"$", xpath)
+        if r:
+            return (True, r.groups()[0:i+1])
+    return (False, None)
+
+
+def find_acl_shortcut(xpath):
+    for shortcut in constants.acl_shortcuts:
+        l = build_exp_re(constants.acl_shortcuts[shortcut])
+        (ec, spec_l) = match_acl_shortcut(xpath, l)
+        if ec:
+            return (shortcut, spec_l)
+    return (None, None)
+
+
+def acl_spec_format(xml_spec, v):
+    key_f = clidisplay.keyword(constants.acl_spec_map[xml_spec])
+    if xml_spec == "xpath":
+        (shortcut, spec_l) = find_acl_shortcut(v)
+        if shortcut:
+            key_f = clidisplay.keyword(shortcut)
+            v_f = ':'.join([clidisplay.attr_value(x) for x in spec_l])
+        else:
+            v_f = clidisplay.attr_value(quote_wrap(v))
+    elif xml_spec == "ref":
+        v_f = '%s' % clidisplay.attr_value(v)
+    else:  # tag and attribute
+        v_f = '%s' % clidisplay.attr_value(v)
+    return v_f and '%s:%s' % (key_f, v_f) or key_f
+
+
+def cli_acl_rule(node, format=1):
+    l = []
+    acl_rule_name = node.tag
+    l.append(clidisplay.keyword(acl_rule_name))
+    for xml_spec in constants.acl_spec_map:
+        v = node.get(xml_spec)
+        if v:
+            l.append(acl_spec_format(xml_spec, v))
+    return ' '.join(l)
+
+
+def cli_acl_roleref(node, format=1):
+    return "%s:%s" % (clidisplay.keyword("role"),
+                      clidisplay.attr_value(node.get("id")))
+
+
+def cli_acl_role(node):
+    return clidisplay.attr_value(node.get("id"))
+
+
+def cli_acl_spec2_format(xml_spec, v):
+    key_f = clidisplay.keyword(xml_spec)
+    if xml_spec == "xpath":
+        (shortcut, spec_l) = find_acl_shortcut(v)
+        if shortcut:
+            key_f = clidisplay.keyword(shortcut)
+            v_f = ':'.join([clidisplay.attr_value(x) for x in spec_l])
+        else:
+            v_f = clidisplay.attr_value(quote_wrap(v))
+    else:  # ref, type and attr
+        v_f = clidisplay.attr_value(v)
+    return v_f and '%s:%s' % (key_f, v_f) or key_f
+
+
+def cli_acl_permission(node):
+    s = [clidisplay.keyword(node.get('kind'))]
+    #if node.get('id'):
+    #    s.append(head_id_format(node.get('id')))
+    if node.get('description'):
+        s.append(nvpair_format('description', node.get('description')))
+    for attrname, cliname in constants.acl_spec_map_2_rev:
+        if attrname in node.attrib:
+            s.append(cli_acl_spec2_format(cliname, node.get(attrname)))
+    return ' '.join(s)
+
+#
+################################################################
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/cmd_status.py b/modules/cmd_status.py
new file mode 100644
index 0000000..9edbea5
--- /dev/null
+++ b/modules/cmd_status.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import utils
+
+_crm_mon = None
+
+
+def crm_mon(opts=''):
+    """
+    Run 'crm_mon -1'
+    opts: Additional options to pass to crm_mon
+    returns: rc, stdout
+    """
+    global _crm_mon
+    if _crm_mon is None:
+        prog = utils.is_program("crm_mon")
+        if not prog:
+            raise IOError("crm_mon not available, check your installation")
+        _, out = utils.get_stdout("%s --help" % (prog))
+        if "--pending" in out:
+            _crm_mon = "%s -1 -j" % (prog)
+        else:
+            _crm_mon = "%s -1" % (prog)
+
+    status_cmd = "%s %s" % (_crm_mon, opts)
+    return utils.get_stdout(utils.add_sudo(status_cmd))
+
+
+def cmd_status(args):
+    '''
+    Calls crm_mon -1, passing optional extra arguments.
+    Displays the output, paging if necessary.
+    Raises IOError if crm_mon fails.
+    '''
+    opts = {
+        "bynode": "-n",
+        "inactive": "-r",
+        "ops": "-o",
+        "timing": "-t",
+        "failcounts": "-f",
+        "verbose": "-V",
+        "quiet": "-Q",
+        "html": "--as-html",
+        "xml": "--as-xml",
+        "simple": "-s",
+        "tickets": "-c",
+        "noheaders": "-D",
+        "detail": "-R",
+        "brief": "-b",
+    }
+    extra = ' '.join(opts.get(arg, arg) for arg in args)
+    rc, s = crm_mon(extra)
+    if rc != 0:
+        raise IOError("crm_mon (rc=%d): %s" % (rc, s))
+
+    utils.page_string(s)
+    return True
diff --git a/modules/command.py b/modules/command.py
new file mode 100644
index 0000000..dd722d0
--- /dev/null
+++ b/modules/command.py
@@ -0,0 +1,497 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+# - Base class for UI levels
+# - Decorators and other helper functions for the UI
+#   Mostly, what these functions do is store extra metadata
+#   inside the functions.
+
+import inspect
+import help as help_module
+import ui_utils
+from msg import common_debug
+
+
+def name(n):
+    '''
+    Overrides the name of the command.
+    This is useful to handle commands with
+    dashes instead of underlines, or commands
+    with awkward names (like commands with a
+    leading underscore).
+    '''
+    def inner(fn):
+        setattr(fn, '_name', n)
+        return fn
+    return inner
+
+
+def alias(*aliases):
+    '''
+    Adds aliases for the command. The command
+    will also be callable using the alias. The
+    command name set in the command context will
+    reflect the alias used (so the same command can
+    behave differently depending on the alias).
+    '''
+    def inner(fn):
+        setattr(fn, '_aliases', aliases)
+        return fn
+    return inner
+
+
+def level(level_class):
+    '''
+    Changes the command into a level movement.
+    Calling the command doesn't actually call the
+    member function this decorator is applied to, so
+    don't put any code in that function.
+
+    This is a bit awkward, but given how decorators work,
+    it's the best I could think of.
+    '''
+    def inner(fn):
+        # check signature of given level function
+        _check_args(fn, ('self',))
+
+        setattr(fn, '_ui_type', 'level')
+        setattr(fn, '_level', level_class)
+
+        def default(arg, val):
+            if not hasattr(fn, arg):
+                setattr(fn, arg, val)
+
+        default('_aliases', tuple())
+        default('_short_help', None)
+        default('_long_help', None)
+        return fn
+    return inner
+
+
+def help(doc):
+    '''
+    Use to set a help text for a command or level
+    which isn't documented in crm.8.txt.
+
+    The first line of the doc string will be used as
+    the short help, the rest will be used as the full
+    help message.
+    '''
+    doc_split = doc.split('\n', 1)
+
+    def inner(fn):
+        setattr(fn, '_short_help', doc_split[0])
+        if len(doc_split) > 1:
+            setattr(fn, '_long_help', doc_split[1])
+        else:
+            setattr(fn, '_long_help', '')
+        return fn
+    return inner
+
+
+def skill_level(new_level):
+    '''
+    Use to set the required skill level of a command:
+
+        @command
+        @skill_level('administrator')
+        def do_rmrf(self, cmd, args):
+            ...
+    '''
+    if isinstance(new_level, basestring):
+        levels = {'operator': 0, 'administrator': 1, 'expert': 2}
+        if new_level.lower() not in levels:
+            raise ValueError("Unknown skill level: " + new_level)
+        new_level = levels[new_level.lower()]
+
+    def inner(fn):
+        setattr(fn, '_skill_level', new_level)
+        return fn
+    return inner
+
+
+def wait(fn):
+    '''
+    A command with this decorator will
+    force the interactive shell to wait
+    for the command to complete.
+
+        @command
+        @wait
+        def do_bigop(self, cmd, args):
+            ...
+    '''
+    setattr(fn, '_wait', True)
+    return fn
+
+
+def completer(cb):
+    '''
+    Use to set a tab completer for the command.
+    The completer is called for the command, regardless
+    of the number of arguments called so far
+    '''
+    def inner(fn):
+        setattr(fn, '_completer', cb)
+        return fn
+    return inner
+
+
+def completers(*fns):
+    '''
+    Use to set a list of positional tab completers for the command.
+    Each completer gets as its argument the command line entered so far,
+    and returns a list of possible completions.
+    '''
+    def completer(args):
+        nargs = len(args) - 1
+        if nargs == 0:
+            return [args[0]]
+        if nargs <= len(fns):
+            return fns[nargs-1](args)
+        return []
+
+    def inner(fn):
+        setattr(fn, '_completer', completer)
+        return fn
+    return inner
+
+
+def completers_repeating(*fns):
+    '''
+    Like completers, but calls the last completer
+    for any additional arguments
+    '''
+    def completer(args):
+        nargs = len(args) - 1
+        if nargs == 0:
+            return [args[0]]
+        if nargs <= len(fns):
+            return fns[nargs-1](args)
+        return fns[-1](args)
+
+    def inner(fn):
+        setattr(fn, '_completer', completer)
+        return fn
+    return inner
+
+
+def _cd_completer(args, context):
+    'TODO: make better completion'
+    ret = []
+    if context.previous_level():
+        ret += ['..']
+    return ret + [l for l in context.current_level().get_completions()
+                  if context.current_level().is_sublevel(l)]
+
+
+def _help_completer(args, context):
+    'TODO: make better completion'
+    return help_module.list_help_topics() + context.current_level().get_completions()
+
+
+class UI(object):
+    '''
+    Base class for all ui levels.
+    Things that I need to solve:
+      - Error handling
+      - Help
+      - Completion
+    '''
+
+    # Name of level: override this in the subclass.
+    name = None
+
+    def requires(self):
+        '''
+        Returns False if requirements for level are
+        not met. Checked before entering the level.
+        '''
+        return True
+
+    def end_game(self, no_questions_asked=False):
+        '''
+        Overriding end_game() allows levels to ask
+        for confirmation before exiting.
+        '''
+        pass
+
+    def should_wait(self):
+        '''
+        A kludge to allow in-transit configuration changes to
+        make us wait on transition to finish. Needs to be
+        implemented in the level (currently, just configure).
+        '''
+        return False
+
+    @alias('end', 'back')
+    @help('''Go back to previous level
+Navigates back in the user interface.
+''')
+    def do_up(self, context):
+        '''
+        TODO: Implement full cd navigation. cd ../configure, for example
+        Also implement ls to list commands / levels from current location
+        '''
+        ok = context.up()
+        context.save_stack()
+        return ok
+
+    @help('''List levels and commands
+Lists the available sublevels and commands
+at the current level.
+''')
+    def do_ls(self, context):
+        '''
+        Shows list of places to go and commands to call
+        '''
+        out = []
+        if context.previous_level():
+            out = ['..']
+        out += context.current_level().get_completions()
+        for i, o in enumerate(out):
+            print '%-16s' % (o),
+            if ((i - 2) % 3) == 0:
+                print ''
+        print ''
+
+    @help('''Navigate the level structure
+This command works similar to how `cd` works in a regular unix
+system shell. `cd ..` returns to the previous level.
+
+If the current level is `resource`, executing `cd ../configure` will
+move directly to the `configure` level.
+
+One difference between this command and the usual behavior of `cd`
+is that without any argument, this command will go back one level
+instead of doing nothing.
+
+Examples:
+....
+        cd ..
+        cd configure
+        cd ../configure
+        cd configure/ra
+....
+''')
+    @completer(_cd_completer)
+    def do_cd(self, context, optarg='..'):
+        ok = True
+        path = optarg.split('/', 1)
+        if len(path) == 1:
+            path = path[0]
+            if path == '..':
+                ok = context.up()
+            elif path == '.' or not path:
+                return ok
+            else:
+                info = context.current_level().get_child(path)
+                if not info or not info.level:
+                    common_debug("children: %s" % (self._children))
+                    context.fatal_error("%s not found in %s" % (path, context.current_level()))
+                context.enter_level(info.level)
+        else:
+            if not self.do_cd(context, path[0]):
+                ok = False
+            if not self.do_cd(context, path[1]):
+                ok = False
+        context.save_stack()
+        return True
+
+    @alias('bye', 'exit')
+    @help('''Exit the interactive shell
+Terminates `crm` immediately. For some levels, `quit` may
+ask for confirmation before terminating, if there are
+uncommitted changes to the configuration.
+''')
+    def do_quit(self, context):
+        context.quit()
+
+    @alias('?', '-h', '--help')
+    @help('''show help (help topics for list of topics)
+The help subsystem consists of the command reference and a list
+of topics. The former is what you need in order to get the
+details regarding a specific command. The latter should help with
+concepts and examples.
+
+Examples:
+....
+        help Introduction
+        help quit
+....
+''')
+    @completer(_help_completer)
+    def do_help(self, context, subject=None, subtopic=None):
+        """usage: help topic|level|command"""
+        h = help_module.help_contextual(context.level_name(), subject, subtopic)
+        h.paginate()
+
+    def get_completions(self):
+        '''
+        return tab completions
+        '''
+        return self._children.keys()
+
+    def get_child(self, child):
+        '''
+        Returns child info for the given name, or None
+        if the child is not found.
+        '''
+        return self._children.get(child)
+
+    def is_sublevel(self, child):
+        '''
+        True if the given name is a sublevel of this level
+        '''
+        sub = self._children.get(child)
+        return sub and sub.type == 'level'
+
+    @classmethod
+    def init_ui(cls):
+        def get_if_command(attr):
+            "Return the named attribute if it's a command"
+            child = getattr(cls, attr)
+            return child if attr.startswith('do_') and inspect.ismethod(child) else None
+
+        def add_aliases(children, info):
+            "Add any aliases for command to child map"
+            for alias in info.aliases:
+                children[alias] = info
+
+        def add_help(info):
+            "Add static help to the help system"
+            if info.short_help:
+                entry = help_module.HelpEntry(info.short_help, info.long_help, generated=True)
+            elif info.type == 'command':
+                entry = help_module.HelpEntry(
+                    'Help for command ' + info.name,
+                    'Note: This command is not documented.\n' +
+                    'Usage: %s %s' % (info.name,
+                                      ui_utils.pretty_arguments(info.function, nskip=2)),
+                    generated=True)
+            elif info.type == 'level':
+                entry = help_module.HelpEntry('Help for level ' + info.name,
+                                              'Note: This level is not documented.\n',
+                                              generated=True)
+            if info.type == 'command':
+                help_module.add_help(entry, level=cls.name, command=info.name)
+            elif info.type == 'level':
+                help_module.add_help(entry, level=info.name)
+
+        def prepare(children, child):
+            info = ChildInfo(child, cls)
+            if info.type == 'command' and not is_valid_command_function(info.function):
+                raise ValueError("Invalid command function: %s.%s" %
+                                 (cls.__name__, info.function.__name__))
+            children[info.name] = info
+            add_aliases(children, info)
+            add_help(info)
+
+        children = {}
+        for child_name in dir(cls):
+            child = get_if_command(child_name)
+            if child:
+                prepare(children, child)
+        setattr(cls, '_children', children)
+        return children
+
+
+def make_name(new_name):
+    '''
+    Generate command name from command function name.
+    '''
+    if new_name.startswith('do_'):
+        return new_name[3:]
+    return new_name
+
+
+class ChildInfo(object):
+    '''
+    Declares the given method a command method.
+    Sets extra attributes in the function itself,
+    which are picked up by the UILevel class and used
+    to generate ChildInfo data.
+
+    The given method is expected to take a first parameter
+    (after self) which is a UI context, which holds information
+    about where the user came from when calling the command, controls
+    for manipulating the current level (up(), quit(), etc),
+    the name used when calling the command, error reporting and warning
+    methods.
+
+    The rest of the parameters are the actual arguments to the method. These
+    are tokenized using shlex and then matched to the actual arguments of the
+    method.
+
+    Information about a child node in the hierarchy:
+    A node is either a level or a command.
+    '''
+    def __init__(self, fn, parent):
+        def maybe(attr, default):
+            if hasattr(fn, attr):
+                return getattr(fn, attr)
+            return default
+
+        self.function = fn
+        self.name = maybe('_name', make_name(fn.__name__))
+        self.type = maybe('_ui_type', 'command')
+        self.aliases = maybe('_aliases', tuple())
+        self.short_help = maybe('_short_help', None)
+        self.long_help = maybe('_long_help', None)
+        self.skill_level = maybe('_skill_level', 0)
+        self.wait = maybe('_wait', False)
+        self.level = maybe('_level', None)
+        self.completer = maybe('_completer', None)
+        self.parent = parent
+        self.children = {}
+        if self.type == 'level' and self.level:
+            self.children = self.level.init_ui()
+
+    def complete(self, context, args):
+        '''
+        Execute the completer for this command with the given arguments.
+        The completer mostly completes based on argument position, but
+        some commands are context sensitive...
+        - make args[0] be name of command
+        '''
+        ret = []
+        if self.completer is not None:
+            specs = inspect.getargspec(self.completer)
+            if 'context' in specs.args:
+                ret = self.completer([self.name] + args, context)
+            else:
+                ret = self.completer([self.name] + args)
+        return ret
+
+    def __repr__(self):
+        return "%s:%s (%s)" % (self.type, self.name, self.short_help)
+
+
+def is_valid_command_function(fn):
+    '''
+    Returns True if fn is a valid command function:
+    named do_xxx, takes (self, context) as the first two parameters
+    '''
+    specs = inspect.getargspec(fn)
+    return len(specs.args) >= 2 and specs.args[0] == 'self' and specs.args[1] == 'context'
+
+
+def _check_args(fn, expected):
+    argnames = fn.func_code.co_varnames[:fn.func_code.co_argcount]
+    if argnames != expected:
+        raise ValueError(fn.__name__ +
+                         ": Expected method with signature " + repr(expected))
diff --git a/modules/completers.py b/modules/completers.py
new file mode 100644
index 0000000..bcd5ab9
--- /dev/null
+++ b/modules/completers.py
@@ -0,0 +1,78 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+# Helper completers
+
+import xmlutil
+
+
+def choice(lst):
+    '''
+    Static completion from a list
+    '''
+    def completer(args):
+        return lst
+    return completer
+
+
+null = choice([])
+
+
+def call(fn, *fnargs):
+    '''
+    Call the given function with the given arguments.
+    The function has to return a list of completions.
+    '''
+    def completer(args):
+        return fn(*fnargs)
+    return completer
+
+
+def join(*fns):
+    '''
+    Combine the output of several completers
+    into a single completer.
+    '''
+    def completer(args):
+        ret = []
+        for fn in fns:
+            ret += fn(args)
+        return ret
+    return completer
+
+
+booleans = choice(['yes', 'no', 'true', 'false', 'on', 'off'])
+
+
+def resources(args):
+    cib_el = xmlutil.resources_xml()
+    if cib_el is None:
+        return []
+    nodes = xmlutil.get_interesting_nodes(cib_el, [])
+    return [x.get("id") for x in nodes if xmlutil.is_resource(x)]
+
+
+def primitives(args):
+    cib_el = xmlutil.resources_xml()
+    if cib_el is None:
+        return []
+    nodes = xmlutil.get_interesting_nodes(cib_el, [])
+    return [x.get("id") for x in nodes if xmlutil.is_primitive(x)]
+
+nodes = call(xmlutil.listnodes)
+
+shadows = call(xmlutil.listshadows)
diff --git a/modules/config.py b/modules/config.py
new file mode 100644
index 0000000..4722ccd
--- /dev/null
+++ b/modules/config.py
@@ -0,0 +1,411 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+'''
+Holds user-configurable options.
+'''
+
+import os
+import re
+import ConfigParser
+import userdir
+
+
+_SYSTEMWIDE = '/etc/crm/crm.conf'
+_PERUSER = os.getenv("CRM_CONFIG_FILE") or os.path.join(userdir.CONFIG_HOME, 'crm.conf')
+
+
+# opt_ classes
+# members: default, completions, validate()
+
+class opt_program(object):
+    def __init__(self, envvar, proglist):
+        self.default = ''
+        if envvar and os.getenv(envvar):
+            self.default = os.getenv(envvar)
+        else:
+            for prog in proglist:
+                if self._is_program(prog):
+                    self.default = prog
+                    break
+        self.completions = proglist
+
+    def _is_program(self, prog):
+        """Is this program available?"""
+        for p in os.getenv("PATH").split(os.pathsep):
+            filename = os.path.join(p, prog)
+            if os.path.isfile(filename) and os.access(filename, os.X_OK):
+                return True
+        return False
+
+    def validate(self, prog):
+        if not self._is_program(prog):
+            raise ValueError("%s does not exist or is not a program" % prog)
+
+    def get(self, value):
+        if value.startswith('$'):
+            return os.getenv(value[1:])
+        elif value.startswith('\\$'):
+            return value[1:]
+        return value
+
+
+class opt_string(object):
+    def __init__(self, value):
+        self.default = value
+        self.completions = ()
+
+    def validate(self, val):
+        return True
+
+    def get(self, value):
+        return value
+
+
+class opt_choice(object):
+    def __init__(self, dflt, choices):
+        self.default = dflt
+        self.completions = choices
+
+    def validate(self, val):
+        if val not in self.completions:
+            raise ValueError("%s not in %s" % (val, ', '.join(self.completions)))
+
+    def get(self, value):
+        return value
+
+
+class opt_multichoice(object):
+    def __init__(self, dflt, choices):
+        self.default = dflt
+        self.completions = choices
+
+    def validate(self, val):
+        vals = [x.strip() for x in val.split(',')]
+        for otype in vals:
+            if not otype in self.completions:
+                raise ValueError("%s not in %s" % (val, ', '.join(self.completions)))
+
+    def get(self, value):
+        return value
+
+
+class opt_boolean(object):
+    def __init__(self, dflt):
+        self.default = dflt
+        self.completions = ('yes', 'true', 'on', '1', 'no', 'false', 'off', '0')
+
+    def validate(self, val):
+        if val is True:
+            val = 'true'
+        elif val is False:
+            val = 'false'
+        val = val.lower()
+        if val not in self.completions:
+            raise ValueError("Not a boolean: %s (try one of: %s)" % (
+                val, ', '.join(self.completions)))
+
+    def get(self, value):
+        return value.lower() in ('yes', 'true', 'on', '1')
+
+
+class opt_dir(object):
+    opts = {
+        'datadir': ('/usr/share', '/usr/local/share', '/opt'),
+        'cachedir': ('/var/cache', '/opt/cache'),
+        'libdir': ('/usr/lib64', '/usr/libexec', '/usr/lib',
+                   '/usr/local/lib64', '/usr/local/libexec', '/usr/local/lib'),
+        'varlib': ('/var/lib', '/opt/var/lib')
+    }
+
+    def __init__(self, path):
+        self.default = ''
+        self.completions = []
+        m = re.match(r'\%\(([^\)]+)\)s(.+)', path)
+        if m:
+            t = m.group(1)
+            for dd in self.opts[t]:
+                if os.path.isdir(path % {t: dd}):
+                    self.default = path % {t: dd}
+                    break
+            else:
+                self.default = path % {t: self.opts[t][0]}
+        else:
+            self.default = path
+
+    def validate(self, val):
+        if not os.path.isdir(val):
+            raise ValueError("Directory not found: " % (val))
+
+    def get(self, value):
+        return value
+
+
+class opt_color(object):
+    def __init__(self, val):
+        self.default = val
+        self.completions = ('black', 'blue', 'green', 'cyan',
+                            'red', 'magenta', 'yellow', 'white',
+                            'bold', 'blink', 'dim', 'reverse',
+                            'underline', 'normal')
+
+    def validate(self, val):
+        for v in val.split(' '):
+            if v not in self.completions:
+                raise ValueError('Invalid color ' + val)
+
+    def get(self, value):
+        return [s.rstrip(',') for s in value.split(' ')] or ['normal']
+
+
+DEFAULTS = {
+    'core': {
+        'editor': opt_program('EDITOR', ('vim', 'vi', 'emacs', 'nano')),
+        'pager': opt_program('PAGER', ('less', 'more', 'pg')),
+        'user': opt_string(''),
+        'skill_level': opt_choice('expert', ('operator', 'administrator', 'expert')),
+        'sort_elements': opt_boolean('yes'),
+        'check_frequency': opt_choice('always', ('always', 'on-verify', 'never')),
+        'check_mode': opt_choice('strict', ('strict', 'relaxed')),
+        'wait': opt_boolean('no'),
+        'add_quotes': opt_boolean('yes'),
+        'manage_children': opt_choice('ask', ('ask', 'never', 'always')),
+        'force': opt_boolean('no'),
+        'debug': opt_boolean('no'),
+        'ptest': opt_program('', ('ptest', 'crm_simulate')),
+        'dotty': opt_program('', ('dotty',)),
+        'dot': opt_program('', ('dot',)),
+        'ignore_missing_metadata': opt_boolean('no'),
+    },
+    'path': {
+        'sharedir': opt_dir('%(datadir)s/crmsh'),
+        'cache': opt_dir('%(cachedir)s/crm'),
+        'crm_config': opt_dir('%(varlib)s/pacemaker/cib'),
+        'crm_daemon_dir': opt_dir('%(libdir)s/pacemaker'),
+        'crm_daemon_user': opt_string('hacluster'),
+        'ocf_root': opt_dir('%(libdir)s/ocf'),
+        'crm_dtd_dir': opt_dir('%(datadir)s/pacemaker'),
+        'pe_state_dir': opt_dir('%(varlib)s/pacemaker/pengine'),
+        'heartbeat_dir': opt_dir('%(varlib)s/heartbeat'),
+        'hb_delnode': opt_program('', ('%(datadir)s/heartbeat/hb_delnode',)),
+        'nagios_plugins': opt_dir('%(libdir)s/nagios/plugins')
+    },
+    'color': {
+        'style': opt_multichoice('color', ('plain', 'color', 'uppercase')),
+        'error': opt_color('red bold'),
+        'ok': opt_color('green bold'),
+        'warn': opt_color('yellow bold'),
+        'info': opt_color('cyan'),
+        'help_keyword': opt_color('blue bold underline'),
+        'help_header': opt_color('normal bold'),
+        'help_topic': opt_color('yellow bold'),
+        'help_block': opt_color('cyan'),
+        'keyword': opt_color('yellow'),
+        'identifier': opt_color('normal'),
+        'attr_name': opt_color('cyan'),
+        'attr_value': opt_color('red'),
+        'resource_reference': opt_color('green'),
+        'id_reference': opt_color('green'),
+        'score': opt_color('magenta'),
+        'ticket': opt_color('magenta'),
+    }
+}
+
+_parser = None
+
+
+def _stringify(val):
+    if val is True:
+        return 'true'
+    elif val is False:
+        return 'false'
+    elif isinstance(val, basestring):
+        return val
+    else:
+        return str(val)
+
+
+class _Configuration(object):
+    def __init__(self):
+        self._defaults = None
+        self._systemwide = None
+        self._user = None
+
+    def load(self):
+        self._defaults = ConfigParser.SafeConfigParser()
+        for section, keys in DEFAULTS.iteritems():
+            self._defaults.add_section(section)
+            for key, opt in keys.iteritems():
+                self._defaults.set(section, key, opt.default)
+
+        if os.path.isfile(_SYSTEMWIDE):
+            self._systemwide = ConfigParser.SafeConfigParser()
+            self._systemwide.read([_SYSTEMWIDE])
+        # for backwards compatibility with <=2.1.1 due to ridiculous bug
+        elif os.path.isfile("/etc/crm/crmsh.conf"):
+            self._systemwide = ConfigParser.SafeConfigParser()
+            self._systemwide.read(["/etc/crm/crmsh.conf"])
+        if os.path.isfile(_PERUSER):
+            self._user = ConfigParser.SafeConfigParser()
+            self._user.read([_PERUSER])
+
+    def save(self):
+        if self._user:
+            if not os.path.isdir(os.path.dirname(_PERUSER)):
+                os.makedirs(os.path.dirname(_PERUSER))
+            fp = open(_PERUSER, 'w')
+            self._user.write(fp)
+            fp.close()
+
+    def get_impl(self, section, name):
+        if self._user and self._user.has_option(section, name):
+            return self._user.get(section, name) or ''
+        if self._systemwide and self._systemwide.has_option(section, name):
+            return self._systemwide.get(section, name) or ''
+        return self._defaults.get(section, name) or ''
+
+    def get(self, section, name, raw=False):
+        if raw:
+            return self.get_impl(section, name)
+        return DEFAULTS[section][name].get(self.get_impl(section, name))
+
+    def set(self, section, name, value):
+        if section not in ('core', 'path', 'color'):
+            raise ValueError("Setting invalid section " + str(section))
+        if not self._defaults.has_option(section, name):
+            raise ValueError("Setting invalid option %s.%s" % (section, name))
+        DEFAULTS[section][name].validate(value)
+        if self._user is None:
+            self._user = ConfigParser.SafeConfigParser()
+        if not self._user.has_section(section):
+            self._user.add_section(section)
+        self._user.set(section, name, _stringify(value))
+
+    def items(self, section):
+        return [(k, self.get(section, k)) for k, _ in self._defaults.items(section)]
+
+    def configured_keys(self, section):
+        ret = []
+        if self._systemwide and self._systemwide.has_section(section):
+            ret += self._systemwide.options(section)
+        if self._user and self._user.has_section(section):
+            ret += self._user.options(section)
+        return list(set(ret))
+
+    def reset(self):
+        '''reset to what is on disk'''
+        self._user = ConfigParser.SafeConfigParser()
+        self._user.read([_PERUSER])
+
+
+_configuration = _Configuration()
+
+
+class _Section(object):
+    def __init__(self, section):
+        object.__setattr__(self, 'section', section)
+
+    def __getattr__(self, name):
+        return _configuration.get(self.section, name)
+
+    def __setattr__(self, name, value):
+        _configuration.set(self.section, name, value)
+
+    def items(self):
+        return _configuration.items(self.section)
+
+
+def load():
+    _configuration.load()
+
+    os.environ["OCF_ROOT"] = _configuration.get('path', 'ocf_root')
+
+
+def save():
+    '''
+    Only save options that are not default
+    '''
+    _configuration.save()
+
+
+def set_option(section, option, value):
+    _configuration.set(section, option, value)
+
+
+def get_option(section, option, raw=False):
+    '''
+    Return the given option.
+    If raw is True, return the configured value.
+    Example: for a boolean, returns "yes", not True
+    '''
+    return _configuration.get(section, option, raw=raw)
+
+
+def get_all_options():
+    '''Returns a list of all configurable options'''
+    ret = []
+    for sname, section in DEFAULTS.iteritems():
+        ret += ['%s.%s' % (sname, option) for option in section.keys()]
+    return sorted(ret)
+
+
+def get_configured_options():
+    '''Returns a list of all options that have a non-default value'''
+    ret = []
+    for sname in DEFAULTS.keys():
+        for key in _configuration.configured_keys(sname):
+            ret.append('%s.%s' % (sname, key))
+    return ret
+
+
+def complete(section, option):
+    s = DEFAULTS.get(section)
+    if not s:
+        return []
+    o = s.get(option)
+    if not o:
+        return []
+    return o.completions
+
+
+def has_user_config():
+    return os.path.isfile(_PERUSER)
+
+
+def reset():
+    _configuration.reset()
+
+
+load()
+core = _Section('core')
+path = _Section('path')
+color = _Section('color')
+
+
+def load_version():
+    version, build = 'dev', 'unknown'
+    versioninfo_file = os.path.join(path.sharedir, 'version')
+    if os.path.isfile(versioninfo_file):
+        v = open(versioninfo_file).xreadlines()
+        try:
+            version = v.next().strip() or version
+            build = v.next().strip() or build
+        except StopIteration:
+            pass
+    return version, build
+
+VERSION, BUILD_VERSION = load_version()
+CRM_VERSION = "%s (Build %s)" % (VERSION, BUILD_VERSION)
diff --git a/modules/constants.py b/modules/constants.py
new file mode 100644
index 0000000..68fba46
--- /dev/null
+++ b/modules/constants.py
@@ -0,0 +1,289 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+from ordereddict import odict
+
+
+cib_cli_map = {
+    "node": "node",
+    "primitive": "primitive",
+    "group": "group",
+    "clone": "clone",
+    "master": "ms",
+    "rsc_location": "location",
+    "rsc_colocation": "colocation",
+    "rsc_order": "order",
+    "rsc_ticket": "rsc_ticket",
+    "template": "rsc_template",
+    "cluster_property_set": "property",
+    "rsc_defaults": "rsc_defaults",
+    "op_defaults": "op_defaults",
+    "acl_target": "acl_target",
+    "acl_group": "acl_group",
+    "acl_user": "user",
+    "acl_role": "role",
+    "fencing-topology": "fencing_topology",
+    "tag": "tag"
+}
+container_tags = ("group", "clone", "ms", "master")
+clonems_tags = ("clone", "ms", "master")
+resource_tags = ("primitive", "group", "clone", "ms", "master", "template")
+constraint_tags = ("rsc_location", "rsc_colocation", "rsc_order", "rsc_ticket")
+constraint_rsc_refs = ("rsc", "with-rsc", "first", "then")
+children_tags = ("group", "primitive")
+nvpairs_tags = ("meta_attributes", "instance_attributes", "utilization")
+defaults_tags = ("rsc_defaults", "op_defaults")
+resource_cli_names = ("primitive", "group", "clone", "ms", "master", "rsc_template")
+constraint_cli_names = ("location", "colocation", "collocation", "order", "rsc_ticket")
+nvset_cli_names = ("property", "rsc_defaults", "op_defaults")
+op_cli_names = ("monitor",
+                "start",
+                "stop",
+                "migrate_to",
+                "migrate_from",
+                "promote",
+                "demote",
+                "notify")
+ra_operations = ("probe", "monitor", "start", "stop",
+                 "promote", "demote", "notify", "migrate_to", "migrate_from")
+subpfx_list = {
+    "instance_attributes": "instance_attributes",
+    "meta_attributes": "meta_attributes",
+    "utilization": "utilization",
+    "operations": "ops",
+    "rule": "rule",
+    "expression": "expression",
+    "date_expression": "expression",
+    "duration": "duration",
+    "date_spec": "date_spec",
+    "read": "read",
+    "write": "write",
+    "deny": "deny",
+}
+acl_rule_names = ("read", "write", "deny")
+acl_spec_map = odict({
+    "xpath": "xpath",
+    "ref": "ref",
+    "tag": "tag",
+    "attribute": "attribute",
+})
+# ACLs were rewritten in pacemaker 1.1.12
+# this is the new acl syntax
+acl_spec_map_2 = odict({
+    "xpath": "xpath",
+    "ref": "reference",
+    "reference": "reference",
+    "tag": "object-type",
+    "type": "object-type",
+    "attr": "attribute",
+    "attribute": "attribute"
+})
+
+acl_spec_map_2_rev = (('xpath', 'xpath'),
+                      ('reference', 'ref'),
+                      ('attribute', 'attr'),
+                      ('object-type', 'type'))
+
+acl_shortcuts = {
+    "meta":
+    (r"//primitive\[@id='@@'\]/meta_attributes", r"/nvpair\[@name='@@'\]"),
+    "params":
+    (r"//primitive\[@id='@@'\]/instance_attributes", r"/nvpair\[@name='@@'\]"),
+    "utilization":
+    (r"//primitive\[@id='@@'\]/utilization",),
+    "location":
+    (r"//rsc_location\[@id='cli-prefer-@@' and @rsc='@@'\]",),
+    "property":
+    (r"//crm_config/cluster_property_set", r"/nvpair\[@name='@@'\]"),
+    "nodeattr":
+    (r"//nodes/node/instance_attributes", r"/nvpair\[@name='@@'\]"),
+    "nodeutil":
+    (r"//nodes/node/utilization", r"\[@uname='@@'\]"),
+    "node":
+    (r"//nodes/node", r"\[@uname='@@'\]"),
+    "status":
+    (r"/cib/status",),
+    "cib":
+    (r"/cib",),
+}
+lrm_exit_codes = {
+    "success": "0",
+    "unknown": "1",
+    "args": "2",
+    "unimplemented": "3",
+    "perm": "4",
+    "installed": "5",
+    "configured": "6",
+    "not_running": "7",
+    "master": "8",
+    "failed_master": "9",
+}
+lrm_status_codes = {
+    "pending": "-1",
+    "done": "0",
+    "cancelled": "1",
+    "timeout": "2",
+    "notsupported": "3",
+    "error": "4",
+}
+cib_user_attrs = ("validate-with",)
+node_states = ("online", "offline", "unclean")
+precious_attrs = ("id-ref",)
+op_extra_attrs = ("interval",)
+rsc_meta_attributes = (
+    "allow-migrate", "is-managed", "interval-origin",
+    "migration-threshold", "priority", "multiple-active",
+    "failure-timeout", "resource-stickiness", "target-role",
+    "restart-type", "description", "remote-node", "requires",
+)
+group_meta_attributes = ("container", )
+clone_meta_attributes = (
+    "ordered", "notify", "interleave", "globally-unique",
+    "clone-max", "clone-node-max", "clone-state", "description",
+)
+ms_meta_attributes = (
+    "master-max", "master-node-max", "description",
+)
+trace_ra_attr = "trace_ra"
+score_types = {'advisory': '0', 'mandatory': 'INFINITY'}
+boolean_ops = ('or', 'and')
+binary_ops = ('lt', 'gt', 'lte', 'gte', 'eq', 'ne')
+binary_types = ('string', 'version', 'number')
+unary_ops = ('defined', 'not_defined')
+simple_date_ops = ('lt', 'gt')
+date_ops = ('lt', 'gt', 'in_range', 'date_spec')
+date_spec_names = '''hours monthdays weekdays yearsdays months \
+weeks years weekyears moon'''.split()
+in_range_attrs = ('start', 'end')
+roles_names = ('Stopped', 'Started', 'Master', 'Slave')
+actions_names = ('start', 'promote', 'demote', 'stop')
+node_default_type = "normal"
+node_attributes_keyw = ("attributes", "utilization")
+shadow_envvar = "CIB_shadow"
+attr_defaults = {
+    "node": {"type": "normal"},
+    "resource_set": {"sequential": "true", "require-all": "true"},
+    "rule": {"boolean-op": "and"},
+}
+cib_no_section_rc = 6
+# Graphviz attributes for various CIB elements.
+# Shared for edge and node and graph attributes.
+# Keys are graphviz attributes, values are dicts where keys
+# are CIB element names and values graphviz values.
+# - element "." refers to the whole graph
+# - element "class:<ra_class>" refers to primitives of a
+#   specific RA class
+# - optional_set is a resource_set with require-all set to
+#   false
+# - group and optional_set are subgraphs (boxes)
+graph = {
+    ".": {
+        "compound": "true",
+    },
+    "*": {
+        "fontname": "Helvetica",
+        "fontsize": "11",
+    },
+    "node": {
+        "style": "bold",
+        "shape": "box",
+        "color": "blue",
+    },
+    "primitive": {
+        "fillcolor": "lightgrey",
+        "style": "filled",
+    },
+    "rsc_template": {
+        "fillcolor": "lightgrey",
+        "color": "mediumpurple",
+        "style": "filled",
+    },
+    "class:stonith": {
+        "shape": "box",
+        "style": "dashed",
+    },
+    "location": {
+        "style": "dashed",
+        "dir": "none",
+    },
+    "clone": {
+        "color": "red",
+    },
+    "ms": {
+        "color": "maroon",
+    },
+    "group": {
+        "color": "blue",
+        "group": "blue",
+        "labelloc": "b",
+        "labeljust": "r",
+        "labelfontsize": "12",
+    },
+    "optional_set": {
+        "style": "dotted",
+    },
+    "template:edge": {
+        "color": "grey64",
+        "style": "dotted",
+        "arrowtail": "open",
+        "dir": "back",
+    },
+}
+
+prompt = ''
+tmp_cib = False
+tmp_cib_prompt = "@tmp@"
+live_cib_prompt = "live"
+
+simulate_programs = {
+    "ptest": "ptest",
+    "simulate": "crm_simulate",
+}
+
+ra_if = None  # class interface to RA
+stonithd_metadata = None  # stonithd meta data
+pe_metadata = None  # PE meta data
+crmd_metadata = None  # crmd meta data
+cib_metadata = None  # cib meta data
+crm_properties_metadata = None  # PE + crmd + cib meta data
+meta_progs = ("crmd", "pengine", "stonithd", "cib")
+# elide these properties from tab completion
+crmd_metadata_do_not_complete = ("dc-version",
+                                 "cluster-infrastructure",
+                                 "crmd-integration-timeout",
+                                 "crmd-finalization-timeout",
+                                 "expected-quorum-votes")
+extra_cluster_properties = ("dc-version",
+                            "cluster-infrastructure",
+                            "last-lrm-refresh",
+                            "cluster-name")
+pcmk_version = ""  # set later
+
+# r.group(1) transition number (a different thing from file number)
+# r.group(2) contains full path
+# r.group(3) file number
+transition_patt = [
+    # transition start
+    "pengine.* process_pe_message: .*Transition ([0-9]+): .*([^ ]*/pe-[^-]+-(%%)[.]bz2)",
+    # r.group(1) transition number (a different thing from file number)
+    # r.group(2) contains full path
+    # r.group(3) transition status
+    # transition stop
+    "crmd.* run_graph: .*Transition ([0-9]+).*Source=(.*/pe-[^-]+-(%%)[.]bz2).: (Stopped|Complete|Terminated)",
+]
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/corosync.py b/modules/corosync.py
new file mode 100644
index 0000000..c2a8045
--- /dev/null
+++ b/modules/corosync.py
@@ -0,0 +1,532 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+'''
+Functions that abstract creating and editing the corosync.conf
+configuration file, and also the corosync-* utilities.
+'''
+
+import os
+import re
+import utils
+import tmpfiles
+import socket
+from msg import err_buf, common_debug
+
+
+def conf():
+    return os.getenv('COROSYNC_MAIN_CONFIG_FILE', '/etc/corosync/corosync.conf')
+
+
+def is_corosync_stack():
+    return utils.cluster_stack() == 'corosync'
+
+
+def cfgtool(*args):
+    return utils.get_stdout(['corosync-cfgtool'] + list(args), shell=False)
+
+
+def quorumtool(*args):
+    return utils.get_stdout(['corosync-quorumtool'] + list(args), shell=False)
+
+_tCOMMENT = 0
+_tBEGIN = 1
+_tEND = 2
+_tVALUE = 3
+
+
+class Token(object):
+    def __init__(self, token, path, key=None, value=None):
+        self.token = token
+        self.path = '.'.join(path)
+        self.key = key
+        self.value = value
+
+    def __repr__(self):
+        if self.token == _tCOMMENT:
+            return self.key
+        elif self.token == _tBEGIN:
+            return "%s {" % (self.key)
+        elif self.token == _tEND:
+            return '}'
+        else:
+            return '%s: %s' % (self.key, self.value)
+
+
+def corosync_tokenizer(stream):
+    """Parses the corosync config file into a token stream"""
+    section_re = re.compile(r'(\w+)\s*{')
+    value_re = re.compile(r'(\w+):\s*(\S+)')
+    path = []
+    while stream:
+        stream = stream.lstrip()
+        if not stream:
+            break
+        if stream[0] == '#':
+            end = stream.find('\n')
+            t = Token(_tCOMMENT, [], stream[:end])
+            stream = stream[end:]
+            yield t
+            continue
+        if stream[0] == '}':
+            t = Token(_tEND, [])
+            stream = stream[1:]
+            yield t
+            path = path[:-1]
+            continue
+        m = section_re.match(stream)
+        if m:
+            path.append(m.group(1))
+            t = Token(_tBEGIN, path, m.group(1))
+            stream = stream[m.end():]
+            yield t
+            continue
+        m = value_re.match(stream)
+        if m:
+            t = Token(_tVALUE, path + [m.group(1)], m.group(1), m.group(2))
+            stream = stream[m.end():]
+            yield t
+            continue
+        raise ValueError("Parse error at [..%s..]" % (stream[:16]))
+
+
+def make_section(path, contents=None):
+    "Create a token sequence representing a section"
+    if not contents:
+        contents = []
+    sp = path.split('.')
+    name = sp[-1]
+    for t in contents:
+        if t.path and not t.path.startswith(path):
+            raise ValueError("%s (%s) not in path %s" % (t.path, t.key, path))
+    return [Token(_tBEGIN, sp, name)] + contents + [Token(_tEND, [])]
+
+
+def make_value(path, value):
+    "Create a token sequence representing a value"
+    sp = path.split('.')
+    name = sp[-1]
+    return [Token(_tVALUE, sp, name, value)]
+
+
+class Parser(object):
+    def __init__(self, data):
+        self._tokens = list(corosync_tokenizer(data))
+
+    def find(self, name, start=0):
+        """Gets the index of the element with the given path"""
+        for i, t in enumerate(self._tokens[start:]):
+            if t.path == name:
+                return i + start
+        return -1
+
+    def find_bounds(self, name, start=0):
+        """find the (start, end) of the next instance of name found at start"""
+        i = self.find(name, start)
+        if i < 0:
+            return -1, -1
+        if self._tokens[i].token != _tBEGIN:
+            return i, i
+        e = i + 1
+        depth = 0
+        while e < len(self._tokens):
+            t = self._tokens[e]
+            if t.token == _tBEGIN:
+                depth += 1
+            if t.token == _tEND:
+                depth -= 1
+            if depth < 0:
+                break
+            e += 1
+        if e == len(self._tokens):
+            raise ValueError("Unclosed section")
+        return i, e
+
+    def get(self, path):
+        """Gets the value for the key (if any)"""
+        for t in self._tokens:
+            if t.token == _tVALUE and t.path == path:
+                return t.value
+        return None
+
+    def get_all(self, path):
+        """Returns all values matching path"""
+        ret = []
+        for t in self._tokens:
+            if t.token == _tVALUE and t.path == path:
+                ret.append(t.value)
+        return ret
+
+    def all_paths(self):
+        """Returns all value paths"""
+        ret = []
+        for t in self._tokens:
+            if t.token == _tVALUE:
+                ret.append(t.path)
+        return ret
+
+    def count(self, path):
+        """Returns the number of elements matching path"""
+        n = 0
+        for t in self._tokens:
+            if t.path == path:
+                n += 1
+        return n
+
+    def remove(self, path):
+        """Removes the given section or value"""
+        i, e = self.find_bounds(path)
+        if i < 0:
+            return
+        self._tokens = self._tokens[:i] + self._tokens[(e+1):]
+
+    def remove_section_where(self, path, key, value):
+        """
+        Remove section which contains key: value
+        Used to remove node definitions
+        """
+        nth = -1
+        start = 0
+        keypath = '.'.join([path, key])
+        while True:
+            nth += 1
+            i, e = self.find_bounds(path, start)
+            start = e + 1
+            if i < 0:
+                break
+            k = self.find(keypath, i)
+            if k < 0 or k > e:
+                continue
+            vt = self._tokens[k]
+            if vt.token == _tVALUE and vt.value == value:
+                self._tokens = self._tokens[:i] + self._tokens[(e+1):]
+                return nth
+        return -1
+
+    def add(self, path, tokens):
+        """Adds tokens to a section"""
+        common_debug("corosync.add (%s) (%s)" % (path, tokens))
+        if not path:
+            self._tokens += tokens
+            return
+        start = self.find(path)
+        if start < 0:
+            return None
+        depth = 0
+        end = None
+        for i, t in enumerate(self._tokens[start + 1:]):
+            if t.token == _tBEGIN:
+                depth += 1
+            elif t.token == _tEND:
+                depth -= 1
+            if depth < 0:
+                end = start + i + 1
+                break
+        if end is None:
+            raise ValueError("Unterminated section at %s" % (start))
+        self._tokens = self._tokens[:end] + tokens + self._tokens[end:]
+
+    def set(self, path, value):
+        """Sets a key: value entry. sections are given
+        via dot-notation."""
+        i = self.find(path)
+        if i < 0:
+            spath = path.split('.')
+            return self.add('.'.join(spath[:-1]),
+                            make_value(path, value))
+        if self._tokens[i].token != _tVALUE:
+            raise ValueError("%s is not a value" % (path))
+        self._tokens[i].value = value
+
+    def to_string(self):
+        '''
+        Serialize tokens into the corosync.conf
+        file format
+        '''
+        def joiner(tstream):
+            indent = 0
+            last = None
+            while tstream:
+                t = tstream[0]
+                if indent and t.token == _tEND:
+                    indent -= 1
+                s = ''
+                if t.token == _tCOMMENT and (last and last.token != _tCOMMENT):
+                    s += '\n'
+                s += ('\t'*indent) + str(t) + '\n'
+                if t.token == _tEND:
+                    s += '\n'
+                yield s
+                if t.token == _tBEGIN:
+                    indent += 1
+                last = t
+                tstream = tstream[1:]
+        return ''.join(joiner(self._tokens))
+
+
+def logfile(conftext):
+    '''
+    Return corosync logfile (if set)
+    '''
+    return Parser(conftext).get('logging.logfile')
+
+
+def push_configuration(nodes):
+    '''
+    Push the local configuration to the list of remote nodes
+    '''
+    try:
+        from psshlib import api as pssh
+        _has_pssh = True
+    except ImportError:
+        _has_pssh = False
+
+    if not _has_pssh:
+        raise ValueError("PSSH is required to push")
+
+    local_path = conf()
+
+    opts = pssh.Options()
+    opts.timeout = 60
+    opts.ssh_options += ['ControlPersist=no']
+    ok = True
+    for host, result in pssh.copy(nodes,
+                                  local_path,
+                                  local_path, opts).iteritems():
+        if isinstance(result, pssh.Error):
+            err_buf.error("Failed to push configuration to %s: %s" % (host, result))
+            ok = False
+        else:
+            err_buf.ok(host)
+    return ok
+
+
+def pull_configuration(from_node):
+    '''
+    Copy the configuration from the given node to this node
+    '''
+    local_path = conf()
+    _, fname = tmpfiles.create()
+    print "Retrieving %s:%s..." % (from_node, local_path)
+    cmd = ['scp', '-qC',
+           '-o', 'PasswordAuthentication=no',
+           '-o', 'StrictHostKeyChecking=no',
+           '%s:%s' % (from_node, local_path),
+           fname]
+    rc = utils.ext_cmd_nosudo(cmd, shell=False)
+    if rc == 0:
+        data = open(fname).read()
+        newhash = hash(data)
+        if os.path.isfile(local_path):
+            oldata = open(local_path).read()
+            oldhash = hash(oldata)
+            if newhash == oldhash:
+                print "No change."
+                return
+        print "Writing %s..."
+        local_file = open(local_path, 'w')
+        local_file.write(data)
+        local_file.close()
+    else:
+        raise ValueError("Failed to retrieve %s from %s" % (local_path, from_node))
+
+
+def _diff_slurp(pssh, nodes, filename):
+    tmpdir = tmpfiles.create_dir()
+    opts = pssh.Options()
+    opts.localdir = tmpdir
+    dst = os.path.basename(filename)
+    return pssh.slurp(nodes, filename, dst, opts).items()
+
+
+def _diff_this(pssh, local_path, nodes, this_node):
+    by_host = _diff_slurp(pssh, nodes, local_path)
+    for host, result in by_host:
+        if isinstance(result, pssh.Error):
+            raise ValueError("Failed on %s: %s" % (host, str(result)))
+        _, _, _, path = result
+        _, s = utils.get_stdout("diff -U 0 -d -b --label %s --label %s %s %s" %
+                                (host, this_node, path, local_path))
+        utils.page_string(s)
+
+
+def _diff(pssh, local_path, nodes):
+    by_host = _diff_slurp(pssh, nodes, local_path)
+    for host, result in by_host:
+        if isinstance(result, pssh.Error):
+            raise ValueError("Failed on %s: %s" % (host, str(result)))
+    h1, r1 = by_host[0]
+    h2, r2 = by_host[1]
+    _, s = utils.get_stdout("diff -U 0 -d -b --label %s --label %s %s %s" %
+                            (h1, h2, r1[3], r2[3]))
+    utils.page_string(s)
+
+
+def _checksum(pssh, local_path, nodes, this_node):
+    import hashlib
+
+    by_host = _diff_slurp(pssh, nodes, local_path)
+    for host, result in by_host:
+        if isinstance(result, pssh.Error):
+            raise ValueError(str(result))
+
+    print "%-16s  SHA1 checksum of %s" % ('Host', local_path)
+    if this_node not in nodes:
+        print "%-16s: %s" % (this_node, hashlib.sha1(open(local_path).read()).hexdigest())
+    for host, result in by_host:
+        _, _, _, path = result
+        print "%-16s: %s" % (host, hashlib.sha1(open(path).read()).hexdigest())
+
+
+def diff_configuration(nodes, checksum=False):
+    try:
+        from psshlib import api as pssh
+        _has_pssh = True
+    except ImportError:
+        _has_pssh = False
+    if not _has_pssh:
+        raise ValueError("PSSH is required to diff")
+
+    local_path = conf()
+    this_node = utils.this_node()
+    nodes = list(nodes)
+    if checksum or len(nodes) > 2:
+        _checksum(pssh, local_path, nodes, this_node)
+    elif len(nodes) == 1:
+        _diff_this(pssh, local_path, nodes, this_node)
+    elif this_node in nodes:
+        nodes.remove(this_node)
+        _diff_this(pssh, local_path, nodes, this_node)
+    elif len(nodes):
+        _diff(pssh, local_path, nodes)
+
+
+def next_nodeid(parser):
+    ids = parser.get_all('nodelist.node.nodeid')
+    if not ids:
+        return 1
+    return max([int(i) for i in ids]) + 1
+
+
+def get_ip(node):
+    try:
+        return socket.gethostbyname(node)
+    except:
+        return None
+
+
+def get_all_paths():
+    f = open(conf()).read()
+    p = Parser(f)
+    return p.all_paths()
+
+
+def get_value(path):
+    f = open(conf()).read()
+    p = Parser(f)
+    return p.get(path)
+
+
+def get_values(path):
+    f = open(conf()).read()
+    p = Parser(f)
+    return p.get_all(path)
+
+
+def set_value(path, value):
+    f = open(conf()).read()
+    p = Parser(f)
+    p.set(path, value)
+    f = open(conf(), 'w')
+    f.write(p.to_string())
+    f.close()
+
+
+def add_node(name):
+    '''
+    Add node to corosync.conf
+    '''
+    coronodes = None
+    nodes = None
+    coronodes = utils.list_corosync_nodes()
+    try:
+        nodes = utils.list_cluster_nodes()
+    except Exception:
+        nodes = []
+    ipaddr = get_ip(name)
+    if name in coronodes or (ipaddr and ipaddr in coronodes):
+        err_buf.warning("%s already in corosync.conf" % (name))
+        return
+    if name in nodes:
+        err_buf.warning("%s already in configuration" % (name))
+        return
+
+    f = open(conf()).read()
+    p = Parser(f)
+
+    node_addr = name
+    node_id = next_nodeid(p)
+
+    p.add('nodelist',
+          make_section('nodelist.node',
+                       make_value('nodelist.node.ring0_addr', node_addr) +
+                       make_value('nodelist.node.nodeid', str(node_id))))
+
+    num_nodes = p.count('nodelist.node')
+    if num_nodes > 2:
+        p.remove('quorum.two_node')
+
+    f = open(conf(), 'w')
+    f.write(p.to_string())
+    f.close()
+
+    # update running config (if any)
+    if nodes:
+        utils.ext_cmd(["corosync-cmapctl",
+                       "-s", "nodelist.node.%s.nodeid" % (num_nodes - 1),
+                       "u32", str(node_id)], shell=False)
+        utils.ext_cmd(["corosync-cmapctl",
+                       "-s", "nodelist.node.%s.ring0_addr" % (num_nodes - 1),
+                       "str", node_addr], shell=False)
+
+
+def del_node(addr):
+    '''
+    Remove node from corosync
+    '''
+    f = open(conf()).read()
+    p = Parser(f)
+    nth = p.remove_section_where('nodelist.node', 'ring0_addr', addr)
+    if nth == -1:
+        return
+
+    if p.count('nodelist.node') <= 2:
+        p.set('quorum.two_node', '1')
+
+    f = open(conf(), 'w')
+    f.write(p.to_string())
+    f.close()
+
+    # check for running config
+    try:
+        nodes = utils.list_cluster_nodes()
+    except Exception:
+        nodes = []
+    if nodes:
+        utils.ext_cmd(["corosync-cmapctl", "-D", "nodelist.node.%s.nodeid" % (nth)],
+                      shell=False)
+        utils.ext_cmd(["corosync-cmapctl", "-D", "nodelist.node.%s.ring0_addr" % (nth)],
+                      shell=False)
diff --git a/modules/crm_gv.py b/modules/crm_gv.py
new file mode 100644
index 0000000..f27d4a9
--- /dev/null
+++ b/modules/crm_gv.py
@@ -0,0 +1,242 @@
+# Copyright (C) 2013 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import config
+import tmpfiles
+import utils
+from msg import common_err
+from ordereddict import odict
+
+# graphviz stuff
+
+
+def _attr_str(attr_d):
+    return ','.join(['%s="%s"' % (k, v)
+                     for k, v in attr_d.iteritems()])
+
+
+class Gv(object):
+    '''
+    graph.
+    '''
+    EDGEOP = ''  # actually defined in subclasses
+
+    def __init__(self, id=None):
+        if id:
+            self.id = self.gv_id(id)
+        else:
+            self.id = ""
+        self.nodes = {}
+        self.edges = []
+        self.subgraphs = []
+        self.node_attrs = odict()
+        self.attrs = odict()
+        self.graph_attrs = odict()
+        self.edge_attrs = []
+        self.top_nodes = []
+        self.norank_nodes = []
+
+    def gv_id(self, n):
+        return n.replace('-', '_').replace('.', '_')
+
+    def new_graph_attr(self, attr, v):
+        self.graph_attrs[attr] = v
+
+    def new_attr(self, n, attr_n, attr_v):
+        id = self.gv_id(n)
+        if id not in self.attrs:
+            self.attrs[id] = odict()
+        self.attrs[id][attr_n] = attr_v
+
+    def new_node(self, n, top_node=False, norank=False):
+        '''
+        Register every node.
+        '''
+        id = self.gv_id(n)
+        if top_node:
+            self.top_nodes.append(id)
+        elif id not in self.nodes:
+            self.nodes[id] = 0
+        if norank:
+            self.norank_nodes.append(id)
+
+    def my_edge(self, e):
+        return [self.gv_id(x) for x in e]
+
+    def new_edge(self, e):
+        ne = self.my_edge(e)
+        for i, node in enumerate(ne):
+            if i == 0:
+                continue
+            if node in self.top_nodes:
+                continue
+            self.nodes[node] = i
+        self.edges.append(ne)
+        self.edge_attrs.append(odict())
+        return len(self.edges)-1
+
+    def new_edge_attr(self, e_id, attr_n, attr_v):
+        if e_id >= len(self.edge_attrs):
+            return  # if the caller didn't create an edge beforehand
+        self.edge_attrs[e_id][attr_n] = attr_v
+
+    def edge_str(self, e_id):
+        e_s = self.EDGEOP.join(self.edges[e_id])
+        if e_id < len(self.edge_attrs):
+            return('%s [%s]' % (e_s, _attr_str(self.edge_attrs[e_id])))
+        else:
+            return e_s
+
+    def invis_edge_str(self, tn, node):
+        attrs = 'style="invis"'
+        if node in self.norank_nodes:
+            attrs = '%s,constraint="false"' % attrs
+        return '%s [%s];' % (self.EDGEOP.join([tn, node]), attrs)
+
+    def invisible_edges(self):
+        '''
+        Dump invisible edges from top_nodes to every node which
+        is at the top of the edge or not in any other edge. This
+        seems to be the only way to keep the nodes (as in cluster
+        nodes) above resources.
+        NB: This is O(n^2) (nodes times resources).
+        '''
+        l = []
+        for tn in self.top_nodes:
+            for node, rank in self.nodes.iteritems():
+                if rank > 0:
+                    continue
+                l.append('\t%s' % self.invis_edge_str(tn, node))
+        return l
+
+    def header(self):
+        return ''
+
+    def footer(self):
+        return ''
+
+    def repr(self):
+        '''
+        Dump gv graph to a string.
+        '''
+        l = []
+        l.append(self.header())
+        if self.node_attrs:
+            l.append('\tnode [%s];' % _attr_str(self.node_attrs))
+        for attr, v in self.graph_attrs.iteritems():
+            l.append('\t%s="%s";' % (attr, v))
+        for sg in self.subgraphs:
+            l.append('\t%s' % '\n\t'.join(sg.repr()))
+        for e_id in range(len(self.edges)):
+            l.append('\t%s;' % self.edge_str(e_id))
+        for n, attr_d in self.attrs.iteritems():
+            attr_s = _attr_str(attr_d)
+            l.append('\t%s [%s];' % (n, attr_s))
+        l += self.invisible_edges()
+        l.append(self.footer())
+        return l
+
+    def totmpf(self):
+        return utils.str2tmp('\n'.join(self.repr()))
+
+    def save(self, outf):
+        f = utils.safe_open_w(outf)
+        if not f:
+            return False
+        f.write('\n'.join(self.repr()))
+        f.write('\n')
+        utils.safe_close_w(f)
+        return True
+
+
+class GvDot(Gv):
+    '''
+    graphviz dot directed graph.
+    '''
+    EDGEOP = ' -> '
+
+    def __init__(self, id=None):
+        Gv.__init__(self, id)
+
+    def header(self):
+        name = self.id and self.id or "G"
+        return 'digraph %s {\n' % (name)
+
+    def footer(self):
+        return '}'
+
+    def group(self, members, id=None):
+        '''
+        Groups are subgraphs.
+        '''
+        sg_obj = SubgraphDot(id)
+        sg_obj.new_edge(members)
+        self.subgraphs.append(sg_obj)
+        self.new_node(members[0])
+        return sg_obj
+
+    def optional_set(self, members, id=None):
+        '''
+        Optional resource sets.
+        '''
+        sg_obj = SubgraphDot(id)
+        e_id = sg_obj.new_edge(members)
+        sg_obj.new_edge_attr(e_id, 'style', 'invis')
+        sg_obj.new_edge_attr(e_id, 'constraint', 'false')
+        self.subgraphs.append(sg_obj)
+        return sg_obj
+
+    def display(self):
+        if not config.core.dotty:
+            common_err("dotty not found")
+            return False
+        dotf = self.totmpf()
+        if not dotf:
+            return False
+        utils.show_dot_graph(dotf, desc="configuration graph")
+        return True
+
+    def image(self, img_type, outf):
+        if not config.core.dot:
+            common_err("dot not found")
+            return False
+        dotf = self.totmpf()
+        if not dotf:
+            return False
+        tmpfiles.add(dotf)
+        return (utils.ext_cmd_nosudo("%s -T%s -o%s %s" %
+                                     (config.core.dot, img_type, outf, dotf)) == 0)
+
+
+class SubgraphDot(GvDot):
+    '''
+    graphviz subgraph.
+    '''
+    def __init__(self, id=None):
+        Gv.__init__(self, id)
+
+    def header(self):
+        if self.id:
+            return 'subgraph %s {' % self.id
+        else:
+            return '{'
+
+gv_types = {
+    "dot": GvDot,
+}
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/crm_pssh.py b/modules/crm_pssh.py
new file mode 100755
index 0000000..bbfdc81
--- /dev/null
+++ b/modules/crm_pssh.py
@@ -0,0 +1,237 @@
+# Modified pssh
+# Copyright (c) 2011, Dejan Muhamedagic
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+"""Parallel ssh to the set of nodes in hosts.txt.
+
+For each node, this essentially does an "ssh host -l user prog [arg0] [arg1]
+...". The -o option can be used to store stdout from each remote node in a
+directory.  Each output file in that directory will be named by the
+corresponding remote node's hostname or IP address.
+"""
+
+import os
+import sys
+import glob
+import re
+
+parent, bindir = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0])))
+if os.path.exists(os.path.join(parent, 'psshlib')):
+    sys.path.insert(0, parent)
+
+from psshlib.manager import Manager, FatalError
+from psshlib.task import Task
+from psshlib.cli import common_parser, common_defaults
+
+try:
+    from psshlib import api as pssh_api
+    has_patched_pssh = True
+except ImportError:
+    has_patched_pssh = False
+
+from msg import common_err, common_debug, common_warn
+
+
+_DEFAULT_TIMEOUT = 60
+_EC_LOGROT = 120
+
+
+def make_pssh_opts(outdir, errdir):
+    '''
+    -q is only available if pssh has been
+    patched
+    '''
+    if has_patched_pssh:
+        return ["-q", "-o", outdir, "-e", errdir]
+    else:
+        return ["-o", outdir, "-e", errdir]
+
+
+def option_parser():
+    '''
+    Create commandline option parser.
+    '''
+    parser = common_parser()
+    parser.usage = "%prog [OPTIONS] command [...]"
+    parser.epilog = "Example: pssh -h hosts.txt -l irb2 -o /tmp/foo uptime"
+
+    parser.add_option('-i', '--inline', dest='inline', action='store_true',
+                      help='inline aggregated output for each server')
+    parser.add_option('-I', '--send-input', dest='send_input',
+                      action='store_true',
+                      help='read from standard input and send as input to ssh')
+    parser.add_option('-P', '--print', dest='print_out', action='store_true',
+                      help='print output as we get it')
+
+    return parser
+
+
+def parse_args(myargs, t=_DEFAULT_TIMEOUT):
+    '''
+    Parse the given commandline arguments.
+    '''
+    parser = option_parser()
+    defaults = common_defaults(timeout=t)
+    parser.set_defaults(**defaults)
+    opts, args = parser.parse_args(myargs)
+    return opts, args
+
+
+def get_output(dir, host):
+    '''
+    Looks for the output returned by the given host.
+    This is somewhat problematic, since it is possible that
+    different hosts can have similar hostnames. For example naming
+    hosts "host.1" and "host.2" will confuse this code.
+    '''
+    l = []
+    for fname in ["%s/%s" % (dir, host)] + glob.glob("%s/%s.[0-9]*" % (dir, host)):
+        try:
+            if os.path.isfile(fname):
+                l += open(fname).readlines()
+        except:
+            continue
+    return l
+
+
+def show_output(dir, hosts, desc):
+    '''
+    Display output from hosts. See get_output for caveats.
+    '''
+    for host in hosts:
+        out_l = get_output(dir, host)
+        if out_l:
+            print "%s %s:" % (host, desc)
+            print ''.join(out_l)
+
+
+def do_pssh(l, opts):
+    '''
+    Adapted from psshlib. Perform command across list of hosts.
+    l = [(host, command), ...]
+    '''
+    if opts.outdir and not os.path.exists(opts.outdir):
+        os.makedirs(opts.outdir)
+    if opts.errdir and not os.path.exists(opts.errdir):
+        os.makedirs(opts.errdir)
+    if opts.send_input:
+        stdin = sys.stdin.read()
+    else:
+        stdin = None
+    manager = Manager(opts)
+    user = ""
+    port = ""
+    hosts = []
+    for host, cmdline in l:
+        cmd = ['ssh', host,
+               '-o', 'PasswordAuthentication=no',
+               '-o', 'SendEnv=PSSH_NODENUM',
+               '-o', 'StrictHostKeyChecking=no']
+        if opts.options:
+            for opt in opts.options:
+                cmd += ['-o', opt]
+        if user:
+            cmd += ['-l', user]
+        if port:
+            cmd += ['-p', port]
+        if opts.extra:
+            cmd.extend(opts.extra)
+        if cmdline:
+            cmd.append(cmdline)
+        hosts.append(host)
+        t = Task(host, port, user, cmd, opts, stdin)
+        manager.add_task(t)
+    try:
+        return manager.run()  # returns a list of exit codes
+    except FatalError:
+        common_err("pssh to nodes failed")
+        show_output(opts.errdir, hosts, "stderr")
+        return False
+
+
+def examine_outcome(l, opts, statuses):
+    '''
+    A custom function to show stderr in case there were issues.
+    Not suited for callers who want better control of output or
+    per-host processing.
+    '''
+    hosts = [x[0] for x in l]
+    if min(statuses) < 0:
+        # At least one process was killed.
+        common_err("ssh process was killed")
+        show_output(opts.errdir, hosts, "stderr")
+        return False
+    # The any builtin was introduced in Python 2.5 (so we can't use it yet):
+    #elif any(x==255 for x in statuses):
+    for status in statuses:
+        if status == 255:
+            common_warn("ssh processes failed")
+            show_output(opts.errdir, hosts, "stderr")
+            return False
+    for status in statuses:
+        if status not in (0, _EC_LOGROT):
+            common_warn("some ssh processes failed")
+            show_output(opts.errdir, hosts, "stderr")
+            return False
+    return True
+
+
+def next_loglines(a, outdir, errdir):
+    '''
+    pssh to nodes to collect new logs.
+    '''
+    l = []
+    for node, rptlog, logfile, nextpos in a:
+        common_debug("updating %s from %s (pos %d)" %
+                     (logfile, node, nextpos))
+        cmdline = "perl -e 'exit(%d) if (stat(\"%s\"))[7]<%d' && tail -c +%d %s" % (
+            _EC_LOGROT, logfile, nextpos-1, nextpos, logfile)
+        opts, args = parse_args(make_pssh_opts(outdir, errdir))
+        l.append([node, cmdline])
+    statuses = do_pssh(l, opts)
+    if statuses:
+        return examine_outcome(l, opts, statuses)
+    else:
+        return False
+
+
+def next_peinputs(node_pe_l, outdir, errdir):
+    '''
+    pssh to nodes to collect new logs.
+    '''
+    l = []
+    for node, pe_l in node_pe_l:
+        r = re.search("(.*)/pengine/", pe_l[0])
+        if not r:
+            common_err("strange, %s doesn't contain string pengine" % pe_l[0])
+            continue
+        dir = "/%s" % r.group(1)
+        red_pe_l = [x.replace("%s/" % r.group(1), "") for x in pe_l]
+        common_debug("getting new PE inputs %s from %s" % (red_pe_l, node))
+        cmdline = "tar -C %s -cf - %s" % (dir, ' '.join(red_pe_l))
+        opts, args = parse_args(make_pssh_opts(outdir, errdir))
+        l.append([node, cmdline])
+    if not l:
+        # is this a failure?
+        return True
+    statuses = do_pssh(l, opts)
+    if statuses:
+        return examine_outcome(l, opts, statuses)
+    else:
+        return False
+
+
+def do_pssh_cmd(cmd, node_l, outdir, errdir, timeout=20000):
+    '''
+    pssh to nodes and run cmd.
+    '''
+    l = []
+    for node in node_l:
+        l.append([node, cmd])
+    if not l:
+        return True
+    opts, args = parse_args(make_pssh_opts(outdir, errdir), t=str(int(timeout/1000)))
+    return do_pssh(l, opts)
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/help.py b/modules/help.py
new file mode 100644
index 0000000..49b04ba
--- /dev/null
+++ b/modules/help.py
@@ -0,0 +1,403 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+'''
+The commands exposed by this module all
+get their data from the doc/crm.8.txt text
+file. In that file, there are help for
+ - topics
+ - levels
+ - commands in levels
+
+The help file is lazily loaded when the first
+request for help is made.
+
+All help is in the following form in the manual:
+[[cmdhelp_<level>_<cmd>,<short help text>]]
+=== ...
+Long help text.
+...
+[[cmdhelp_<level>_<cmd>,<short help text>]]
+
+Help for the level itself is like this:
+
+[[cmdhelp_<level>,<short help text>]]
+'''
+
+import os
+import re
+from utils import page_string
+from msg import common_err
+import config
+import clidisplay
+from ordereddict import odict
+
+
+class HelpFilter(object):
+    _B0 = re.compile(r'^\.{4,}')
+    _B1 = re.compile(r'^\*{4,}')
+    _QUOTED = re.compile(r'`([^`]+)`')
+    _MONO = re.compile(r'\+([^+]+)\+')
+    _TOPIC = re.compile(r'(.*)::$')
+    _TOPIC2 = re.compile(r'^\.\w+')
+
+    def __init__(self):
+        self.in_block = False
+
+    def _filter(self, line):
+        block_edge = self._B0.match(line) or self._B1.match(line)
+        if block_edge and not self.in_block:
+            self.in_block = True
+            return ''
+        elif block_edge and self.in_block:
+            self.in_block = False
+            return ''
+        elif not self.in_block:
+            if self._TOPIC2.match(line):
+                return clidisplay.help_topic(line[1:])
+            line = self._QUOTED.sub(clidisplay.help_keyword(r'\1'), line)
+            line = self._MONO.sub(clidisplay.help_block(r'\1'), line)
+            line = self._TOPIC.sub(clidisplay.help_topic(r'\1'), line)
+            return line
+        else:
+            return clidisplay.help_block(line)
+
+    def __call__(self, text):
+        return '\n'.join([self._filter(line) for line in text.splitlines()]) + '\n'
+
+
+class HelpEntry(object):
+    def __init__(self, short_help, long_help='', alias_for=None, generated=False):
+        if short_help:
+            self.short = short_help[0].upper() + short_help[1:]
+        else:
+            self.short = 'Help'
+        self.long = long_help
+        self.alias_for = alias_for
+        self.generated = generated
+
+    def is_alias(self):
+        return self.alias_for is not None
+
+    def paginate(self):
+        '''
+        Display help, paginated.
+        Replace asciidoc syntax with colorized output where possible.
+        '''
+        helpfilter = HelpFilter()
+
+        short_help = clidisplay.help_header(self.short)
+
+        long_help = self.long
+        if long_help:
+            long_help = helpfilter(long_help)
+            if not long_help.startswith('\n'):
+                long_help = '\n' + long_help
+
+        prefix = ''
+        if self.is_alias():
+            prefix = helpfilter("(Redirected from `%s` to `%s`)\n" % self.alias_for)
+
+        page_string(short_help + '\n' + prefix + long_help)
+
+    def __str__(self):
+        if self.long:
+            return self.short + '\n' + self.long
+        return self.short
+
+    def __repr__(self):
+        return str(self)
+
+
+HELP_FILE = os.path.join(config.path.sharedir, 'crm.8.txt')
+
+_DEFAULT = HelpEntry('No help available', long_help='', alias_for=None, generated=True)
+_REFERENCE_RE = re.compile(r'<<[^,]+,(.+)>>')
+
+# loaded on demand
+# _LOADED is set to True when an attempt
+# has been made (so it won't be tried again)
+_LOADED = False
+_TOPICS = odict()
+_LEVELS = odict()
+_COMMANDS = odict()
+
+_TOPICS["Overview"] = HelpEntry("Available help topics and commands", generated=True)
+_TOPICS["Topics"] = HelpEntry("Available help topics", generated=True)
+
+
+def _titleline(title, desc, suffix=''):
+    return '%-16s %s\n' % (('`%s`' % (title)) + suffix, desc)
+
+
+def help_overview():
+    '''
+    Returns an overview of all available
+    topics and commands.
+    '''
+    _load_help()
+    s = "Available topics:\n\n"
+    for title, topic in _TOPICS.iteritems():
+        s += '\t' + _titleline(title, topic.short)
+    s += "\n"
+    s += "Available commands:\n\n"
+
+    for title, command in _COMMANDS.get('root', {}).iteritems():
+        if not command.is_alias():
+            s += '\t' + _titleline(title, command.short)
+    s += "\n"
+
+    hidden_commands = ('up', 'cd', 'help', 'quit', 'ls')
+
+    for title, level in _LEVELS.iteritems():
+        if title != 'root' and title in _COMMANDS:
+            s += '\t' + _titleline(title, level.short, suffix='/')
+            for cmdname, cmd in _COMMANDS[title].iteritems():
+                if cmdname in hidden_commands:
+                    continue
+                if not cmd.is_alias():
+                    s += '\t\t' + _titleline(cmdname, cmd.short)
+            s += "\n"
+    return HelpEntry('Help overview for crmsh\n', s, generated=True)
+
+
+def help_topics():
+    '''
+    Returns an overview of all available
+    topics.
+    '''
+    _load_help()
+    s = ''
+    for title, topic in _TOPICS.iteritems():
+        s += '\t' + _titleline(title, topic.short)
+    return HelpEntry('Available topics\n', s, generated=True)
+
+
+def list_help_topics():
+    _load_help()
+    return _TOPICS.keys()
+
+
+def help_topic(topic):
+    '''
+    Returns a help entry for a given topic.
+    '''
+    _load_help()
+    return _TOPICS.get(topic, _DEFAULT)
+
+
+def help_level(level):
+    '''
+    Returns a help entry for a given level.
+    '''
+    _load_help()
+    return _LEVELS.get(level, _DEFAULT)
+
+
+def help_command(level, command):
+    '''
+    Returns a help entry for a given command
+    '''
+    _load_help()
+    lvlhelp = _COMMANDS.get(level)
+    if not lvlhelp:
+        raise ValueError("Undocumented topic '%s'" % (level))
+    cmdhelp = lvlhelp.get(command)
+    if not cmdhelp:
+        raise ValueError("Undocumented topic '%s' in '%s'" % (command, level))
+    return cmdhelp
+
+
+def _is_help_topic(arg):
+    return arg and arg[0].isupper()
+
+
+def _is_command(level, command):
+    return level in _COMMANDS and command in _COMMANDS[level]
+
+
+def _is_level(level):
+    return level in _LEVELS
+
+
+def help_contextual(context, subject, subtopic):
+    """
+    Returns contextual help
+    """
+    _load_help()
+    if subject is None:
+        if context == 'root':
+            return help_overview()
+        return help_level(context)
+    if subject.lower() == 'overview':
+        return help_overview()
+    if subject.lower() == 'topics':
+        return help_topics()
+    if _is_help_topic(subject):
+        return help_topic(subject)
+    if subtopic is not None:
+        return help_command(subject, subtopic)
+    if _is_command(context, subject):
+        return help_command(context, subject)
+    if _is_level(subject):
+        return help_level(subject)
+    raise ValueError("Undocumented topic '%s'" % (subject))
+
+
+def add_help(entry, topic=None, level=None, command=None):
+    '''
+    Takes a help entry as argument and inserts it into the
+    help system.
+
+    Used to define some help texts statically, for example
+    for 'up' and 'help' itself.
+    '''
+    if topic:
+        if topic not in _TOPICS or _TOPICS[topic] is _DEFAULT:
+            _TOPICS[topic] = entry
+    elif level and command:
+        if level not in _LEVELS:
+            _LEVELS[level] = HelpEntry("No description available", generated=True)
+        if level not in _COMMANDS:
+            _COMMANDS[level] = odict()
+        lvl = _COMMANDS[level]
+        if command not in lvl or lvl[command] is _DEFAULT:
+            lvl[command] = entry
+    elif level:
+        if level not in _LEVELS or _LEVELS[level] is _DEFAULT:
+            _LEVELS[level] = entry
+
+
+def _load_help():
+    '''
+    Lazily load and parse crm.8.txt.
+    '''
+    global _LOADED
+    if _LOADED:
+        return
+    _LOADED = True
+
+    def parse_header(line):
+        'returns a new entry'
+        entry = {'type': '', 'name': '', 'short': '', 'long': ''}
+        line = line[2:-3]  # strip [[ and ]]\n
+        info, short_help = line.split(',', 1)
+        entry['short'] = short_help.strip()
+        info = info.split('_')
+        if info[0] == 'topics':
+            entry['type'] = 'topic'
+            entry['name'] = info[-1]
+        elif info[0] == 'cmdhelp':
+            if len(info) == 2:
+                entry['type'] = 'level'
+                entry['name'] = info[1]
+            elif len(info) >= 3:
+                entry['type'] = 'command'
+                entry['level'] = info[1]
+                entry['name'] = '_'.join(info[2:])
+
+        return entry
+
+    def process(entry):
+        'writes the entry into topics/levels/commands'
+        short_help = entry['short']
+        long_help = entry['long']
+        if long_help.startswith('=='):
+            long_help = long_help.split('\n', 1)[1]
+        helpobj = HelpEntry(short_help, long_help.rstrip())
+        name = entry['name']
+        if entry['type'] == 'topic':
+            _TOPICS[name] = helpobj
+        elif entry['type'] == 'level':
+            _LEVELS[name] = helpobj
+        elif entry['type'] == 'command':
+            lvl = entry['level']
+            if lvl not in _COMMANDS:
+                _COMMANDS[lvl] = odict()
+            _COMMANDS[lvl][name] = helpobj
+
+    def filter_line(line):
+        '''clean up an input line
+         - <<...>> references -> short description
+        '''
+        return _REFERENCE_RE.sub(r'\1', line)
+
+    def append_cmdinfos():
+        "append command information to level descriptions"
+        for lvlname, level in _LEVELS.iteritems():
+            if lvlname in _COMMANDS:
+                level.long += "\n\nCommands:\n"
+                for cmdname, cmd in _COMMANDS[lvlname].iteritems():
+                    level.long += "\t" + _titleline(cmdname, cmd.short)
+
+    def fixup_root_commands():
+        "root commands appear as levels"
+
+        strip_topics = []
+        for tname, topic in _LEVELS.iteritems():
+            if not _COMMANDS.get(tname):
+                strip_topics.append(tname)
+        for t in strip_topics:
+            del _LEVELS[t]
+
+    def fixup_help_aliases():
+        "add help for aliases"
+
+        def add_help_for_alias(lvlname, command, alias):
+            if lvlname not in _COMMANDS:
+                return
+            if command not in _COMMANDS[lvlname]:
+                return
+            if alias in _COMMANDS[lvlname]:
+                return
+            info = _COMMANDS[lvlname][command]
+            _COMMANDS[lvlname][alias] = HelpEntry(info.short, info.long, (alias, command))
+
+        def add_aliases_for_level(lvl):
+            for name, info in lvl._children.iteritems():
+                for alias in info.aliases:
+                    add_help_for_alias(lvl.name, info.name, alias)
+                if info.level:
+                    add_aliases_for_level(info.level)
+        from ui_root import Root
+        add_aliases_for_level(Root)
+
+    try:
+        name = os.getenv("CRM_HELP_FILE") or HELP_FILE
+        helpfile = open(name, 'r')
+        entry = None
+        for line in helpfile:
+            if line.startswith('[['):
+                if entry is not None:
+                    process(entry)
+                entry = parse_header(line)
+            elif entry is not None and line.startswith('===') and entry['long']:
+                process(entry)
+                entry = None
+            elif entry is not None:
+                entry['long'] += filter_line(line)
+        if entry is not None:
+            process(entry)
+        helpfile.close()
+        append_cmdinfos()
+        fixup_root_commands()
+        fixup_help_aliases()
+    except IOError, msg:
+        common_err("Help text not found! %s" % (msg))
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/idmgmt.py b/modules/idmgmt.py
new file mode 100644
index 0000000..24f988f
--- /dev/null
+++ b/modules/idmgmt.py
@@ -0,0 +1,203 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import constants
+import copy
+from msg import common_error, id_used_err
+import xmlutil
+
+
+'''
+Make sure that ids are unique.
+'''
+_id_store = {}
+_state = []
+ok = True  # error var
+
+
+def push_state():
+    _state.append(copy.deepcopy(_id_store))
+
+
+def pop_state():
+    try:
+        global _id_store
+        _id_store = _state.pop()
+        return True
+    except IndexError:
+        return False
+
+
+def drop_state():
+    try:
+        _state.pop()
+    except KeyError:
+        pass
+
+
+def clean_state():
+    global _state
+    _state = []
+
+
+def new(node, pfx):
+    '''
+    Create a unique id for the xml node.
+    '''
+    name = node.get("name")
+    if node.tag == "nvpair":
+        node_id = "%s-%s" % (pfx, name)
+    elif node.tag == "op":
+        interval = node.get("interval")
+        if interval:
+            node_id = "%s-%s-%s" % (pfx, name, interval)
+        else:
+            node_id = "%s-%s" % (pfx, name)
+    else:
+        subpfx = constants.subpfx_list.get(node.tag, '')
+        if subpfx:
+            node_id = "%s-%s" % (pfx, subpfx)
+        else:
+            node_id = pfx
+    if is_used(node_id):
+        node_id = _gen_free_id(node_id)
+    save(node_id)
+    return node_id
+
+
+def _gen_free_id(node_id):
+    "generate a unique id"
+    # shouldn't really get here
+    for cnt in range(99):
+        try_id = "%s-%d" % (node_id, cnt)
+        if not is_used(try_id):
+            node_id = try_id
+            break
+    return node_id
+
+
+def check_node(node, lvl):
+    global ok
+    node_id = node.get("id")
+    if not node_id:
+        return
+    if id_in_use(node_id):
+        common_error("id_store: id %s is in use" % node_id)
+        ok = False
+        return
+
+
+def _store_node(node, lvl):
+    save(node.get("id"))
+
+
+def _drop_node(node, lvl):
+    remove(node.get("id"))
+
+
+def check_xml(node):
+    global ok
+    ok = True
+    xmlutil.xmltraverse_thin(node, check_node)
+    return ok
+
+
+def store_xml(node):
+    if not check_xml(node):
+        return False
+    xmlutil.xmltraverse_thin(node, _store_node)
+    return True
+
+
+def remove_xml(node):
+    xmlutil.xmltraverse_thin(node, _drop_node)
+
+
+def replace_xml(oldnode, newnode):
+    remove_xml(oldnode)
+    if not store_xml(newnode):
+        store_xml(oldnode)
+        return False
+    return True
+
+
+def is_used(node_id):
+    return node_id in _id_store
+
+
+def id_in_use(obj_id):
+    if is_used(obj_id):
+        id_used_err(obj_id)
+        return True
+    return False
+
+
+def save(node_id):
+    if not node_id:
+        return
+    _id_store[node_id] = 1
+
+
+def rename(old_id, new_id):
+    if not old_id or not new_id:
+        return
+    if not is_used(old_id):
+        return
+    if is_used(new_id):
+        return
+    remove(old_id)
+    save(new_id)
+
+
+def remove(node_id):
+    if not node_id:
+        return
+    try:
+        del _id_store[node_id]
+    except KeyError:
+        pass
+
+
+def clear():
+    global _id_store
+    global _state
+    _id_store = {}
+    _state = []
+
+
+def set(node, oldnode, id_hint, id_required=True):
+    '''
+    Set the id attribute for the node.
+    - if the node already contains "id", keep it
+    - if the old node contains "id", copy that
+    - if the node contains "uname", copy that
+    - else if required, create a new one using id_hint
+    - save the new id in idmgmt.
+    '''
+    old_id = oldnode.get("id") if oldnode is not None else None
+    new_id = node.get("id") or old_id or node.get("uname")
+    if new_id:
+        save(new_id)
+    elif id_required:
+        new_id = new(node, id_hint)
+    if new_id:
+        node.set("id", new_id)
+        if oldnode is not None and old_id == new_id:
+            xmlutil.set_id_used_attr(oldnode)
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/log_patterns.py b/modules/log_patterns.py
new file mode 100644
index 0000000..12fe469
--- /dev/null
+++ b/modules/log_patterns.py
@@ -0,0 +1,77 @@
+# Copyright (C) 2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# log pattern specification
+#
+# patterns are grouped one of several classes:
+#  - resource: pertaining to a resource
+#  - node: pertaining to a node
+#  - quorum: quorum changes
+#  - events: other interesting events (core dumps, etc)
+#
+# paterns are grouped based on a detail level
+# detail level 0 is the lowest, i.e. should match the least
+# number of relevant messages
+
+# NB:
+# %% stands for whatever user input we get, for instance a
+# resource name or node name or just some regular expression
+# in optimal case, it should be surrounded by literals
+#
+# [Note that resources may contain clone numbers!]
+
+log_patterns = {
+	"resource": (
+		( # detail 0
+			"lrmd.*rsc:%% (?:start|stop|promote|demote|migrate)",
+			"lrmd.*RA output: .%%:.*:stderr",
+			"lrmd.*WARN: Managed %%:.*exited",
+			"lrmd.*WARN: .* %% .*timed out$",
+			"crmd.*process_lrm_event: LRM operation %%_(?:start|stop|promote|demote|migrate)_.*confirmed=true",
+			"crmd.*process_lrm_event: LRM operation %%_.*Timed Out",
+			"[(]%%[)][[]",
+		),
+		( # detail 1
+			"lrmd.*rsc:%% (?:probe|notify)",
+			"lrmd.*info: Managed %%:.*exited",
+		),
+	),
+	"node": (
+		( # detail 0
+			" %% .*Corosync.Cluster.Engine",
+			" %% .*Executive.Service.RELEASE",
+			" %% .*crm_shutdown:.Requesting.shutdown",
+			" %% .*pcmk_shutdown:.Shutdown.complete",
+			" %% .*Configuration.validated..Starting.heartbeat",
+			"pengine.*Scheduling Node %% for STONITH",
+			"crmd.* tengine_stonith_callback: .* of %% failed",
+			"stonith-ng.*log_operation:.*host '%%'",
+			"te_fence_node: Exec.*on %% ",
+			"pe_fence_node: Node %% will be fenced",
+			"stonith-ng.*remote_op_timeout:.*for %% timed",
+			"stonith-ng.*can_fence_host_with_device:.*can not fence %%:",
+			"stonithd.*Succeeded.*node %%:",
+			"pcmk_peer_update.*(?:lost|memb): %% ",
+			"crmd.*ccm_event.*(?:NEW|LOST):.* %% ",
+			"Node return implies stonith of %% ",
+		),
+		( # detail 1
+		),
+	),
+	"quorum": (
+		( # detail 0
+			"crmd.*crm_update_quorum:.Updating.quorum.status",
+			"crmd.*ais.disp.*quorum.(?:lost|ac?quir)",
+		),
+		( # detail 1
+		),
+	),
+	"events": (
+		( # detail 0
+			"CRIT:",
+			"ERROR:",
+		),
+		( # detail 1
+			"WARN:",
+		),
+	),
+}
diff --git a/modules/log_patterns_118.py b/modules/log_patterns_118.py
new file mode 100644
index 0000000..30834a9
--- /dev/null
+++ b/modules/log_patterns_118.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2012 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# log pattern specification (for pacemaker v1.1.8)
+#
+# patterns are grouped one of several classes:
+#  - resource: pertaining to a resource
+#  - node: pertaining to a node
+#  - quorum: quorum changes
+#  - events: other interesting events (core dumps, etc)
+#
+# paterns are grouped based on a detail level
+# detail level 0 is the lowest, i.e. should match the least
+# number of relevant messages
+
+# NB:
+# %% stands for whatever user input we get, for instance a
+# resource name or node name or just some regular expression
+# in optimal case, it should be surrounded by literals
+#
+# [Note that resources may contain clone numbers!]
+
+log_patterns = {
+	"resource": (
+		( # detail 0
+			"crmd.*Initiating.*%%_(?:start|stop|promote|demote|migrate)_",
+			"lrmd.*operation_finished: %%_",
+			"crmd.*process_lrm_event: LRM operation %%_(?:start|stop|promote|demote|migrate)_.*confirmed=true",
+			"crmd.*process_lrm_event: LRM operation %%_.*Timed Out",
+			"[(]%%[)][[]",
+		),
+		( # detail 1
+			"crmd.*Initiating%%_(?:monitor_0|notify)",
+		),
+	),
+	"node": (
+		( # detail 0
+			" %% .*Corosync.Cluster.Engine",
+			" %% .*Executive.Service.RELEASE",
+			" %% .*crm_shutdown:.Requesting.shutdown",
+			" %% .*pcmk_shutdown:.Shutdown.complete",
+			" %% .*Configuration.validated..Starting.heartbeat",
+			"pengine.*Scheduling Node %% for STONITH",
+			"pengine.*pe_fence_node: Node %% will be fenced",
+			"crmd.* tengine_stonith_callback: .* for %% failed",
+			"stonith-ng.*log_operation:.*host '%%'",
+			"te_fence_node: Exec.*on %% ",
+			"pe_fence_node: Node %% will be fenced",
+			"stonith-ng.*remote_op_timeout.*on %% for.*timed out",
+			"stonith-ng.*can_fence_host_with_device:.*can not fence %%:",
+			"stonithd.*Succeeded.*node %%:",
+			"pcmk_peer_update.*(?:lost|memb): %% ",
+			"crmd.*ccm_event.*(?:NEW|LOST):.* %% ",
+			"Node return implies stonith of %% ",
+		),
+		( # detail 1
+		),
+	),
+	"quorum": (
+		( # detail 0
+			"crmd.*crm_update_quorum:.Updating.quorum.status",
+			"crmd.*ais.disp.*quorum.(?:lost|ac?quir)",
+		),
+		( # detail 1
+		),
+	),
+	"events": (
+		( # detail 0
+			"(?:CRIT|crit):",
+			"(?:ERROR|error):",
+		),
+		( # detail 1
+			"(?:WARN|warning):",
+		),
+	),
+}
diff --git a/modules/main.py b/modules/main.py
new file mode 100644
index 0000000..d0ab271
--- /dev/null
+++ b/modules/main.py
@@ -0,0 +1,389 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import sys
+import os
+import getopt
+import atexit
+import random
+
+import config
+import options
+import constants
+from msg import err_buf, common_err
+import clidisplay
+import term
+import utils
+import userdir
+
+import ui_root
+import ui_context
+
+
+random.seed()
+
+
+def load_rc(context, rcfile):
+    # only load the RC file if there is no new-style user config
+    if config.has_user_config():
+        return
+
+    try:
+        f = open(rcfile)
+    except:
+        return
+    save_stdin = sys.stdin
+    sys.stdin = f
+    while True:
+        inp = utils.multi_input()
+        if inp is None:
+            break
+        try:
+            if not context.run(inp):
+                raise ValueError("Error in RC file: " + rcfile)
+        except ValueError, msg:
+            common_err(msg)
+    f.close()
+    sys.stdin = save_stdin
+
+
+def exit_handler():
+    '''
+    Write the history file. Remove tmp files.
+    '''
+    if options.interactive and not options.batch:
+        try:
+            from readline import write_history_file
+            write_history_file(userdir.HISTORY_FILE)
+        except:
+            pass
+
+
+# prefer the user set PATH
+def envsetup():
+    mybinpath = os.path.dirname(sys.argv[0])
+    for p in mybinpath, config.path.crm_daemon_dir:
+        if p not in os.environ["PATH"].split(':'):
+            os.environ['PATH'] = "%s:%s" % (os.environ['PATH'], p)
+
+
+# three modes: interactive (no args supplied), batch (input from
+# a file), half-interactive (args supplied, but not batch)
+def cib_prompt():
+    shadow = utils.get_cib_in_use()
+    if not shadow:
+        return constants.live_cib_prompt
+    if constants.tmp_cib:
+        return constants.tmp_cib_prompt
+    return shadow
+
+
+def usage(rc):
+    f = sys.stderr
+    if rc == 0:
+        f = sys.stdout
+    print >> f, """Usage: crm [OPTIONS] [SUBCOMMAND ARGS...]
+
+    -f, --file='FILE'::
+        Load commands from the given file. If a dash `-` is used in place
+        of a file name, `crm` will read commands from the shell standard
+        input (`stdin`).
+
+    -c, --cib='CIB'::
+        Start the session using the given shadow CIB file.
+        Equivalent to `cib use <CIB>`.
+
+    -D, --display='OUTPUT_TYPE'::
+        Choose one of the output options: plain, color, or
+        uppercase. The default is color if the terminal emulation
+        supports colors. Otherwise, plain is used.
+
+    -F, --force::
+        Make `crm` proceed with applying changes where it would normally
+        ask the user to confirm before proceeding. This option is mainly
+        useful in scripts, and should be used with care.
+
+    -w, --wait::
+        Make crm wait for the cluster transition to finish (for the
+        changes to take effect) after each processed line.
+
+    -H, --history='DIR|FILE'::
+        The `history` commands can either work directly on the live
+        cluster (default), or on a report generated by the `report`
+        command. Use this option to specify a directory or file containing
+        the previously generated report.
+
+    -h, --help::
+        Print help page.
+
+    --version::
+        Print crmsh version and build information (Mercurial Hg changeset
+        hash).
+
+    -d, --debug::
+        Print verbose debugging information.
+
+    -R, --regression-tests::
+        Enables extra verbose trace logging used by the regression
+        tests. Logs all external calls made by crmsh.
+
+    --scriptdir='DIR'::
+        Extra directory where crm looks for cluster scripts. Can be
+        a semicolon-separated list of directories.
+
+    Use crm without arguments for an interactive session.
+    Supply one or more arguments for a "single-shot" use.
+    Supply level name to start working at that level.
+    Specify with -f a file which contains a script. Use '-' for
+    standard input or use pipe/redirection.
+
+    Examples:
+
+        # crm -f stopapp2.txt
+        # crm -w resource stop global_www
+        # echo stop global_www | crm resource
+        # crm configure property no-quorum-policy=ignore
+        # crm ra info pengine
+        # crm status
+
+    See the crm(8) man page or the crm help system for more details.
+    """
+    sys.exit(rc)
+
+
+def set_interactive():
+    '''Set the interactive option only if we're on a tty.'''
+    if utils.can_ask():
+        options.interactive = True
+
+
+def compatibility_setup():
+    if not utils.is_pcmk_118():
+        del constants.attr_defaults["node"]
+        constants.cib_no_section_rc = 22
+
+
+def add_quotes(args):
+    '''
+    Add quotes if there's whitespace in one of the
+    arguments; so that the user doesn't need to protect the
+    quotes.
+
+    If there are two kinds of quotes which actually _survive_
+    the getopt, then we're _probably_ screwed.
+
+    At any rate, stuff like ... '..."..."'
+    as well as '...\'...\''  do work.
+    '''
+    l = []
+    for s in args:
+        if config.core.add_quotes and ' ' in s:
+            q = '"' in s and "'" or '"'
+            if q not in s:
+                s = "%s%s%s" % (q, s, q)
+        l.append(s)
+    return l
+
+
+def do_work(context, user_args):
+    compatibility_setup()
+
+    if options.shadow:
+        if not context.run("cib use " + options.shadow):
+            return 1
+
+    # this special case is silly, but we have to keep it to
+    # preserve the backward compatibility
+    if len(user_args) == 1 and user_args[0].startswith("conf"):
+        if not context.run("configure"):
+            return 1
+    elif len(user_args) > 0:
+        # we're not sure yet whether it's an interactive session or not
+        # (single-shot commands aren't)
+        err_buf.reset_lineno()
+        options.interactive = False
+
+        l = add_quotes(user_args)
+        if context.run(' '.join(l)):
+            # if the user entered a level, then just continue
+            if not context.previous_level():
+                return 0
+            set_interactive()
+            if options.interactive:
+                err_buf.reset_lineno(-1)
+        else:
+            return 1
+
+    if options.input_file and options.input_file != "-":
+        try:
+            sys.stdin = open(options.input_file)
+        except IOError, msg:
+            common_err(msg)
+            usage(2)
+
+    if options.interactive and not options.batch:
+        context.setup_readline()
+
+    rc = 0
+    while True:
+        try:
+            rendered_prompt = constants.prompt
+            if options.interactive and not options.batch:
+                # TODO: fix how color interacts with readline,
+                # seems the color prompt messes it up
+                promptstr = "crm(%s)%s# " % (cib_prompt(), context.prompt())
+                constants.prompt = promptstr
+                if clidisplay.colors_enabled():
+                    rendered_prompt = term.render(clidisplay.prompt(promptstr))
+                else:
+                    rendered_prompt = promptstr
+            inp = utils.multi_input(rendered_prompt)
+            if inp is None:
+                if options.interactive:
+                    rc = 0
+                context.quit(rc)
+            try:
+                if not context.run(inp):
+                    rc = 1
+            except ValueError, msg:
+                rc = 1
+                common_err(msg)
+        except KeyboardInterrupt:
+            if options.interactive and not options.batch:
+                print "Ctrl-C, leaving"
+            context.quit(1)
+    return rc
+
+
+def compgen():
+    args = sys.argv[2:]
+    if len(args) < 2:
+        return
+
+    options.shell_completion = True
+
+    #point = int(args[0])
+    line = args[1]
+
+    # remove crm from commandline
+    line_split = line.split(' ', 1)
+    if len(line_split) == 1:
+        return
+    line = line_split[1].lstrip()
+
+    options.interactive = False
+    ui = ui_root.Root()
+    context = ui_context.Context(ui)
+    last_word = line.rsplit(' ', 1)
+    if len(last_word) > 1 and ':' in last_word[1]:
+        idx = last_word[1].rfind(':')
+        for w in context.complete(line):
+            print w[idx+1:]
+    else:
+        for w in context.complete(line):
+            print w
+
+
+def parse_options():
+    try:
+        opts, user_args = getopt.getopt(
+            sys.argv[1:],
+            'whdc:f:FX:RD:H:',
+            ("wait", "version", "help", "debug",
+             "cib=", "file=", "force", "profile=",
+             "regression-tests", "display=", "history=",
+             "scriptdir="))
+        for o, p in opts:
+            if o in ("-h", "--help"):
+                usage(0)
+            elif o == "--version":
+                print >> sys.stdout, ("%s" % config.CRM_VERSION)
+                sys.exit(0)
+            elif o == "-d":
+                config.core.debug = "yes"
+            elif o == "-X":
+                options.profile = p
+            elif o == "-R":
+                options.regression_tests = True
+            elif o in ("-D", "--display"):
+                config.color.style = p
+            elif o in ("-F", "--force"):
+                config.core.force = "yes"
+            elif o in ("-f", "--file"):
+                options.batch = True
+                options.interactive = False
+                err_buf.reset_lineno()
+                options.input_file = p
+            elif o in ("-H", "--history"):
+                options.history = p
+            elif o in ("-w", "--wait"):
+                config.core.wait = "yes"
+            elif o in ("-c", "--cib"):
+                options.shadow = p
+            elif o == "--scriptdir":
+                options.scriptdir = p
+        return user_args
+    except getopt.GetoptError, msg:
+        print msg
+        usage(1)
+
+
+def profile_run(context, user_args):
+    import cProfile
+    cProfile.runctx('do_work(context, user_args)',
+                    globals(),
+                    {'context': context, 'user_args': user_args},
+                    filename=options.profile)
+    # print how to use the profile file, but don't disturb
+    # the regression tests
+    if not options.regression_tests:
+        stats_cmd = "; ".join(['import pstats',
+                               's = pstats.Stats("%s")' % options.profile,
+                               's.sort_stats("cumulative").print_stats()'])
+        print "python -c '%s' | less" % (stats_cmd)
+    return 0
+
+
+def run():
+    try:
+        if len(sys.argv) >= 2 and sys.argv[1] == '--compgen':
+            compgen()
+            return 0
+        envsetup()
+        userdir.mv_user_files()
+
+        ui = ui_root.Root()
+        context = ui_context.Context(ui)
+
+        load_rc(context, userdir.RC_FILE)
+        atexit.register(exit_handler)
+        options.interactive = utils.can_ask()
+        if not options.interactive:
+            err_buf.reset_lineno()
+            options.batch = True
+        user_args = parse_options()
+        if options.profile:
+            return profile_run(context, user_args)
+        else:
+            return do_work(context, user_args)
+    except KeyboardInterrupt:
+        print "Ctrl-C, leaving"
+        sys.exit(1)
+    except ValueError, e:
+        common_err(str(e))
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/msg.py b/modules/msg.py
new file mode 100644
index 0000000..c216c15
--- /dev/null
+++ b/modules/msg.py
@@ -0,0 +1,283 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import sys
+from lxml import etree
+import config
+import clidisplay
+import options
+
+ERR_STREAM = sys.stderr
+
+
+class ErrorBuffer(object):
+    '''
+    Show error messages either immediately or buffered.
+    '''
+    def __init__(self):
+        try:
+            import term
+            self._term = term
+        except:
+            self._term = None
+        self.msg_list = []
+        self.mode = "immediate"
+        self.lineno = -1
+        self.written = {}
+
+    def buffer(self):
+        self.mode = "keep"
+
+    def release(self):
+        if self.msg_list:
+            if ERR_STREAM:
+                print >> ERR_STREAM, '\n'.join(self.msg_list)
+            else:
+                print '\n'.join(self.msg_list)
+            if not options.batch:
+                try:
+                    raw_input("Press enter to continue... ")
+                except EOFError:
+                    pass
+            self.msg_list = []
+        self.mode = "immediate"
+
+    def writemsg(self, msg, to=None):
+        if to is None:
+            to = ERR_STREAM
+        if msg.endswith('\n'):
+            msg = msg[:-1]
+        if self.mode == "immediate":
+            if options.regression_tests or not to:
+                print msg
+            else:
+                print >> to, msg
+        else:
+            self.msg_list.append(msg)
+
+    def reset_lineno(self, to=0):
+        self.lineno = to
+
+    def incr_lineno(self):
+        if self.lineno >= 0:
+            self.lineno += 1
+
+    def start_tmp_lineno(self):
+        self._save_lineno = self.lineno
+        self.reset_lineno()
+
+    def stop_tmp_lineno(self):
+        self.lineno = self._save_lineno
+
+    def add_lineno(self, s):
+        if self.lineno > 0:
+            return "%d: %s" % (self.lineno, s)
+        else:
+            return s
+
+    def _prefix(self, pfx, s, to=None):
+        self.writemsg(self._render("%s: %s" % (pfx, self.add_lineno(s))), to=to)
+
+    def ok(self, s):
+        self._prefix(clidisplay.ok("OK"), s, to=sys.stdout)
+
+    def error(self, s):
+        self._prefix(clidisplay.error("ERROR"), s)
+
+    def warning(self, s):
+        self._prefix(clidisplay.warn("WARNING"), s)
+
+    def one_warning(self, s):
+        if s not in self.written:
+            self.written[s] = 1
+            self.writemsg(self._render(clidisplay.warn("WARNING")) + ": %s" %
+                          self.add_lineno(s))
+
+    def info(self, s):
+        self._prefix(clidisplay.info("INFO"), s)
+
+    def debug(self, s):
+        if config.core.debug:
+            self._prefix("DEBUG", s)
+
+    def _render(self, s):
+        'Render for TERM.'
+        if self._term:
+            return self._term.render(s)
+        return s
+
+
+def common_error(s):
+    err_buf.error(s)
+
+
+def common_err(s):
+    err_buf.error(s)
+
+
+def common_warning(s):
+    err_buf.warning(s)
+
+
+def common_warn(s):
+    err_buf.warning(s)
+
+
+def warn_once(s):
+    err_buf.one_warning(s)
+
+
+def common_info(s):
+    err_buf.info(s)
+
+
+def common_debug(s):
+    err_buf.debug(s)
+
+
+def no_prog_err(name):
+    err_buf.error("%s not available, check your installation" % name)
+
+
+def no_file_err(name):
+    err_buf.error("%s does not exist" % name)
+
+
+def missing_prog_warn(name):
+    err_buf.warning("could not find any %s on the system" % name)
+
+
+def node_err(msg, node):
+    err_buf.error("%s: %s" % (msg, etree.tostring(node, pretty_print=True)))
+
+
+def node_debug(msg, node):
+    err_buf.debug("%s: %s" % (msg, etree.tostring(node, pretty_print=True)))
+
+
+def no_attribute_err(attr, obj_type):
+    err_buf.error("required attribute %s not found in %s" % (attr, obj_type))
+
+
+def bad_def_err(what, msg):
+    err_buf.error("bad %s definition: %s" % (what, msg))
+
+
+def unsupported_err(name):
+    err_buf.error("%s is not supported" % name)
+
+
+def no_such_obj_err(name):
+    err_buf.error("%s object is not supported" % name)
+
+
+def missing_obj_err(node):
+    err_buf.error("object %s:%s missing (shouldn't have happened)" %
+                  (node.tag, node.get("id")))
+
+
+def constraint_norefobj_err(constraint_id, obj_id):
+    err_buf.error("constraint %s references a resource %s which doesn't exist" %
+                  (constraint_id, obj_id))
+
+
+def obj_exists_err(name):
+    err_buf.error("object %s already exists" % name)
+
+
+def no_object_err(name):
+    err_buf.error("object %s does not exist" % name)
+
+
+def invalid_id_err(obj_id):
+    err_buf.error("%s: invalid object id" % obj_id)
+
+
+def id_used_err(node_id):
+    err_buf.error("%s: id is already in use" % node_id)
+
+
+def skill_err(s):
+    err_buf.error("%s: this command is not allowed at this skill level" % s)
+
+
+def syntax_err(s, token='', context='', msg=''):
+    err = "syntax"
+    if context:
+        err += " in "
+        err += context
+    if msg:
+        err += ": %s" % (msg)
+    if isinstance(s, basestring):
+        err += " parsing '%s'" % (s)
+    elif token:
+        err += " near <%s> parsing '%s'" % (token, ' '.join(s))
+    else:
+        err += " parsing '%s'" % (' '.join(s))
+    err_buf.error(err)
+
+
+def bad_usage(cmd, args, msg=None):
+    if not msg:
+        err_buf.error("Bad usage: '%s %s'" % (cmd, args))
+    else:
+        err_buf.error("Bad usage: %s, command: '%s %s'" % (msg, cmd, args))
+
+
+def empty_cib_err():
+    err_buf.error("No CIB!")
+
+
+def cib_parse_err(msg, s):
+    err_buf.error("%s" % msg)
+    err_buf.info("offending string: %s" % s)
+
+
+def cib_no_elem_err(el_name):
+    err_buf.error("CIB contains no '%s' element!" % el_name)
+
+
+def cib_ver_unsupported_err(validator, rel):
+    err_buf.error("CIB not supported: validator '%s', release '%s'" %
+                  (validator, rel))
+    err_buf.error("You may try the upgrade command")
+
+
+def update_err(obj_id, cibadm_opt, xml, rc):
+    if cibadm_opt == '-U':
+        task = "update"
+    elif cibadm_opt == '-D':
+        task = "delete"
+    elif cibadm_opt == '-P':
+        task = "patch"
+    else:
+        task = "replace"
+    err_buf.error("could not %s %s (rc=%d)" % (task, obj_id, rc))
+    if rc == 54:
+        err_buf.info("Permission denied.")
+    elif task == "patch":
+        err_buf.info("offending xml diff: %s" % xml)
+    else:
+        err_buf.info("offending xml: %s" % xml)
+
+
+def not_impl_info(s):
+    err_buf.info("%s is not implemented yet" % s)
+
+
+err_buf = ErrorBuffer()
+# vim:ts=4:sw=4:et:
diff --git a/modules/options.py b/modules/options.py
new file mode 100644
index 0000000..4769021
--- /dev/null
+++ b/modules/options.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+'''
+Session-only options (not saved).
+'''
+
+interactive = False
+batch = False
+regression_tests = False
+profile = ""
+history = "live"
+input_file = ""
+shadow = ""
+scriptdir = ""
+# set to true when completing non-interactively
+shell_completion = False
diff --git a/modules/ordereddict.py b/modules/ordereddict.py
new file mode 100644
index 0000000..198fa30
--- /dev/null
+++ b/modules/ordereddict.py
@@ -0,0 +1,130 @@
+# Copyright (c) 2009 Raymond Hettinger
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+#     The above copyright notice and this permission notice shall be
+#     included in all copies or substantial portions of the Software.
+#
+#     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+#     EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+#     OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+#     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+#     HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+#     WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+#     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+#     OTHER DEALINGS IN THE SOFTWARE.
+
+from UserDict import DictMixin
+
+
+class OrderedDict(dict, DictMixin):
+
+    def __init__(self, *args, **kwds):
+        if len(args) > 1:
+            raise TypeError('expected at most 1 arguments, got %d' % len(args))
+        try:
+            self.__end
+        except AttributeError:
+            self.clear()
+        self.update(*args, **kwds)
+
+    def clear(self):
+        self.__end = end = []
+        end += [None, end, end]         # sentinel node for doubly linked list
+        self.__map = {}                 # key --> [key, prev, next]
+        dict.clear(self)
+
+    def __setitem__(self, key, value):
+        if key not in self:
+            end = self.__end
+            curr = end[1]
+            curr[2] = end[1] = self.__map[key] = [key, curr, end]
+        dict.__setitem__(self, key, value)
+
+    def __delitem__(self, key):
+        dict.__delitem__(self, key)
+        key, prev, next = self.__map.pop(key)
+        prev[2] = next
+        next[1] = prev
+
+    def __iter__(self):
+        end = self.__end
+        curr = end[2]
+        while curr is not end:
+            yield curr[0]
+            curr = curr[2]
+
+    def __reversed__(self):
+        end = self.__end
+        curr = end[1]
+        while curr is not end:
+            yield curr[0]
+            curr = curr[1]
+
+    def popitem(self, last=True):
+        if not self:
+            raise KeyError('dictionary is empty')
+        if last:
+            key = reversed(self).next()
+        else:
+            key = iter(self).next()
+        value = self.pop(key)
+        return key, value
+
+    def __reduce__(self):
+        items = [[k, self[k]] for k in self]
+        tmp = self.__map, self.__end
+        del self.__map, self.__end
+        inst_dict = vars(self).copy()
+        self.__map, self.__end = tmp
+        if inst_dict:
+            return (self.__class__, (items,), inst_dict)
+        return self.__class__, (items,)
+
+    def keys(self):
+        return list(self)
+
+    setdefault = DictMixin.setdefault
+    update = DictMixin.update
+    pop = DictMixin.pop
+    values = DictMixin.values
+    items = DictMixin.items
+    iterkeys = DictMixin.iterkeys
+    itervalues = DictMixin.itervalues
+    iteritems = DictMixin.iteritems
+
+    def __repr__(self):
+        if not self:
+            return '%s()' % (self.__class__.__name__,)
+        return '%s(%r)' % (self.__class__.__name__, self.items())
+
+    def copy(self):
+        return self.__class__(self)
+
+    @classmethod
+    def fromkeys(cls, iterable, value=None):
+        d = cls()
+        for key in iterable:
+            d[key] = value
+        return d
+
+    def __eq__(self, other):
+        if isinstance(other, OrderedDict):
+            if len(self) != len(other):
+                return False
+            for p, q in zip(self.items(), other.items()):
+                if p != q:
+                    return False
+            return True
+        return dict.__eq__(self, other)
+
+    def __ne__(self, other):
+        return not self == other
+
+odict = OrderedDict
diff --git a/modules/orderedset.py b/modules/orderedset.py
new file mode 100644
index 0000000..0464ca0
--- /dev/null
+++ b/modules/orderedset.py
@@ -0,0 +1,98 @@
+# Copyright (C) 2009 Raymond Hettinger
+
+#                          *** MIT License ***
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is furnished to do
+# so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+# {{{ http://code.activestate.com/recipes/576694/ (r7)
+
+import collections
+
+KEY, PREV, NEXT = range(3)
+
+
+class OrderedSet(collections.MutableSet):
+
+    def __init__(self, iterable=None):
+        self.end = end = []
+        end += [None, end, end]         # sentinel node for doubly linked list
+        self.map = {}                   # key --> [key, prev, next]
+        if iterable is not None:
+            self |= iterable
+
+    def __len__(self):
+        return len(self.map)
+
+    def __contains__(self, key):
+        return key in self.map
+
+    def add(self, key):
+        if key not in self.map:
+            end = self.end
+            curr = end[PREV]
+            curr[NEXT] = end[PREV] = self.map[key] = [key, curr, end]
+
+    def discard(self, key):
+        if key in self.map:
+            key, prev, next = self.map.pop(key)
+            prev[NEXT] = next
+            next[PREV] = prev
+
+    def __iter__(self):
+        end = self.end
+        curr = end[NEXT]
+        while curr is not end:
+            yield curr[KEY]
+            curr = curr[NEXT]
+
+    def __reversed__(self):
+        end = self.end
+        curr = end[PREV]
+        while curr is not end:
+            yield curr[KEY]
+            curr = curr[PREV]
+
+    def pop(self, last=True):
+        # changed default to last=False - by default, treat as queue.
+        if not self:
+            raise KeyError('set is empty')
+        key = next(reversed(self)) if last else next(iter(self))
+        self.discard(key)
+        return key
+
+    def __repr__(self):
+        if not self:
+            return '%s()' % (self.__class__.__name__,)
+        return '%s(%r)' % (self.__class__.__name__, list(self))
+
+    def __eq__(self, other):
+        if isinstance(other, OrderedSet):
+            return len(self) == len(other) and list(self) == list(other)
+        return set(self) == set(other)
+
+    def __del__(self):
+        self.clear()                    # remove circular references
+
+
+oset = OrderedSet
+
+if __name__ == '__main__':
+    print(OrderedSet('abracadaba'))
+    print(OrderedSet('simsalabim'))
+
+# end of http://code.activestate.com/recipes/576694/ }}}
diff --git a/modules/pacemaker.py b/modules/pacemaker.py
new file mode 100644
index 0000000..521374d
--- /dev/null
+++ b/modules/pacemaker.py
@@ -0,0 +1,398 @@
+# Copyright (C) 2009 Yan Gao <ygao at novell.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import tempfile
+import copy
+import re
+from lxml import etree
+
+
+class PacemakerError(Exception):
+    '''PacemakerError exceptions'''
+
+
+def get_validate_name(cib_elem):
+    if cib_elem is not None:
+        return cib_elem.get("validate-with")
+    else:
+        return None
+
+
+def get_validate_type(cib_elem):
+    validate_name = get_validate_name(cib_elem)
+    if re.match(r"pacemaker-\d+\.\d+", validate_name):
+        return "rng"
+    return None
+
+
+def get_schema_filename(validate_name):
+    if re.match(r"pacemaker-\d+\.\d+", validate_name):
+        return "%s.rng" % (validate_name)
+    return None
+
+
+def read_schema_local(validate_name, file_path):
+    try:
+        f = open(file_path)
+        schema = f.read()
+    except IOError, msg:
+        raise PacemakerError("Cannot read the schema file: " + str(msg))
+
+    f.close()
+    return schema
+
+
+def delete_dir(dir_path):
+    real_path = os.path.realpath(dir_path)
+    if real_path.count(os.sep) == len(real_path):
+        raise PacemakerError("Do not delete the root directory")
+
+    for root, dirs, files in os.walk(dir_path, False):
+        for name in files:
+            try:
+                os.unlink(os.path.join(root, name))
+            except OSError:
+                continue
+        for name in dirs:
+            try:
+                os.rmdir(os.path.join(root, name))
+            except OSError:
+                continue
+
+    os.rmdir(dir_path)
+
+
+def subset_select(sub_set, optional):
+    "Helper used to select attributes/elements based on subset and optional flag"
+    if sub_set == 'r':  # required
+        return not optional
+    if sub_set == 'o':  # optional
+        return optional
+    return True
+
+
+def CrmSchema(cib_elem, local_dir):
+    return RngSchema(cib_elem, local_dir)
+
+
+class Schema(object):
+    validate_name = None
+
+    def __init__(self, cib_elem, local_dir, is_local=True, get_schema_fn=None):
+        self.is_local = is_local
+        if get_schema_fn is not None:
+            self.get_schema_fn = get_schema_fn
+        else:
+            self.get_schema_fn = read_schema_local
+
+        self.local_dir = local_dir
+        self.refresh(cib_elem)
+        self.schema_str_docs = {}
+        self.schema_filename = None
+
+    def update_schema(self):
+        'defined in subclasses'
+        raise NotImplementedError
+
+    def find_elem(self, elem_name):
+        'defined in subclasses'
+        raise NotImplementedError
+
+    def refresh(self, cib_elem):
+        saved_validate_name = self.validate_name
+        self.validate_name = get_validate_name(cib_elem)
+        self.schema_filename = get_schema_filename(self.validate_name)
+        if self.validate_name != saved_validate_name:
+            return self.update_schema()
+
+    def validate_cib(self, new_cib_elem):
+        detail_msg = ""
+
+        if self.is_local:
+            schema_f = os.path.join(self.local_dir, self.schema_filename)
+        else:
+            try:
+                tmp_f = self.tmp_schema_f()
+            except EnvironmentError, msg:
+                raise PacemakerError("Cannot expand the Relax-NG schema: " + str(msg))
+            if tmp_f is None:
+                raise PacemakerError("Cannot expand the Relax-NG schema")
+            else:
+                schema_f = tmp_f
+
+        try:
+            cib_elem = etree.fromstring(etree.tostring(new_cib_elem))
+        except etree.Error, msg:
+            raise PacemakerError("Failed to parse the CIB XML: " + str(msg))
+
+        try:
+            schema = etree.RelaxNG(file=schema_f)
+
+        except etree.Error, msg:
+            raise PacemakerError("Failed to parse the Relax-NG schema: " + str(msg))
+        #try:
+        #   schema.assertValid(cib_elem)
+        #except etree.DocumentInvalid, err_msg:
+        #   print err_msg
+        #   print schema.error_log
+        try:
+            etree.clear_error_log()
+        except:
+            pass
+
+        is_valid = schema.validate(cib_elem)
+        if not is_valid:
+            for error_entry in schema.error_log:
+                detail_msg += error_entry.level_name + ": " + error_entry.message + "\n"
+
+        if not self.is_local:
+            try:
+                delete_dir(os.path.dirname(tmp_f))
+            except:
+                pass
+
+        return (is_valid, detail_msg)
+
+    def tmp_schema_f(self):
+        tmp_dir = tempfile.mkdtemp()
+        for schema_doc_name in self.schema_str_docs:
+            schema_doc_filename = os.path.join(tmp_dir, schema_doc_name)
+            fd = os.open(schema_doc_filename, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0644)
+
+            schema_doc_str = self.schema_str_docs[schema_doc_name]
+
+            os.write(fd, schema_doc_str)
+            os.close(fd)
+
+        if self.schema_filename in self.schema_str_docs:
+            return os.path.join(tmp_dir, self.schema_filename)
+        else:
+            return None
+
+    def get_sub_elems_by_obj(self, obj, sub_set='a'):
+        '''defined in subclasses'''
+        raise NotImplementedError
+
+    def get_elem_attrs_by_obj(self, obj, sub_set='a'):
+        '''defined in subclasses'''
+        raise NotImplementedError
+
+    # sub_set: 'a'(all), 'r'(required), 'o'(optional)
+    def get_elem_attrs(self, elem_name, sub_set='a'):
+        elem_obj = self.find_elem(elem_name)
+        if elem_obj is None:
+            return None
+        return self.get_elem_attrs_by_obj(elem_obj, sub_set)
+
+    # sub_set: 'a'(all), 'r'(required), 'o'(optional)
+    def get_sub_elems(self, elem_name, sub_set='a'):
+        elem_obj = self.find_elem(elem_name)
+        if elem_obj is None:
+            return None
+        return self.get_sub_elems_by_obj(elem_obj, sub_set)
+
+    def supported_rsc_types(self):
+        return self.get_sub_elems("resources")
+
+
+def get_local_tag(el):
+    return el.tag.replace("{%s}" % el.nsmap[None], "")
+
+
+class RngSchema(Schema):
+    expr = '//*[local-name() = $name]'
+
+    def __init__(self, cib_elem, local_dir, is_local=True, get_schema_fn=None):
+        self.rng_docs = {}
+        Schema.__init__(self, cib_elem, local_dir, is_local=is_local, get_schema_fn=get_schema_fn)
+
+    def update_schema(self):
+        self.rng_docs = {}
+        self.schema_str_docs = {}
+        self.update_rng_docs(self.validate_name, self.schema_filename)
+        return True
+
+    def update_rng_docs(self, validate_name="", file=""):
+        self.rng_docs[file] = self.find_start_rng_node(validate_name, file)
+        if self.rng_docs[file] is None:
+            return
+        for extern_ref in self.rng_docs[file][0].xpath(self.expr, name="externalRef"):
+            href_value = extern_ref.get("href")
+            if self.rng_docs.get(href_value) is None:
+                self.update_rng_docs(validate_name, href_value)
+
+    def find_start_rng_node(self, validate_name="", file=""):
+        schema_info = validate_name + " " + file
+        crm_schema = self.get_schema_fn(validate_name,
+                                        os.path.join(self.local_dir, file))
+        if not crm_schema:
+            raise PacemakerError("Cannot get the Relax-NG schema: " + schema_info)
+
+        self.schema_str_docs[file] = crm_schema
+
+        try:
+            grammar = etree.fromstring(crm_schema)
+        except Exception, msg:
+            raise PacemakerError("Failed to parse the Relax-NG schema: " + str(msg) + schema_info)
+
+        start_nodes = grammar.xpath(self.expr, name="start")
+        if len(start_nodes) > 0:
+            start_node = start_nodes[0]
+            return (grammar, start_node)
+        else:
+            raise PacemakerError("Cannot find the start in the Relax-NG schema: " + schema_info)
+
+    def find_in_grammar(self, grammar, node, name):
+        for elem_node in grammar.xpath(self.expr, name=node):
+            if elem_node.get("name") == name:
+                return elem_node
+        return None
+
+    def find_elem(self, elem_name):
+        elem_node = None
+        for (grammar, start_node) in self.rng_docs.values():
+            elem_node = self.find_in_grammar(grammar, 'element', elem_name)
+            if elem_node is not None:
+                return (grammar, elem_node)
+        return None
+
+    def rng_xpath(self, xpath, namespaces=None):
+        return [grammar.xpath(xpath, namespaces=namespaces)
+                for grammar, _ in self.rng_docs.values()]
+
+    def get_sub_rng_nodes(self, grammar, rng_node):
+        sub_rng_nodes = []
+        for child_node in rng_node.iterchildren():
+            if not isinstance(child_node.tag, basestring):
+                continue
+            local_tag = get_local_tag(child_node)
+            if local_tag == "ref":
+                def_node = self.find_in_grammar(grammar, 'define', child_node.get('name'))
+                if def_node is not None:
+                    sub_rng_nodes.extend(self.get_sub_rng_nodes(grammar, def_node))
+            elif local_tag == "externalRef":
+                nodes = self.get_sub_rng_nodes(*self.rng_docs[child_node.get("href")])
+                sub_rng_nodes.extend(nodes)
+            elif local_tag in ["element", "attribute", "value", "data", "text"]:
+                sub_rng_nodes.append([(grammar, child_node)])
+            elif local_tag in ["interleave", "optional", "zeroOrMore",
+                               "choice", "group", "oneOrMore"]:
+                nodes = self.get_sub_rng_nodes(grammar, child_node)
+                for node in nodes:
+                    node.append(copy.deepcopy(child_node))
+                sub_rng_nodes.extend(nodes)
+        return sub_rng_nodes
+
+    def sorted_sub_rng_nodes_by_name(self, obj_type):
+        rng_node = self.find_elem(obj_type)
+        if rng_node is None or rng_node[1] is None:
+            return None
+        return self.sorted_sub_rng_nodes_by_node(*rng_node)
+
+    def sorted_sub_rng_nodes_by_node(self, grammar, rng_node):
+        sub_rng_nodes = self.get_sub_rng_nodes(grammar, rng_node)
+        sorted_nodes = {}
+        for sub_rng_node in sub_rng_nodes:
+            name = get_local_tag(sub_rng_node[0][1])
+            if sorted_nodes.get(name) is None:
+                sorted_nodes[name] = []
+            sorted_nodes[name].append(sub_rng_node)
+        return sorted_nodes
+
+    def get_elem_attr_objs(self, obj_type):
+        return self.sorted_sub_rng_nodes_by_name(obj_type).get("attribute", [])
+
+    def get_sub_elem_objs(self, obj_type):
+        return self.sorted_sub_rng_nodes_by_name(obj_type).get("element", [])
+
+    def find_decl(self, rng_node, name, first=True):
+        decl_node_index = 0
+        for decl_node in rng_node[1:]:
+            if get_local_tag(decl_node) == name:
+                decl_node_index = rng_node.index(decl_node) - len(rng_node)
+                if first:
+                    break
+        return decl_node_index
+
+    def get_sorted_decl_nodes(self, decl_nodes_list, decl_type):
+        sorted_nodes = []
+        for rng_nodes in decl_nodes_list:
+            rng_node = rng_nodes.get(decl_type)
+            if rng_node is not None and rng_node not in sorted_nodes:
+                sorted_nodes.append(rng_node)
+        return sorted_nodes
+
+    def get_obj_name(self, rng_node):
+        return rng_node[0][1].get("name")
+
+    def get_attr_type(self, attr_rng_node):
+        sub_rng_nodes = self.sorted_sub_rng_nodes_by_node(*attr_rng_node[0])
+        for sub_rng_node in sub_rng_nodes.get("data", []):
+            return sub_rng_nodes["data"][0][0][1].get("type")
+
+        return None
+
+    def get_attr_values(self, attr_rng_node):
+        attr_values = []
+        sub_rng_nodes = self.sorted_sub_rng_nodes_by_node(*attr_rng_node[0])
+        for sub_rng_node in sub_rng_nodes.get("value", []):
+            #print etree.tostring(sub_rng_node[0][1])
+            #print sub_rng_node[0][1].text
+            #attr_values.append(sub_rng_node[0][1].getchildren()[0].data)
+            attr_values.append(sub_rng_node[0][1].text)
+
+        return attr_values
+
+    def get_attr_default(self, attr_rng_node):
+        return attr_rng_node[0][1].get("ann:defaultValue")
+
+    def _get_by_obj(self, rng_obj, typ, sub_set):
+        """
+        Used to select attributes or elements based on
+        sub_set selector and optionality.
+        typ: 'attribute' or 'element'
+        sub_set: 'a'(all), 'r'(required), 'o'(optional)
+        """
+        grammar, rng_node = rng_obj
+        if rng_node is None:
+            return None
+
+        selected = []
+        sub_rng_nodes = self.get_sub_rng_nodes(grammar, rng_node)
+        for node in sub_rng_nodes:
+            head = node[0][1]
+            if get_local_tag(head) != typ:
+                continue
+            name = head.get("name")
+            if selected.count(name):
+                continue
+            # the complicated case: 'choice'
+            #if self.find_decl(sub_rng_node, "choice") != 0:
+            optional = any(self.find_decl(node, opt) != 0
+                           for opt in ("optional", "zeroOrMore"))
+            if subset_select(sub_set, optional):
+                selected.append(name)
+        return selected
+
+    def get_elem_attrs_by_obj(self, rng_obj, sub_set='a'):
+        "sub_set: 'a'(all), 'r'(required), 'o'(optional)"
+        return self._get_by_obj(rng_obj, 'attribute', sub_set=sub_set)
+
+    def get_sub_elems_by_obj(self, rng_obj, sub_set='a'):
+        "sub_set: 'a'(all), 'r'(required), 'o'(optional)"
+        return self._get_by_obj(rng_obj, 'element', sub_set=sub_set)
diff --git a/modules/parse.py b/modules/parse.py
new file mode 100644
index 0000000..3633682
--- /dev/null
+++ b/modules/parse.py
@@ -0,0 +1,1600 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import shlex
+import re
+from lxml import etree
+import constants
+from ra import disambiguate_ra_type, ra_type_validate
+import schema
+from utils import keyword_cmp, verify_boolean, lines2cli
+from utils import get_boolean, olist, canonical_boolean
+from msg import common_err, syntax_err
+import xmlbuilder
+import xmlutil
+
+
+class ParseError(Exception):
+    '''
+    Raised by parsers when parsing fails.
+    No error message, parsers should write
+    error messages before raising the exception.
+    '''
+
+
+class BaseParser(object):
+    _NVPAIR_RE = re.compile(r'([^=@$][^=]*)=(.*)$')
+    _NVPAIR_ID_RE = re.compile(r'\$([^:=]+)(?::(.+))?=(.*)$')
+    _NVPAIR_REF_RE = re.compile(r'@([^:]+)(?::(.+))?$')
+    _NVPAIR_KEY_RE = re.compile(r'([^:=]+)$', re.IGNORECASE)
+    _IDENT_RE = re.compile(r'([a-z0-9_#$-][^=]*)$', re.IGNORECASE)
+    _DISPATCH_RE = re.compile(r'[a-z0-9_]+$', re.IGNORECASE)
+    _DESC_RE = re.compile(r'description=(.+)$', re.IGNORECASE)
+    _ATTR_RE = re.compile(r'\$?([^=]+)=(.*)$')
+    _RESOURCE_RE = re.compile(r'([a-z_#$][^=]*)$', re.IGNORECASE)
+    _IDSPEC_RE = re.compile(r'(\$id-ref|\$id)=(.*)$', re.IGNORECASE)
+    _ID_RE = re.compile(r'\$id=(.*)$', re.IGNORECASE)
+    _ID_NEW_RE = re.compile(r'([\w-]+):$', re.IGNORECASE)
+
+    def can_parse(self):
+        "Returns a list of commands this parser understands"
+        raise NotImplementedError
+
+    def parse(self, cmd):
+        "Called by do_parse(). Raises ParseError if parsing fails."
+        raise NotImplementedError
+
+    def init(self, validation):
+        self.validation = validation
+
+    def err(self, errmsg):
+        "Report a parse error and abort."
+        token = None
+        if self.has_tokens():
+            token = self._cmd[self._currtok]
+        syntax_err(self._cmd, context=self._cmd[0], token=token, msg=errmsg)
+        raise ParseError
+
+    def begin(self, cmd, min_args=-1):
+        self._cmd = cmd
+        self._currtok = 0
+        self._lastmatch = None
+        if min_args > -1 and len(cmd) < min_args + 1:
+            self.err("Expected at least %d arguments" % (min_args))
+
+    def begin_dispatch(self, cmd, min_args=-1):
+        """
+        Begin parsing cmd.
+        Dispatches to parse_<resource> based on the first token.
+        """
+        self.begin(cmd, min_args=min_args)
+        return self.match_dispatch(errmsg="Unknown command")
+
+    def do_parse(self, cmd):
+        """
+        Called by CliParser. Calls parse()
+        Parsers should pass their return value through this method.
+        """
+        out = self.parse(cmd)
+        if self.has_tokens():
+            self.err("Unknown arguments: " + ' '.join(self._cmd[self._currtok:]))
+        return out
+
+    def try_match(self, rx):
+        """
+        Try to match the given regex with the curren token.
+        rx: compiled regex or string
+        returns: the match object, if the match is successful
+        """
+        tok = self.current_token()
+        if not tok:
+            return None
+        if isinstance(rx, basestring):
+            if not rx.endswith('$'):
+                rx = rx + '$'
+            self._lastmatch = re.match(rx, tok, re.IGNORECASE)
+        else:
+            self._lastmatch = rx.match(tok)
+        if self._lastmatch is not None:
+            if not self.has_tokens():
+                self.err("Unexpected end of line")
+            self._currtok += 1
+        return self._lastmatch
+
+    def match(self, rx, errmsg=None):
+        """
+        Match the given regex with the current token.
+        If match fails, parse is aborted and an error reported.
+        rx: compiled regex or string.
+        errmsg: optional error message if match fails.
+        Returns: The matched token.
+        """
+        if not self.try_match(rx):
+            if errmsg:
+                self.err(errmsg)
+            elif isinstance(rx, basestring):
+                self.err("Expected " + rx)
+            else:
+                self.err("Expected " + rx.pattern.rstrip('$'))
+        return self.matched(0)
+
+    def matched(self, idx=0):
+        """
+        After a successful match, returns
+        the groups generated by the match.
+        """
+        return self._lastmatch.group(idx)
+
+    def lastmatch(self):
+        return self._lastmatch
+
+    def rewind(self):
+        "useful for when validation fails, to undo the match"
+        if self._currtok > 0:
+            self._currtok -= 1
+
+    def current_token(self):
+        if self.has_tokens():
+            return self._cmd[self._currtok]
+        return None
+
+    def has_tokens(self):
+        return self._currtok < len(self._cmd)
+
+    def match_rest(self):
+        '''
+        matches and returns the rest
+        of the tokens in a list
+        '''
+        ret = self._cmd[self._currtok:]
+        self._currtok = len(self._cmd)
+        return ret
+
+    def match_any(self):
+        if not self.has_tokens():
+            self.err("Unexpected end of line")
+        tok = self.current_token()
+        self._currtok += 1
+        self._lastmatch = tok
+        return tok
+
+    def match_nvpairs_bykey(self, valid_keys, minpairs=1):
+        """
+        matches string of p=v | p tokens, but only if p is in valid_keys
+        Returns list of <nvpair> tags
+        """
+        _KEY_RE = re.compile(r'(%s)=(.+)$' % '|'.join(valid_keys))
+        _NOVAL_RE = re.compile(r'(%s)$' % '|'.join(valid_keys))
+        ret = []
+        while True:
+            if self.try_match(_KEY_RE):
+                ret.append(xmlbuilder.nvpair(self.matched(1), self.matched(2)))
+            elif self.try_match(_NOVAL_RE):
+                ret.append(xmlbuilder.nvpair(self.matched(1), ""))
+            else:
+                break
+        if len(ret) < minpairs:
+            if minpairs == 1:
+                self.err("Expected at least one name-value pair")
+            else:
+                self.err("Expected at least %d name-value pairs" % (minpairs))
+        return ret
+
+    def match_nvpairs(self, terminator=None, minpairs=1):
+        """
+        Matches string of p=v tokens
+        Returns list of <nvpair> tags
+        p tokens are also accepted and an nvpair tag with no value attribute
+        is created, as long as they are not in the terminator list
+        """
+        ret = []
+        if terminator is None:
+            terminator = RuleParser._TERMINATORS
+        while True:
+            tok = self.current_token()
+            if tok is not None and tok.lower() in terminator:
+                break
+            elif self.try_match(self._NVPAIR_REF_RE):
+                ret.append(xmlbuilder.nvpair_ref(self.matched(1),
+                                                 self.matched(2)))
+            elif self.try_match(self._NVPAIR_ID_RE):
+                ret.append(xmlbuilder.nvpair_id(self.matched(1),
+                                                self.matched(2),
+                                                self.matched(3)))
+            elif self.try_match(self._NVPAIR_RE):
+                ret.append(xmlbuilder.nvpair(self.matched(1),
+                                             self.matched(2)))
+            elif len(terminator) and self.try_match(self._NVPAIR_KEY_RE):
+                ret.append(xmlbuilder.new("nvpair", name=self.matched(1)))
+            else:
+                break
+        if len(ret) < minpairs:
+            if minpairs == 1:
+                self.err("Expected at least one name-value pair")
+            else:
+                self.err("Expected at least %d name-value pairs" % (minpairs))
+        return ret
+
+    def try_match_nvpairs(self, name, terminator=None):
+        """
+        Matches sequence of <name> [<key>=<value> [<key>=<value> ...] ...]
+        """
+        if self.try_match(name):
+            self._lastmatch = self.match_nvpairs(terminator=terminator, minpairs=1)
+        else:
+            self._lastmatch = []
+        return self._lastmatch
+
+    def match_identifier(self):
+        return self.match(self._IDENT_RE, errmsg="Expected identifier")
+
+    def match_resource(self):
+        return self.match(self._RESOURCE_RE, errmsg="Expected resource")
+
+    def match_idspec(self):
+        """
+        matches $id=<id> | $id-ref=<id>
+        matched(1) = $id|$id-ref
+        matched(2) = <id>
+        """
+        return self.match(self._IDSPEC_RE, errmsg="Expected $id-ref=<id> or $id=<id>")
+
+    def try_match_idspec(self):
+        """
+        matches $id=<value> | $id-ref=<value>
+        matched(1) = $id|$id-ref
+        matched(2) = <value>
+        """
+        return self.try_match(self._IDSPEC_RE)
+
+    def try_match_initial_id(self):
+        """
+        Used as the first match on certain commands
+        like node and property, to match either
+        node $id=<id>
+        or
+        node <id>:
+        """
+        m = self.try_match(self._ID_RE)
+        if m:
+            return m
+        return self.try_match(self._ID_NEW_RE)
+
+    def match_split(self):
+        """
+        matches value[:value]
+        """
+        if not self.current_token():
+            self.err("Expected value[:value]")
+        sp = self.current_token().split(':')
+        if len(sp) > 2:
+            self.err("Expected value[:value]")
+        while len(sp) < 2:
+            sp.append(None)
+        self.match_any()
+        return sp
+
+    def match_dispatch(self, errmsg=None):
+        """
+        Match on the next token. Looks
+        for a method named parse_<token>.
+        If found, the named function is called.
+        Else, an error is reported.
+        """
+        t = self.match(self._DISPATCH_RE, errmsg=errmsg)
+        t = 'parse_' + t.lower()
+        if hasattr(self, t) and callable(getattr(self, t)):
+            return getattr(self, t)()
+        self.rewind()  # rewind for more accurate error message
+        self.err(errmsg)
+
+    def try_match_description(self):
+        """
+        reads a description=? token if one is next
+        """
+        if self.try_match(self._DESC_RE):
+            return self.matched(1)
+        return None
+
+    def match_until(self, end_token):
+        tokens = []
+        while self.current_token() is not None and self.current_token() != end_token:
+            tokens.append(self.match_any())
+        return tokens
+
+
+class RuleParser(BaseParser):
+    """
+    Adds matchers to parse rule expressions.
+    """
+    _SCORE_RE = re.compile(r"([^:]+):$")
+    _ROLE_RE = re.compile(r"\$?role=(.+)$", re.IGNORECASE)
+    _BOOLOP_RE = re.compile(r'(%s)$' % ('|'.join(constants.boolean_ops)), re.IGNORECASE)
+    _UNARYOP_RE = re.compile(r'(%s)$' % ('|'.join(constants.unary_ops)), re.IGNORECASE)
+    _BINOP_RE = None
+
+    _TERMINATORS = ('params', 'meta', 'utilization', 'operations', 'op', 'rule')
+
+    def match_attr_list(self, name, tag):
+        """
+        matches <name> [$id=<id>] [<score>:] <n>=<v> <n>=<v> ... | $id-ref=<id-ref>
+        """
+        from cibconfig import cib_factory
+
+        self.match(name)
+        xmlid = None
+        if self.try_match_idspec():
+            if self.matched(1) == '$id-ref':
+                r = xmlbuilder.new(tag)
+                ref = cib_factory.resolve_id_ref(name, self.matched(2))
+                r.set('id-ref', ref)
+                return r
+            else:
+                xmlid = self.matched(2)
+        score = None
+        if self.try_match(self._SCORE_RE):
+            score = self.matched(1)
+        rules = self.match_rules()
+        values = self.match_nvpairs(minpairs=0)
+        return xmlbuilder.attributes(tag, rules, values, xmlid=xmlid, score=score)
+
+    def match_attr_lists(self, name_map):
+        """
+        generator which matches attr_lists
+        name_map: maps CLI name to XML name
+        """
+        while self.try_match('|'.join(name_map.keys())):
+            name = self.matched(0).lower()
+            self.rewind()
+            yield self.match_attr_list(name, name_map[name])
+
+    def match_rules(self):
+        '''parse rule definitions'''
+        from cibconfig import cib_factory
+
+        rules = []
+        while self.try_match('rule'):
+            rule = xmlbuilder.new('rule')
+            rules.append(rule)
+            idref = False
+            if self.try_match_idspec():
+                idtyp, idval = self.matched(1)[1:], self.matched(2)
+                if idtyp == 'id-ref':
+                    idval = cib_factory.resolve_id_ref('rule', idval)
+                    idref = True
+                rule.set(idtyp, idval)
+            if self.try_match(self._ROLE_RE):
+                rule.set('role', self.matched(1))
+            if idref:
+                continue
+            if self.try_match(self._SCORE_RE):
+                rule.set(*self.validate_score(self.matched(1)))
+            else:
+                rule.set('score', 'INFINITY')
+            boolop, exprs = self.match_rule_expression()
+            if boolop and not keyword_cmp(boolop, 'and'):
+                rule.set('boolean-op', boolop)
+            for expr in exprs:
+                rule.append(expr)
+        return rules
+
+    def match_rule_expression(self):
+        """
+        expression :: <simple_exp> [bool_op <simple_exp> ...]
+        bool_op :: or | and
+        simple_exp :: <attribute> [type:]<binary_op> <value>
+                      | <unary_op> <attribute>
+                      | date <date_expr>
+        type :: string | version | number
+        binary_op :: lt | gt | lte | gte | eq | ne
+        unary_op :: defined | not_defined
+
+        date_expr :: lt <end>
+                     | gt <start>
+                     | in_range start=<start> end=<end>
+                     | in_range start=<start> <duration>
+                     | date_spec <date_spec>
+        duration|date_spec ::
+                     hours=<value>
+                     | monthdays=<value>
+                     | weekdays=<value>
+                     | yearsdays=<value>
+                     | months=<value>
+                     | weeks=<value>
+                     | years=<value>
+                     | weekyears=<value>
+                     | moon=<value>
+        """
+        boolop = None
+        exprs = [self._match_simple_exp()]
+        while self.try_match(self._BOOLOP_RE):
+            if boolop and self.matched(1) != boolop:
+                self.err("Mixing bool ops not allowed: %s != %s" % (boolop, self.matched(1)))
+            else:
+                boolop = self.matched(1)
+            exprs.append(self._match_simple_exp())
+        return boolop, exprs
+
+    def _match_simple_exp(self):
+        if self.try_match('date'):
+            return self.match_date()
+        elif self.try_match(self._UNARYOP_RE):
+            unary_op = self.matched(1)
+            attr = self.match_identifier()
+            return xmlbuilder.new('expression', operation=unary_op, attribute=attr)
+        else:
+            attr = self.match_identifier()
+            if not self._BINOP_RE:
+                self._BINOP_RE = re.compile(r'((%s):)?(%s)$' % (
+                    '|'.join(self.validation.expression_types()),
+                    '|'.join(constants.binary_ops)), re.IGNORECASE)
+            self.match(self._BINOP_RE)
+            optype = self.matched(2)
+            binop = self.matched(3)
+            val = self.match_any()
+            node = xmlbuilder.new('expression',
+                                  operation=binop,
+                                  attribute=attr,
+                                  value=val)
+            xmlbuilder.maybe_set(node, 'type', optype)
+            return node
+
+    def match_date(self):
+        """
+        returns for example:
+        <date_expression id="" operation="op">
+        <date_spec hours="9-16"/>
+        </date_expression>
+        """
+        node = xmlbuilder.new('date_expression')
+
+        date_ops = self.validation.date_ops()
+        # spec -> date_spec
+        if 'date_spec' in date_ops:
+            date_ops.append('spec')
+        # in -> in_range
+        if 'in_range' in date_ops:
+            date_ops.append('in')
+        self.match('(%s)$' % ('|'.join(date_ops)))
+        op = self.matched(1)
+        opmap = {'in': 'in_range', 'spec': 'date_spec'}
+        node.set('operation', opmap.get(op, op))
+        if op in olist(constants.simple_date_ops):
+            # lt|gt <value>
+            val = self.match_any()
+            if keyword_cmp(op, 'lt'):
+                node.set('end', val)
+            else:
+                node.set('start', val)
+            return node
+        elif op in ('in_range', 'in'):
+            # date in start=<start> end=<end>
+            # date in start=<start> <duration>
+            valid_keys = list(constants.in_range_attrs) + constants.date_spec_names
+            vals = self.match_nvpairs_bykey(valid_keys, minpairs=2)
+            return xmlbuilder.set_date_expression(node, 'duration', vals)
+        elif op in ('date_spec', 'spec'):
+            valid_keys = constants.date_spec_names
+            vals = self.match_nvpairs_bykey(valid_keys, minpairs=1)
+            return xmlbuilder.set_date_expression(node, 'date_spec', vals)
+        else:
+            self.err("Unknown date operation '%s', please upgrade crmsh" % (op))
+
+    def validate_score(self, score, noattr=False):
+        if not noattr and score in olist(constants.score_types):
+            return constants.score_types[score.lower()]
+        elif re.match("^[+-]?(inf(inity)?|INF(INITY)?|[0-9]+)$", score):
+            score = re.sub("inf(inity)?|INF(INITY)?", "INFINITY", score)
+            return ["score", score]
+        if noattr:
+            # orders have the special kind attribute
+            kind = self.validation.canonize(score, self.validation.rsc_order_kinds())
+            if not kind:
+                self.err("Invalid kind: " + score)
+            return ['kind', kind]
+        else:
+            return ['score-attribute', score]
+
+
+class NodeParser(RuleParser):
+    _UNAME_RE = re.compile(r'([^:]+)(:(normal|member|ping|remote))?$', re.IGNORECASE)
+
+    def can_parse(self):
+        return ('node',)
+
+    def parse(self, cmd):
+        """
+        node [<id>:|$id=<id>] <uname>[:<type>]
+          [description=<description>]
+          [attributes <param>=<value> [<param>=<value>...]]
+          [utilization <param>=<value> [<param>=<value>...]]
+
+        type :: normal | member | ping | remote
+        """
+        self.begin(cmd, min_args=1)
+        self.match('node')
+        out = xmlbuilder.new('node')
+        xmlbuilder.maybe_set(out, "id", self.try_match_initial_id() and self.matched(1))
+        self.match(self._UNAME_RE, errmsg="Expected uname[:type]")
+        out.set("uname", self.matched(1))
+        if self.validation.node_type_optional():
+            xmlbuilder.maybe_set(out, "type", self.matched(3))
+        else:
+            out.set("type", self.matched(3) or constants.node_default_type)
+        xmlbuilder.maybe_set(out, "description", self.try_match_description())
+        for attr_list in self.match_attr_lists({'attributes': 'instance_attributes',
+                                                'utilization': 'utilization'}):
+            out.append(attr_list)
+        return out
+
+
+class ResourceParser(RuleParser):
+    _TEMPLATE_RE = re.compile(r'@(.+)$')
+    _RA_TYPE_RE = re.compile(r'[a-z0-9_:-]+$', re.IGNORECASE)
+    _OPTYPE_RE = re.compile(r'(%s)$' % ('|'.join(constants.op_cli_names)), re.IGNORECASE)
+
+    def can_parse(self):
+        return ('primitive', 'group', 'clone', 'ms', 'master', 'rsc_template')
+
+    def match_ra_type(self, out):
+        "[<class>:[<provider>:]]<type>"
+        if not self.current_token():
+            self.err("Expected resource type")
+        cpt = self.validation.class_provider_type(self.current_token())
+        if not cpt:
+            self.err("Unknown resource type")
+        self.match_any()
+        xmlbuilder.maybe_set(out, 'class', cpt[0])
+        xmlbuilder.maybe_set(out, 'provider', cpt[1])
+        xmlbuilder.maybe_set(out, 'type', cpt[2])
+
+    def match_op(self, out, pfx='op'):
+        """
+        op <optype> [<n>=<v> ...]
+
+        to:
+          <op name="monitor" timeout="30" interval="10" id="p_mysql-monitor-10">
+            <instance_attributes id="p_mysql-monitor-10-instance_attributes">
+              <nvpair name="depth" value="0" id="p_mysql-monitor-10-instance_attributes-depth"/>
+            </instance_attributes>
+          </op>
+        """
+        self.match('op')
+        op_type = self.match(self._OPTYPE_RE, errmsg="Expected operation type")
+        all_attrs = self.match_nvpairs(minpairs=0)
+        node = xmlbuilder.new('op', name=op_type)
+        if not any(nvp.get('name') == 'interval' for nvp in all_attrs):
+            all_attrs.append(xmlbuilder.nvpair('interval', '0'))
+        valid_attrs = self.validation.op_attributes()
+        inst_attrs = None
+        for nvp in all_attrs:
+            if nvp.get('name') in valid_attrs:
+                node.set(nvp.get('name'), nvp.get('value'))
+            else:
+                if inst_attrs is None:
+                    inst_attrs = xmlbuilder.child(node, 'instance_attributes')
+                inst_attrs.append(nvp)
+        out.append(node)
+
+    def match_operations(self, out, match_id):
+        from cibconfig import cib_factory
+
+        def is_op():
+            return self.has_tokens() and self.current_token().lower() == 'op'
+        if match_id:
+            self.match('operations')
+        node = xmlbuilder.child(out, 'operations')
+        if match_id:
+            self.match_idspec()
+            match_id = self.matched(1)[1:].lower()
+            idval = self.matched(2)
+            if match_id == 'id-ref':
+                idval = cib_factory.resolve_id_ref('operations', idval)
+
+            node.set(match_id, idval)
+
+        # The ID assignment skips the operations node if possible,
+        # so we need to pass the prefix (id of the owner node)
+        # to match_op
+        pfx = out.get('id') or 'op'
+
+        while is_op():
+            self.match_op(node, pfx=pfx)
+
+    def match_arguments(self, out, name_map):
+        """
+        [<name> attr_list]
+        [operations id_spec]
+        [op op_type [<attribute>=<value> ...] ...]
+
+        attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+        id_spec :: $id=<id> | $id-ref=<id>
+        op_type :: start | stop | monitor
+        """
+        names = olist(name_map.keys())
+        oplist = olist([op for op in name_map if op.lower() in ('operations', 'op')])
+        for op in oplist:
+            del name_map[op]
+        while self.has_tokens() and self.current_token() in names:
+            t = self.current_token().lower()
+            if t in oplist:
+                self.match_operations(out, t == 'operations')
+            else:
+                for attr_list in self.match_attr_lists(name_map):
+                    out.append(attr_list)
+
+    def parse(self, cmd):
+        return self.begin_dispatch(cmd, min_args=2)
+
+    def _primitive_or_template(self):
+        """
+        primitive <rsc> {[<class>:[<provider>:]]<type>|@<template>]
+          [params attr_list]
+          [meta attr_list]
+          [utilization attr_list]
+          [operations id_spec]
+          [op op_type [<attribute>=<value> ...] ...]
+
+        attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val> ...] | $id-ref=<id>
+        id_spec :: $id=<id> | $id-ref=<id>
+        op_type :: start | stop | monitor
+        """
+        t = self.matched(0).lower()
+        if t == 'primitive':
+            out = xmlbuilder.new('primitive')
+        else:
+            out = xmlbuilder.new('template')
+        out.set('id', self.match_identifier())
+        if t == 'primitive' and self.try_match(self._TEMPLATE_RE):
+            out.set('template', self.matched(1))
+        else:
+            self.match_ra_type(out)
+        xmlbuilder.maybe_set(out, 'description', self.try_match_description())
+        self.match_arguments(out, {'params': 'instance_attributes',
+                                   'meta': 'meta_attributes',
+                                   'utilization': 'utilization',
+                                   'operations': 'operations',
+                                   'op': 'op'})
+        return out
+
+    parse_primitive = _primitive_or_template
+    parse_rsc_template = _primitive_or_template
+
+    def _master_or_clone(self):
+        if self.matched(0).lower() == 'clone':
+            out = xmlbuilder.new('clone')
+        else:
+            out = xmlbuilder.new('master')
+        out.set('id', self.match_identifier())
+
+        child = xmlbuilder.new('crmsh-ref', id=self.match_resource())
+        xmlbuilder.maybe_set(out, 'description', self.try_match_description())
+        self.match_arguments(out, {'params': 'instance_attributes',
+                                   'meta': 'meta_attributes'})
+        out.append(child)
+        return out
+
+    parse_master = _master_or_clone
+    parse_ms = _master_or_clone
+    parse_clone = _master_or_clone
+
+    def _try_group_resource(self):
+        t = self.current_token()
+        if (not t) or ('=' in t) or (t.lower() in ('params', 'meta')):
+            return None
+        return self.match_any()
+
+    def parse_group(self):
+        out = xmlbuilder.new('group')
+        out.set('id', self.match_identifier())
+        children = []
+        while self._try_group_resource():
+            child = self.lastmatch()
+            if child in children:
+                self.err("child %s listed more than once in group %s" %
+                         (child, out.get('id')))
+            children.append(child)
+        xmlbuilder.maybe_set(out, 'description', self.try_match_description())
+        self.match_arguments(out, {'params': 'instance_attributes',
+                                   'meta': 'meta_attributes'})
+        for child in children:
+            xmlbuilder.child(out, 'crmsh-ref', id=child)
+        return out
+
+
+class ConstraintParser(RuleParser):
+    _ROLE2_RE = re.compile(r"role=(.+)$", re.IGNORECASE)
+
+    def can_parse(self):
+        return ('location', 'colocation', 'collocation', 'order', 'rsc_ticket')
+
+    def parse(self, cmd):
+        return self.begin_dispatch(cmd, min_args=2)
+
+    def parse_location(self):
+        """
+        location <id> rsc [[$]<attribute>=<value>] <score>: <node>
+        location <id> rsc [[$]<attribute>=<value>] <rule> [<rule> ...]
+        rsc :: /<rsc-pattern>/
+            | { <rsc-set> }
+            | <rsc>
+        attribute :: role | resource-discovery
+        """
+        out = xmlbuilder.new('rsc_location', id=self.match_identifier())
+        if self.try_match('^/(.+)/$'):
+            out.set('rsc-pattern', self.matched(1))
+        elif self.try_match('{'):
+            tokens = self.match_until('}')
+            self.match('}')
+            if not tokens:
+                self.err("Empty resource set")
+            parser = ResourceSet('role', tokens, self)
+            for rscset in parser.parse():
+                out.append(rscset)
+        else:
+            out.set('rsc', self.match_resource())
+
+        while self.try_match(self._ATTR_RE):
+            out.set(self.matched(1), self.matched(2))
+
+        if self.try_match(self._ROLE_RE) or self.try_match(self._ROLE2_RE):
+            out.set('role', self.matched(1))
+
+        score = False
+        if self.try_match(self._SCORE_RE):
+            score = True
+            out.set(*self.validate_score(self.matched(1)))
+            out.set('node', self.match_identifier())
+            # backwards compatibility: role used to be read here
+            if 'role' not in out:
+                if self.try_match(self._ROLE_RE) or self.try_match(self._ROLE2_RE):
+                    out.set('role', self.matched(1))
+        if not score:
+            rules = self.match_rules()
+            out.extend(rules)
+            if not rules:
+                self.err("expected <score>: <node> or <rule> [<rule> ...]")
+        return out
+
+    def parse_colocation(self):
+        """
+        colocation <id> <score>: <rsc>[:<role>] <rsc>[:<role>] ...
+          [node-attribute=<node_attr>]
+        """
+        out = xmlbuilder.new('rsc_colocation', id=self.match_identifier())
+        self.match(self._SCORE_RE, errmsg="Expected <score>:")
+        out.set(*self.validate_score(self.matched(1)))
+        if self.try_match_tail('node-attribute=(.+)$'):
+            out.set('node-attribute', self.matched(1).lower())
+        self.try_match_rscset(out, 'role')
+        return out
+
+    parse_collocation = parse_colocation
+
+    def parse_order(self):
+        '''
+        order <id> [{kind|<score>}:] <rsc>[:<action>] <rsc>[:<action>] ...
+          [symmetrical=<bool>]
+
+        kind :: Mandatory | Optional | Serialize
+        '''
+        out = xmlbuilder.new('rsc_order', id=self.match_identifier())
+        if self.try_match('(%s):$' % ('|'.join(self.validation.rsc_order_kinds()))):
+            out.set('kind', self.validation.canonize(
+                self.matched(1), self.validation.rsc_order_kinds()))
+        elif self.try_match(self._SCORE_RE):
+            out.set(*self.validate_score(self.matched(1), noattr=True))
+        if self.try_match_tail('symmetrical=(true|false|yes|no|on|off)$'):
+            out.set('symmetrical', canonical_boolean(self.matched(1)))
+        self.try_match_rscset(out, 'action')
+        return out
+
+    def parse_rsc_ticket(self):
+        '''
+        rsc_ticket <id> <ticket_id>: <rsc>[:<role>] [<rsc>[:<role>] ...]
+        [loss-policy=<loss_policy_action>]
+
+        loss_policy_action :: stop | demote | fence | freeze
+        '''
+        out = xmlbuilder.new('rsc_ticket', id=self.match_identifier())
+        self.match(self._SCORE_RE, errmsg="Expected <ticket-id>:")
+        out.set('ticket', self.matched(1))
+        if self.try_match_tail('loss-policy=(stop|demote|fence|freeze)$'):
+            out.set('loss-policy', self.matched(1))
+        self.try_match_rscset(out, 'role', simple_count=1)
+        return out
+
+    def try_match_rscset(self, out, suffix_type, simple_count=2):
+        simple, resources = self.match_resource_set(suffix_type, simple_count=simple_count)
+        if simple:
+            for n, v in resources:
+                out.set(n, v)
+        elif resources:
+            for rscset in resources:
+                out.append(rscset)
+        else:
+            def repeat(v, n):
+                for _ in range(0, n):
+                    yield v
+            self.err("Expected %s | resource_sets" %
+                     " ".join(repeat("<rsc>[:<%s>]" % (suffix_type), simple_count)))
+
+    def try_match_tail(self, rx):
+        "ugly hack to prematurely extract a tail attribute"
+        pos = self._currtok
+        self._currtok = len(self._cmd) - 1
+        ret = self.try_match(rx)
+        if ret:
+            self._cmd = self._cmd[:-1]
+        self._currtok = pos
+        return ret
+
+    def remaining_tokens(self):
+        return len(self._cmd) - self._currtok
+
+    def match_resource_set(self, suffix_type, simple_count=2):
+        simple = False
+        if self.remaining_tokens() == simple_count:
+            simple = True
+            if suffix_type == 'role':
+                return True, self.match_simple_role_set(simple_count)
+            else:
+                return True, self.match_simple_action_set()
+        tokens = self.match_rest()
+        parser = ResourceSet(suffix_type, tokens, self)
+        return simple, parser.parse()
+
+    def match_simple_role_set(self, count):
+        def rsc_role():
+            rsc, role = self.match_split()
+            role, t = self.validation.classify_role(role)
+            if role and not t:
+                self.err("Invalid role '%s' for '%s'" % (role, rsc))
+            return rsc, role, t
+
+        def fmt(info, name):
+            if info[1]:
+                return [[name, info[0]], [name + '-' + info[2], info[1]]]
+            return [[name, info[0]]]
+        ret = fmt(rsc_role(), 'rsc')
+        if count == 2:
+            ret += fmt(rsc_role(), 'with-rsc')
+        return ret
+
+    def match_simple_action_set(self):
+        def rsc_action():
+            rsc, action = self.match_split()
+            action, t = self.validation.classify_action(action)
+            if action and not t:
+                self.err('invalid action: ' + action)
+            return rsc, action, t
+
+        def fmt(info, name):
+            if info[1]:
+                return [[name, info[0]], [name + '-' + info[2], info[1]]]
+            return [[name, info[0]]]
+        ret = fmt(rsc_action(), 'first')
+        return ret + fmt(rsc_action(), 'then')
+
+
+class OpParser(BaseParser):
+    def can_parse(self):
+        return ('monitor',)
+
+    def parse(self, cmd):
+        return self.begin_dispatch(cmd, min_args=2)
+
+    def parse_monitor(self):
+        out = xmlbuilder.new('op', name="monitor")
+        resource, role = self.match_split()
+        if role:
+            role, role_class = self.validation.classify_role(role)
+            if not role_class:
+                self.err("Invalid role '%s' for resource '%s'" % (role, resource))
+            out.set(role_class, role)
+        out.set('rsc', resource)
+        interval, timeout = self.match_split()
+        xmlbuilder.maybe_set(out, 'interval', interval)
+        xmlbuilder.maybe_set(out, 'timeout', timeout)
+        return out
+
+
+class PropertyParser(RuleParser):
+    """
+    property = <cluster_property_set>...</>
+    rsc_defaults = <rsc_defaults><meta_attributes>...</></>
+    op_defaults = <op_defaults><meta_attributes>...</></>
+    """
+    def can_parse(self):
+        return ('property', 'rsc_defaults', 'op_defaults')
+
+    def parse(self, cmd):
+        from cibconfig import cib_factory
+
+        setmap = {'property': 'cluster_property_set',
+                  'rsc_defaults': 'meta_attributes',
+                  'op_defaults': 'meta_attributes'}
+        self.begin(cmd, min_args=1)
+        self.match('(%s)$' % '|'.join(self.can_parse()))
+        if self.matched(1) in constants.defaults_tags:
+            root = xmlbuilder.new(self.matched(1))
+            attrs = xmlbuilder.child(root, setmap[self.matched(1)])
+        else:  # property -> cluster_property_set
+            root = xmlbuilder.new(setmap[self.matched(1)])
+            attrs = root
+        if self.try_match_initial_id():
+            attrs.set('id', self.matched(1))
+        elif self.try_match_idspec():
+            idkey = self.matched(1)[1:]
+            idval = self.matched(2)
+            if idkey == 'id-ref':
+                idval = cib_factory.resolve_id_ref(attrs.tag, idval)
+            attrs.set(idkey, idval)
+        for rule in self.match_rules():
+            attrs.append(rule)
+        for nvp in self.match_nvpairs(minpairs=0):
+            attrs.append(nvp)
+        return root
+
+
+class FencingOrderParser(BaseParser):
+    """
+    <fencing-topology>
+    <fencing-level id=<id> target=<text> index=<+int> devices="\w,\w..."/>
+    </fencing-topology>
+    """
+
+    _TARGET_RE = re.compile(r'([^:]+):$')
+
+    def can_parse(self):
+        return ('fencing-topology', 'fencing_topology')
+
+    def parse(self, cmd):
+        self.begin(cmd, min_args=1)
+        if not self.try_match("fencing-topology"):
+            self.match("fencing_topology")
+        target = "@@"
+        # (target, devices)
+        raw_levels = []
+        while self.has_tokens():
+            if self.try_match(self._TARGET_RE):
+                target = self.matched(1)
+            else:
+                raw_levels.append((target, self.match_any()))
+        if len(raw_levels) == 0:
+            self.err("Missing list of devices")
+        return self._postprocess_levels(raw_levels)
+
+    def _postprocess_levels(self, raw_levels):
+        from collections import defaultdict
+        from itertools import repeat
+        from cibconfig import cib_factory
+        if raw_levels[0][0] == "@@":
+            def node_levels():
+                for node in cib_factory.node_id_list():
+                    for target, devices in raw_levels:
+                        yield node, devices
+            lvl_generator = node_levels
+        else:
+            lvl_generator = lambda: raw_levels
+
+        out = xmlbuilder.new('fencing-topology')
+        targets = defaultdict(repeat(1).next)
+        for target, devices in lvl_generator():
+            xmlbuilder.child(out, 'fencing-level',
+                             target=target,
+                             index=str(targets[target]),
+                             devices=devices)
+            targets[target] += 1
+
+        return out
+
+
+class TagParser(BaseParser):
+    """
+    <tag id=id>
+      <obj_ref id=rsc/>
+      ...
+    </tag>
+    """
+    _TAG_RE = re.compile(r"([^:]+):$")
+
+    def can_parse(self):
+        return ('tag',)
+
+    def parse(self, cmd):
+        self.begin(cmd, min_args=2)
+        self.match('tag')
+        self.match(self._TAG_RE)
+        out = xmlbuilder.new('tag', id=self.matched(1))
+        while self.has_tokens():
+            e = xmlbuilder.new('obj_ref', id=self.match_resource())
+            out.append(e)
+        if len(out) == 0:
+            self.err("Expected at least one resource")
+        return out
+
+
+class AclParser(BaseParser):
+    _ACL_RIGHT_RE = re.compile(r'(%s)$' % ('|'.join(constants.acl_rule_names)), re.IGNORECASE)
+    _ROLE_REF_RE = re.compile(r'role:(.+)$', re.IGNORECASE)
+
+    def can_parse(self):
+        return ('user', 'role', 'acl_target', 'acl_group')
+
+    def parse(self, cmd):
+        return self.begin_dispatch(cmd, min_args=2)
+
+    def parse_user(self):
+        out = xmlbuilder.new('acl_user')
+        out.set('id', self.match_identifier())
+        while self.has_tokens():
+            # role identifier
+            if self.try_match(self._ROLE_REF_RE):
+                xmlbuilder.child(out, 'role_ref', id=self.matched(1))
+            # acl right rule
+            else:
+                out.append(self._add_rule())
+        return out
+
+    def parse_acl_target(self):
+        out = xmlbuilder.new('acl_target')
+        out.set('id', self.match_identifier())
+        while self.has_tokens():
+            xmlbuilder.child(out, 'role', id=self.match_identifier())
+        return out
+
+    def parse_acl_group(self):
+        out = xmlbuilder.new('acl_group')
+        out.set('id', self.match_identifier())
+        while self.has_tokens():
+            xmlbuilder.child(out, 'role', id=self.match_identifier())
+        return out
+
+    def parse_role(self):
+        out = xmlbuilder.new('acl_role')
+        out.set('id', self.match_identifier())
+
+        if self.validation.acl_2_0():
+            xmlbuilder.maybe_set(out, "description", self.try_match_description())
+            while self.has_tokens():
+                self._add_permission(out)
+        else:
+            while self.has_tokens():
+                out.append(self._add_rule())
+        return out
+
+    _PERM_RE = re.compile(r"([^:]+)(?::(.+))?$", re.I)
+
+    def _is_permission(self, val):
+        def permission(x):
+            return x in constants.acl_spec_map_2 or x in constants.acl_shortcuts
+        x = val.split(':', 1)
+        return len(x) > 0 and permission(x[0])
+
+    def _add_permission(self, out):
+        rule = xmlbuilder.new('acl_permission')
+        rule.set('kind', self.match(self._ACL_RIGHT_RE).lower())
+        if self.try_match_initial_id():
+            rule.set('id', self.matched(1))
+        xmlbuilder.maybe_set(rule, "description", self.try_match_description())
+
+        attributes = {}
+
+        while self.has_tokens():
+            if not self._is_permission(self.current_token()):
+                break
+            self.match(self._PERM_RE, errmsg="Expected <type>:<spec>")
+            typ = self.matched(1)
+            typ = constants.acl_spec_map_2.get(typ, typ)
+            val = self.matched(2)
+            if typ in constants.acl_shortcuts:
+                typ, val = self._expand_shortcuts_2(typ, val)
+            elif val is None:
+                self.err("Expected <type>:<spec>")
+            attributes[typ] = val
+        # valid combinations of rule attributes:
+        # xpath
+        # reference
+        # object-type + attribute
+        # split other combinations here
+        from copy import deepcopy
+        if 'xpath' in attributes:
+            rule2 = deepcopy(rule)
+            rule2.set('xpath', attributes['xpath'])
+            out.append(rule2)
+        if 'reference' in attributes:
+            rule2 = deepcopy(rule)
+            rule2.set('reference', attributes['reference'])
+            out.append(rule2)
+        if 'object-type' in attributes:
+            rule2 = deepcopy(rule)
+            rule2.set('object-type', attributes['object-type'])
+            if 'attribute' in attributes:
+                rule2.set('attribute', attributes['attribute'])
+            out.append(rule2)
+        if 'attribute' in attributes and 'object-type' not in attributes:
+            self.err("attribute is only valid in combination with tag/object-type")
+
+    def _add_rule(self):
+        rule = xmlbuilder.new(self.match(self._ACL_RIGHT_RE).lower())
+        eligible_specs = constants.acl_spec_map.values()
+        while self.has_tokens():
+            a = self._expand_shortcuts(self.current_token().split(':', 1))
+            if len(a) != 2 or a[0] not in eligible_specs:
+                break
+            self.match_any()
+            rule.set(a[0], a[1])
+            if self._remove_spec(eligible_specs, a[0]):
+                break
+        return rule
+
+    def _remove_spec(self, speclist, spec):
+        """
+        Remove spec from list of eligible specs.
+        Returns true if spec parse is complete.
+        """
+        try:
+            speclist.remove(spec)
+            if spec == 'xpath':
+                speclist.remove('ref')
+                speclist.remove('tag')
+            elif spec in ('ref', 'tag'):
+                speclist.remove('xpath')
+            else:
+                return True
+        except ValueError:
+            pass
+        return False
+
+    def _remove_spec_2(self, speclist, spec):
+        """
+        Remove spec from list of eligible specs.
+        Returns true if spec parse is complete.
+        """
+        try:
+            speclist.remove(spec)
+            if spec == 'xpath':
+                speclist.remove('reference')
+                speclist.remove('object-type')
+            elif spec in ('reference', 'object-type'):
+                speclist.remove('xpath')
+            else:
+                return True
+        except ValueError:
+            pass
+        return False
+
+    def _expand_shortcuts_2(self, typ, val):
+        '''
+        expand xpath shortcuts: the typ prefix names the shortcut
+        '''
+        expansion = constants.acl_shortcuts[typ]
+        if val is None:
+            if '@@' in expansion[0]:
+                self.err("Missing argument to ACL shortcut %s" % (typ))
+            return 'xpath', expansion[0]
+        a = val.split(':')
+        xpath = ""
+        exp_i = 0
+        for tok in a:
+            try:
+                # some expansions may contain no id placeholders
+                # of course, they don't consume input tokens
+                if '@@' not in expansion[exp_i]:
+                    xpath += expansion[exp_i]
+                    exp_i += 1
+                xpath += expansion[exp_i].replace('@@', tok)
+                exp_i += 1
+            except:
+                return []
+        # need to remove backslash chars which were there to escape
+        # special characters in expansions when used as regular
+        # expressions (mainly '[]')
+        val = xpath.replace("\\", "")
+        return 'xpath', val
+
+    def _expand_shortcuts(self, l):
+        '''
+        Expand xpath shortcuts. The input list l contains the user
+        input. If no shortcut was found, just return l.
+        In case of syntax error, return empty list. Otherwise, l[0]
+        contains 'xpath' and l[1] the expansion as found in
+        constants.acl_shortcuts. The id placeholders '@@' are replaced
+        with the given attribute names or resource references.
+        '''
+        try:
+            expansion = constants.acl_shortcuts[l[0]]
+        except KeyError:
+            return l
+        l[0] = "xpath"
+        if len(l) == 1:
+            if '@@' in expansion[0]:
+                return []
+            l.append(expansion[0])
+            return l
+        a = l[1].split(':')
+        xpath = ""
+        exp_i = 0
+        for tok in a:
+            try:
+                # some expansions may contain no id placeholders
+                # of course, they don't consume input tokens
+                if '@@' not in expansion[exp_i]:
+                    xpath += expansion[exp_i]
+                    exp_i += 1
+                xpath += expansion[exp_i].replace('@@', tok)
+                exp_i += 1
+            except:
+                return []
+        # need to remove backslash chars which were there to escape
+        # special characters in expansions when used as regular
+        # expressions (mainly '[]')
+        l[1] = xpath.replace("\\", "")
+        return l
+
+
+class RawXMLParser(BaseParser):
+    def can_parse(self):
+        return ('xml',)
+
+    def parse(self, cmd):
+        self.begin(cmd, min_args=1)
+        self.match('xml')
+        if not self.has_tokens():
+            self.err("Expected XML data")
+        xml_data = ' '.join(self.match_rest())
+        # strip spaces between elements
+        # they produce text elements
+        try:
+            e = etree.fromstring(xml_data)
+        except Exception, e:
+            common_err("Cannot parse XML data: %s" % xml_data)
+            self.err(e)
+        if e.tag not in constants.cib_cli_map:
+            self.err("Element %s not recognized" % (e.tag))
+        return e
+
+
+class ResourceSet(object):
+    '''
+    Constraint resource set parser. Parses sth like:
+    a ( b c:start ) d:Master e ...
+    Appends one or more resource sets to cli_list.
+    Resource sets are in form:
+    <resource_set [sequential=false] [require-all=false] [action=<action>] [role=<role>]>
+        <resource_ref id="<rsc>"/>
+        ...
+    </resource_set>
+    Action/role change makes a new resource set.
+    '''
+    open_set = ('(', '[')
+    close_set = (')', ']')
+    matching = {
+        '[': ']',
+        '(': ')',
+    }
+
+    def __init__(self, type, s, parent):
+        self.parent = parent
+        self.q_attr = type
+        self.tokens = s
+        self.cli_list = []
+        self.reset_set()
+        self.opened = ''
+        self.sequential = True
+        self.require_all = True
+        self.fix_parentheses()
+
+    def fix_parentheses(self):
+        newtoks = []
+        for p in self.tokens:
+            if p[0] in self.open_set and len(p) > 1:
+                newtoks.append(p[0])
+                newtoks.append(p[1:])
+            elif p[len(p)-1] in self.close_set and len(p) > 1:
+                newtoks.append(p[0:len(p)-1])
+                newtoks.append(p[len(p)-1])
+            else:
+                newtoks.append(p)
+        self.tokens = newtoks
+
+    def reset_set(self):
+        self.set_pl = xmlbuilder.new("resource_set")
+        self.prev_q = ''  # previous qualifier (action or role)
+        self.curr_attr = ''  # attribute (action or role)
+
+    def save_set(self):
+        if not len(self.set_pl):
+            return
+        if not self.require_all:
+            self.set_pl.set("require-all", "false")
+        if not self.sequential:
+            self.set_pl.set("sequential", "false")
+        if self.curr_attr:
+            self.set_pl.set(self.curr_attr, self.prev_q)
+        self.make_resource_set()
+        self.reset_set()
+
+    def make_resource_set(self):
+        self.cli_list.append(self.set_pl)
+
+    def parseattr(self, p, tokpos):
+        attrs = {"sequential": "sequential",
+                 "require-all": "require_all"}
+        l = p.split('=')
+        if len(l) != 2:
+            self.err('Extra = in %s' % (p),
+                     token=self.tokens[tokpos])
+        if l[0] not in attrs:
+            self.err('Unknown attribute',
+                     token=self.tokens[tokpos])
+        k, v = l
+        if not verify_boolean(v):
+            self.err('Not a boolean: %s' % (v),
+                     token=self.tokens[tokpos])
+        setattr(self, attrs[k], get_boolean(v))
+        return True
+
+    def splitrsc(self, p):
+        l = p.split(':')
+        if len(l) == 2:
+            if self.q_attr == 'action':
+                l[1] = self.parent.validation.canonize(
+                    l[1],
+                    self.parent.validation.resource_actions())
+            else:
+                l[1] = self.parent.validation.canonize(
+                    l[1],
+                    self.parent.validation.resource_roles())
+            if not l[1]:
+                self.err('Invalid %s for %s' % (self.q_attr, p))
+        elif len(l) == 1:
+            l = [p, '']
+        return l
+
+    def err(self, errmsg, token=''):
+        syntax_err(self.parent._cmd,
+                   context=self.q_attr,
+                   token=token,
+                   msg=errmsg)
+        raise ParseError
+
+    def update_attrs(self, bracket, tokpos):
+        if bracket in ('(', '['):
+            if self.opened:
+                self.err('Cannot nest resource sets',
+                         token=self.tokens[tokpos])
+            self.sequential = False
+            if bracket == '[':
+                self.require_all = False
+            self.opened = bracket
+        elif bracket in (')', ']'):
+            if not self.opened:
+                self.err('Unmatched closing bracket',
+                         token=self.tokens[tokpos])
+            if bracket != self.matching[self.opened]:
+                self.err('Mismatched closing bracket',
+                         token=self.tokens[tokpos])
+            self.sequential = True
+            self.require_all = True
+            self.opened = ''
+
+    def parse(self):
+        tokpos = -1
+        for p in self.tokens:
+            tokpos += 1
+            if p == "_rsc_set_":
+                continue  # a degenerate resource set
+            if p in self.open_set:
+                self.save_set()
+                self.update_attrs(p, tokpos)
+                continue
+            if p in self.close_set:
+                # empty sets not allowed
+                if not len(self.set_pl):
+                    self.err('Empty resource set',
+                             token=self.tokens[tokpos])
+                self.save_set()
+                self.update_attrs(p, tokpos)
+                continue
+            if '=' in p:
+                self.parseattr(p, tokpos)
+                continue
+            rsc, q = self.splitrsc(p)
+            if q != self.prev_q:  # one set can't have different roles/actions
+                self.save_set()
+                self.prev_q = q
+            if q:
+                if not self.curr_attr:
+                    self.curr_attr = self.q_attr
+            else:
+                self.curr_attr = ''
+            self.set_pl.append(xmlbuilder.new("resource_ref", id=rsc))
+        if self.opened:  # no close
+            self.err('Unmatched opening bracket',
+                     token=self.tokens[tokpos])
+        if len(self.set_pl):  # save the final set
+            self.save_set()
+        ret = self.cli_list
+        self.cli_list = []
+        return ret
+
+
+class Validation(object):
+    def resource_roles(self):
+        'returns list of valid resource roles'
+        return schema.rng_attr_values('resource_set', 'role')
+
+    def resource_actions(self):
+        'returns list of valid resource actions'
+        return schema.rng_attr_values('resource_set', 'action')
+
+    def date_ops(self):
+        'returns list of valid date operations'
+        return schema.rng_attr_values_l('date_expression', 'operation')
+
+    def expression_types(self):
+        'returns list of valid expression types'
+        return schema.rng_attr_values_l('expression', 'type')
+
+    def rsc_order_kinds(self):
+        return schema.rng_attr_values('rsc_order', 'kind')
+
+    def class_provider_type(self, value):
+        """
+        Unravel [class:[provider:]]type
+        returns: (class, provider, type)
+        """
+        c_p_t = disambiguate_ra_type(value)
+        if not ra_type_validate(value, *c_p_t):
+            return None
+        return c_p_t
+
+    def canonize(self, value, lst):
+        'case-normalizes value to what is in lst'
+        value = value.lower()
+        for x in lst:
+            if value == x.lower():
+                return x
+        return None
+
+    def classify_role(self, role):
+        if not role:
+            return role, None
+        elif role in olist(self.resource_roles()):
+            return self.canonize(role, self.resource_roles()), 'role'
+        elif role.isdigit():
+            return role, 'instance'
+        return role, None
+
+    def classify_action(self, action):
+        if not action:
+            return action, None
+        elif action in olist(self.resource_actions()):
+            return self.canonize(action, self.resource_actions()), 'action'
+        elif action.isdigit():
+            return action, 'instance'
+        return action, None
+
+    def op_attributes(self):
+        return olist(schema.get('attr', 'op', 'a'))
+
+    def acl_2_0(self):
+        vname = schema.validate_name()
+        sp = vname.split('-')
+        try:
+            return sp[0] == 'pacemaker' and sp[1] == 'next' or float(sp[1]) >= 2.0
+        except Exception:
+            return False
+
+    def node_type_optional(self):
+        ns = {'t': 'http://relaxng.org/ns/structure/1.0'}
+        path = '//t:element[@name="nodes"]'
+        path = path + '//t:element[@name="node"]/t:optional/t:attribute[@name="type"]'
+        has_optional = schema.rng_xpath(path, namespaces=ns)
+        return len(has_optional) > 0
+
+
+class CliParser(object):
+    parsers = {}
+
+    def __init__(self):
+        self.comments = []
+        validation = Validation()
+        if not self.parsers:
+            def add(*parsers):
+                for pcls in parsers:
+                    p = pcls()
+                    p.init(validation)
+                    for n in p.can_parse():
+                        self.parsers[n] = p
+            add(ResourceParser,
+                ConstraintParser,
+                OpParser,
+                NodeParser,
+                PropertyParser,
+                FencingOrderParser,
+                AclParser,
+                RawXMLParser,
+                TagParser)
+
+    def _xml_lex(self, s):
+        try:
+            l = lines2cli(s)
+            a = []
+            for p in l:
+                a += p.split()
+            return a
+        except ValueError, e:
+            common_err(e)
+            return False
+
+    def _normalize(self, s):
+        '''
+        Handles basic normalization of the input string.
+        Converts unicode to ascii, XML data to CLI format,
+        lexing etc.
+        '''
+        if isinstance(s, unicode):
+            try:
+                s = s.encode('ascii')
+            except Exception, e:
+                common_err(e)
+                return False
+        if isinstance(s, str):
+            if s and s.startswith('#'):
+                self.comments.append(s)
+                return None
+            if s.startswith('xml'):
+                s = self._xml_lex(s)
+            else:
+                s = shlex.split(s)
+        # but there shouldn't be any newlines (?)
+        while '\n' in s:
+            s.remove('\n')
+        if s:
+            s[0] = s[0].lower()
+        return s
+
+    def parse(self, s):
+        '''
+        Input: a list of tokens (or a CLI format string).
+        Return: a cibobject
+            On failure, returns either False or None.
+        '''
+        s = self._normalize(s)
+        if not s:
+            return s
+        kw = s[0]
+        if kw in self.parsers:
+            parser = self.parsers[kw]
+            try:
+                ret = parser.do_parse(s)
+                if ret is not None and len(self.comments) > 0:
+                    if ret.tag in constants.defaults_tags:
+                        xmlutil.stuff_comments(ret[0], self.comments)
+                    else:
+                        xmlutil.stuff_comments(ret, self.comments)
+                    self.comments = []
+                return ret
+            except ParseError:
+                return False
+        syntax_err(s, token=s[0], msg="Unknown command")
+        return False
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/ra.py b/modules/ra.py
new file mode 100644
index 0000000..137f7e9
--- /dev/null
+++ b/modules/ra.py
@@ -0,0 +1,836 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import subprocess
+import copy
+from lxml import etree
+import re
+import glob
+import cache
+import constants
+import config
+import options
+import userdir
+import utils
+from utils import stdout2list, is_program, is_process, add_sudo
+from utils import os_types_list, get_stdout, find_value
+from utils import crm_msec, crm_time_cmp
+from msg import common_debug, common_err, common_warn, common_info
+
+#
+# Resource Agents interface (meta-data, parameters, etc)
+#
+lrmadmin_prog = "lrmadmin"
+
+
+class RaLrmd(object):
+    '''
+    Getting information from the resource agents.
+    '''
+    def __init__(self):
+        self.good = self.is_lrmd_accessible()
+
+    def lrmadmin(self, opts, xml=False):
+        '''
+        Get information directly from lrmd using lrmadmin.
+        '''
+        rc, l = stdout2list("%s %s" % (lrmadmin_prog, opts))
+        if l and not xml:
+            l = l[1:]  # skip the first line
+        return l
+
+    def is_lrmd_accessible(self):
+        if not (is_program(lrmadmin_prog) and is_process("lrmd")):
+            return False
+        cmd = add_sudo(">/dev/null 2>&1 %s -C" % lrmadmin_prog)
+        if options.regression_tests:
+            print ".EXT", cmd
+        return subprocess.call(
+            cmd,
+            shell=True) == 0
+
+    def meta(self, ra_class, ra_type, ra_provider):
+        return self.lrmadmin("-M %s %s %s" % (ra_class, ra_type, ra_provider), True)
+
+    def providers(self, ra_type, ra_class="ocf"):
+        'List of providers for a class:type.'
+        return self.lrmadmin("-P %s %s" % (ra_class, ra_type), True)
+
+    def classes(self):
+        'List of classes.'
+        return self.lrmadmin("-C")
+
+    def types(self, ra_class="ocf", ra_provider=""):
+        'List of types for a class.'
+        return self.lrmadmin("-T %s" % ra_class)
+
+
+class RaOS(object):
+    '''
+    Getting information from the resource agents (direct).
+    '''
+    def __init__(self):
+        self.good = True
+
+    def meta(self, ra_class, ra_type, ra_provider):
+        l = []
+        if ra_class == "ocf":
+            rc, l = stdout2list("%s/resource.d/%s/%s meta-data" %
+                                (os.environ["OCF_ROOT"], ra_provider, ra_type))
+        elif ra_class == "stonith":
+            if ra_type.startswith("fence_") and os.path.exists("/usr/sbin/%s" % ra_type):
+                rc, l = stdout2list("/usr/sbin/%s -o metadata" % ra_type)
+            else:
+                rc, l = stdout2list("stonith -m -t %s" % ra_type)
+        elif ra_class == "nagios":
+            rc, l = stdout2list("%s/check_%s --metadata" %
+                                (config.path.nagios_plugins, ra_type))
+        return l
+
+    def providers(self, ra_type, ra_class="ocf"):
+        'List of providers for a class:type.'
+        l = []
+        if ra_class == "ocf":
+            for s in glob.glob("%s/resource.d/*/%s" % (os.environ["OCF_ROOT"], ra_type)):
+                a = s.split("/")
+                if len(a) == 7:
+                    l.append(a[5])
+        return l
+
+    def classes(self):
+        'List of classes.'
+        return "heartbeat lsb nagios ocf stonith".split()
+
+    def types(self, ra_class="ocf", ra_provider=""):
+        'List of types for a class.'
+        l = []
+        prov = ra_provider and ra_provider or "*"
+        if ra_class == "ocf":
+            l = os_types_list("%s/resource.d/%s/*" % (os.environ["OCF_ROOT"], prov))
+        elif ra_class == "lsb":
+            l = os_types_list("/etc/init.d/*")
+        elif ra_class == "stonith":
+            rc, l = stdout2list("stonith -L")
+            if rc != 0:
+                # stonith(8) may not be installed
+                common_debug("stonith exited with code %d" % rc)
+                l = []
+            for ra in os_types_list("/usr/sbin/fence_*"):
+                if ra not in ("fence_ack_manual", "fence_pcmk", "fence_legacy"):
+                    l.append(ra)
+        elif ra_class == "nagios":
+            l = os_types_list("%s/check_*" % config.path.nagios_plugins)
+            l = [x.replace("check_", "") for x in l]
+        l = list(set(l))
+        l.sort()
+        return l
+
+
+class RaCrmResource(object):
+    '''
+    Getting information from the resource agents via new crm_resource.
+    '''
+    def __init__(self):
+        self.good = True
+
+    def crm_resource(self, opts):
+        '''
+        Get information from crm_resource.
+        '''
+        rc, l = stdout2list("crm_resource %s" % opts, stderr_on=False)
+        # not clear when/why crm_resource exits with non-zero
+        # code
+        #if rc != 0:
+        #    common_debug("crm_resource %s exited with code %d" %
+        #                 (opts, rc))
+        return l
+
+    def meta(self, ra_class, ra_type, ra_provider):
+        if ra_provider:
+            return self.crm_resource("--show-metadata %s:%s:%s" % (ra_class, ra_provider, ra_type))
+        else:
+            return self.crm_resource("--show-metadata %s:%s" % (ra_class, ra_type))
+
+    def providers(self, ra_type, ra_class="ocf"):
+        'List of providers for OCF:type.'
+        if ra_class != "ocf":
+            common_err("no providers for class %s" % ra_class)
+            return []
+        return self.crm_resource("--list-ocf-alternatives %s" % ra_type)
+
+    def classes(self):
+        'List of classes.'
+        l = self.crm_resource("--list-standards")
+        return l
+
+    def types(self, ra_class="ocf", ra_provider=""):
+        'List of types for a class.'
+        if ra_provider:
+            return self.crm_resource("--list-agents %s:%s" % (ra_class, ra_provider))
+        else:
+            return self.crm_resource("--list-agents %s" % ra_class)
+
+
+def can_use_lrmadmin():
+    from distutils import version
+    # after this glue release all users can get meta-data and
+    # similar from lrmd
+    minimum_glue = "1.0.10"
+    rc, glue_ver = get_stdout("%s -v" % lrmadmin_prog, stderr_on=False)
+    if not glue_ver:  # lrmadmin probably not found
+        return False
+    v_min = version.LooseVersion(minimum_glue)
+    v_this = version.LooseVersion(glue_ver)
+    return v_this >= v_min or \
+        (userdir.getuser() in ("root", config.path.crm_daemon_user))
+
+
+def crm_resource_support():
+    rc, s = get_stdout("crm_resource --list-standards", stderr_on=False)
+    return s != ""
+
+
+def ra_if():
+    if constants.ra_if:
+        return constants.ra_if
+    if crm_resource_support():
+        constants.ra_if = RaCrmResource()
+    elif can_use_lrmadmin():
+        constants.ra_if = RaLrmd()
+    if not constants.ra_if or not constants.ra_if.good:
+        constants.ra_if = RaOS()
+    return constants.ra_if
+
+
+def ra_classes():
+    '''
+    List of RA classes.
+    '''
+    if cache.is_cached("ra_classes"):
+        return cache.retrieve("ra_classes")
+    l = ra_if().classes()
+    l.sort()
+    return cache.store("ra_classes", l)
+
+
+def ra_providers(ra_type, ra_class="ocf"):
+    'List of providers for a class:type.'
+    id = "ra_providers-%s-%s" % (ra_class, ra_type)
+    if cache.is_cached(id):
+        return cache.retrieve(id)
+    l = ra_if().providers(ra_type, ra_class)
+    l.sort()
+    return cache.store(id, l)
+
+
+def ra_providers_all(ra_class="ocf"):
+    '''
+    List of providers for a class.
+    '''
+    id = "ra_providers_all-%s" % ra_class
+    if cache.is_cached(id):
+        return cache.retrieve(id)
+    dir = "%s/resource.d" % os.environ["OCF_ROOT"]
+    l = []
+    for s in os.listdir(dir):
+        if os.path.isdir("%s/%s" % (dir, s)):
+            l.append(s)
+    l.sort()
+    return cache.store(id, l)
+
+
+def ra_types(ra_class="ocf", ra_provider=""):
+    '''
+    List of RA type for a class.
+    '''
+    if not ra_class:
+        ra_class = "ocf"
+    id = "ra_types-%s-%s" % (ra_class, ra_provider)
+    if cache.is_cached(id):
+        return cache.retrieve(id)
+    list = []
+    for ra in ra_if().types(ra_class):
+        if (not ra_provider or
+                ra_provider in ra_providers(ra, ra_class)) \
+                and ra not in list:
+            list.append(ra)
+    list.sort()
+    return cache.store(id, list)
+
+
+def get_pe_meta():
+    if not constants.pe_metadata:
+        constants.pe_metadata = RAInfo("pengine", "metadata")
+    return constants.pe_metadata
+
+
+def get_crmd_meta():
+    if not constants.crmd_metadata:
+        constants.crmd_metadata = RAInfo("crmd", "metadata")
+        constants.crmd_metadata.exclude_from_completion(
+            constants.crmd_metadata_do_not_complete)
+    return constants.crmd_metadata
+
+
+def get_stonithd_meta():
+    if not constants.stonithd_metadata:
+        constants.stonithd_metadata = RAInfo("stonithd", "metadata")
+    return constants.stonithd_metadata
+
+
+def get_cib_meta():
+    if not constants.cib_metadata:
+        constants.cib_metadata = RAInfo("cib", "metadata")
+    return constants.cib_metadata
+
+
+def get_properties_meta():
+    if not constants.crm_properties_metadata:
+        get_pe_meta()
+        get_crmd_meta()
+        get_cib_meta()
+        constants.crm_properties_metadata = copy.deepcopy(constants.crmd_metadata)
+        constants.crm_properties_metadata.add_ra_params(constants.pe_metadata)
+        constants.crm_properties_metadata.add_ra_params(constants.cib_metadata)
+    return constants.crm_properties_metadata
+
+
+def get_properties_list():
+    try:
+        return get_properties_meta().params().keys()
+    except:
+        return []
+
+
+def prog_meta(prog):
+    '''
+    Do external program metadata.
+    '''
+    prog = is_program(prog)
+    if prog:
+        rc, l = stdout2list("%s metadata" % prog)
+        if rc == 0:
+            return l
+        common_debug("%s metadata exited with code %d" % (prog, rc))
+    return []
+
+
+def get_nodes_text(n, tag):
+    try:
+        return n.findtext(tag).strip()
+    except:
+        return ''
+
+
+def mk_monitor_name(role, depth):
+    depth = depth != "0" and ("_%s" % depth) or ""
+    return role and role != "Started" and \
+        "monitor_%s%s" % (role, depth) or \
+        "monitor%s" % depth
+
+
+def monitor_name_node(node):
+    depth = node.get("depth") or '0'
+    role = node.get("role")
+    return mk_monitor_name(role, depth)
+
+
+def monitor_name_pl(pl):
+    depth = find_value(pl, "depth") or '0'
+    role = find_value(pl, "role")
+    return mk_monitor_name(role, depth)
+
+
+class RAInfo(object):
+    '''
+    A resource agent and whatever's useful about it.
+    '''
+    ra_tab = "    "  # four horses
+    required_ops = ("start", "stop")
+    skip_ops = ("meta-data", "validate-all")
+    skip_op_attr = ("name", "depth", "role")
+
+    def __init__(self, ra_class, ra_type, ra_provider="heartbeat"):
+        self.excluded_from_completion = []
+        self.ra_class = ra_class
+        self.ra_type = ra_type
+        self.ra_provider = ra_provider
+        if ra_class == 'ocf' and not self.ra_provider:
+            self.ra_provider = "heartbeat"
+        self.ra_elem = None
+        self.broken_ra = False
+
+    def ra_string(self):
+        return self.ra_class == "ocf" and \
+            "%s:%s:%s" % (self.ra_class, self.ra_provider, self.ra_type) or \
+            "%s:%s" % (self.ra_class, self.ra_type)
+
+    def error(self, s):
+        common_err("%s: %s" % (self.ra_string(), s))
+
+    def warn(self, s):
+        common_warn("%s: %s" % (self.ra_string(), s))
+
+    def info(self, s):
+        common_info("%s: %s" % (self.ra_string(), s))
+
+    def debug(self, s):
+        common_debug("%s: %s" % (self.ra_string(), s))
+
+    def exclude_from_completion(self, l):
+        """
+        Exclude given list of metadata params
+        from completion
+        """
+        self.excluded_from_completion = l
+
+    def add_ra_params(self, ra):
+        '''
+        Add parameters from another RAInfo instance.
+        '''
+        try:
+            if self.mk_ra_node() is None or ra.mk_ra_node() is None:
+                return
+        except:
+            return
+        try:
+            params_node = self.ra_elem.findall("parameters")[0]
+        except:
+            params_node = etree.SubElement(self.ra_elem, "parameters")
+        for n in ra.ra_elem.xpath("//parameters/parameter"):
+            params_node.append(copy.deepcopy(n))
+
+    def mk_ra_node(self):
+        '''
+        Return the resource_agent node.
+        '''
+        if self.ra_elem is not None:
+            return self.ra_elem
+        # don't try again in vain
+        if self.broken_ra:
+            return None
+        self.broken_ra = True
+        meta = self.meta()
+        try:
+            self.ra_elem = etree.fromstring('\n'.join(meta))
+        except Exception:
+            if not meta:
+                if not config.core.ignore_missing_metadata:
+                    self.error("got no meta-data, does this RA exist?")
+            else:
+                self.error("meta-data is no good XML")
+            return None
+        try:
+            assert self.ra_elem.tag == 'resource-agent'
+        except Exception:
+            self.error("meta-data contains no resource-agent element")
+            return None
+        if self.ra_class == "stonith":
+            self.add_ra_params(get_stonithd_meta())
+        self.broken_ra = False
+        return self.ra_elem
+
+    def param_type_default(self, n):
+        try:
+            content = n.find("content")
+            type = content.get("type")
+            default = content.get("default")
+            return type, default
+        except:
+            return None, None
+
+    def params(self):
+        '''
+        Construct a dict of dicts: parameters are keys and
+        dictionary of attributes/values are values. Cached too.
+        '''
+        id = "ra_params-%s" % self.ra_string()
+        if cache.is_cached(id):
+            return cache.retrieve(id)
+        if self.mk_ra_node() is None:
+            return None
+        d = {}
+        for c in self.ra_elem.xpath("//parameters/parameter"):
+            name = c.get("name")
+            if not name:
+                continue
+            required = c.get("required")
+            unique = c.get("unique")
+            type, default = self.param_type_default(c)
+            d[name] = {
+                "required": required,
+                "unique": unique,
+                "type": type,
+                "default": default,
+            }
+        return cache.store(id, d)
+
+    def completion_params(self):
+        '''
+        Extra method for completion, for we want to filter some
+        (advanced) parameters out. And we want this to be fast.
+        '''
+        if self.mk_ra_node() is None:
+            return None
+        return [c.get("name")
+                for c in self.ra_elem.xpath("//parameters/parameter")
+                if c.get("name")
+                and c.get("name") not in self.excluded_from_completion]
+
+    def actions(self):
+        '''
+        Construct a dict of dicts: actions are keys and
+        dictionary of attributes/values are values. Cached too.
+        '''
+        id = "ra_actions-%s" % self.ra_string()
+        if cache.is_cached(id):
+            return cache.retrieve(id)
+        if self.mk_ra_node() is None:
+            return None
+        d = {}
+        for c in self.ra_elem.xpath("//actions/action"):
+            name = c.get("name")
+            if not name or name in self.skip_ops:
+                continue
+            if name == "monitor":
+                name = monitor_name_node(c)
+            d[name] = {}
+            for a in c.attrib.keys():
+                if a in self.skip_op_attr:
+                    continue
+                v = c.get(a)
+                if v:
+                    d[name][a] = v
+        # add monitor ops without role, if they don't already
+        # exist
+        d2 = {}
+        for op in d.keys():
+            if re.match("monitor_[^0-9]", op):
+                norole_op = re.sub(r'monitor_[^0-9_]+_(.*)', r'monitor_\1', op)
+                if norole_op not in d:
+                    d2[norole_op] = d[op]
+        d.update(d2)
+        return cache.store(id, d)
+
+    def reqd_params_list(self):
+        '''
+        List of required parameters.
+        '''
+        d = self.params()
+        if not d:
+            return []
+        return [x for x in d if d[x]["required"] == '1']
+
+    def param_default(self, pname):
+        '''
+        Parameter's default.
+        '''
+        d = self.params()
+        try:
+            return d[pname]["default"]
+        except:
+            return None
+
+    def unreq_param(self, p):
+        '''
+        Allow for some exceptions.
+
+        - the rhcs stonith agents sometimes require "action" (in
+          the meta-data) and "port", but they're automatically
+          supplied by stonithd
+        '''
+        if self.ra_class == "stonith" and \
+            (self.ra_type.startswith("rhcs/") or
+             self.ra_type.startswith("fence_")):
+            if p in ("action", "port"):
+                return True
+        return False
+
+    def sanity_check_params(self, id, nvpairs, existence_only=False):
+        '''
+        nvpairs is a list of <nvpair> tags.
+        - are all required parameters defined
+        - do all parameters exist
+        '''
+        rc = 0
+        d = {}
+        for nvp in nvpairs:
+            if 'name' in nvp.attrib and 'value' in nvp.attrib:
+                d[nvp.get('name')] = nvp.get('value')
+        if not existence_only:
+            for p in self.reqd_params_list():
+                if self.unreq_param(p):
+                    continue
+                if p not in d:
+                    common_err("%s: required parameter %s not defined" % (id, p))
+                    rc |= utils.get_check_rc()
+        for p in d:
+            if p.startswith("$"):
+                # these are special, non-RA parameters
+                continue
+            if p not in self.params():
+                common_err("%s: parameter %s does not exist" % (id, p))
+                rc |= utils.get_check_rc()
+        return rc
+
+    def get_adv_timeout(self, op, node=None):
+        if node is not None and op == "monitor":
+            name = monitor_name_node(node)
+        else:
+            name = op
+        try:
+            return self.actions()[name]["timeout"]
+        except:
+            return None
+
+    def sanity_check_ops(self, id, ops, default_timeout):
+        '''
+        ops is a list of operations
+        - do all operations exist
+        - are timeouts sensible
+        '''
+        rc = 0
+        n_ops = {}
+        for op in ops:
+            n_op = op[0] == "monitor" and monitor_name_pl(op[1]) or op[0]
+            n_ops[n_op] = {}
+            for p, v in op[1]:
+                if p in self.skip_op_attr:
+                    continue
+                n_ops[n_op][p] = v
+        for req_op in self.required_ops:
+            if req_op not in n_ops:
+                if not (self.ra_class == "stonith" and req_op in ("start", "stop")):
+                    n_ops[req_op] = {}
+        intervals = {}
+        for op in n_ops:
+            if self.ra_class == "stonith" and op in ("start", "stop"):
+                continue
+            if op not in self.actions():
+                common_warn("%s: action %s not advertised in meta-data, it may not be supported by the RA"  % (id, op))
+                rc |= 1
+            if "interval" in n_ops[op]:
+                v = n_ops[op]["interval"]
+                v_msec = crm_msec(v)
+                if op in ("start", "stop") and v_msec != 0:
+                    common_warn("%s: Specified interval for %s is %s, it must be 0" % (id, op, v))
+                    rc |= 1
+                if op.startswith("monitor") and v_msec != 0:
+                    if v_msec not in intervals:
+                        intervals[v_msec] = 1
+                    else:
+                        common_warn("%s: interval in %s must be unique" % (id, op))
+                        rc |= 1
+            try:
+                adv_timeout = self.actions()[op]["timeout"]
+            except:
+                continue
+            if "timeout" in n_ops[op]:
+                v = n_ops[op]["timeout"]
+                timeout_string = "specified timeout"
+            else:
+                v = default_timeout
+                timeout_string = "default timeout"
+            if crm_msec(v) < 0:
+                continue
+            if crm_time_cmp(adv_timeout, v) > 0:
+                common_warn("%s: %s %s for %s is smaller than the advised %s" %
+                            (id, timeout_string, v, op, adv_timeout))
+                rc |= 1
+        return rc
+
+    def meta(self):
+        '''
+        RA meta-data as raw xml.
+        '''
+        sid = "ra_meta-%s" % self.ra_string()
+        if cache.is_cached(sid):
+            return cache.retrieve(sid)
+        if self.ra_class in constants.meta_progs:
+            l = prog_meta(self.ra_class)
+        else:
+            l = ra_if().meta(self.ra_class, self.ra_type, self.ra_provider)
+        if not l:
+            return None
+        self.debug("read and cached meta-data")
+        return cache.store(sid, l)
+
+    def meta_pretty(self):
+        '''
+        Print the RA meta-data in a human readable form.
+        '''
+        if self.mk_ra_node() is None:
+            return ''
+        l = []
+        title = self.meta_title()
+        l.append(title)
+        longdesc = get_nodes_text(self.ra_elem, "longdesc")
+        if longdesc:
+            l.append(longdesc)
+        if self.ra_class != "heartbeat":
+            params = self.meta_parameters()
+            if params:
+                l.append(params.rstrip())
+        actions = self.meta_actions()
+        if actions:
+            l.append(actions)
+        return '\n\n'.join(l)
+
+    def get_shortdesc(self, n):
+        name = n.get("name")
+        shortdesc = get_nodes_text(n, "shortdesc")
+        longdesc = get_nodes_text(n, "longdesc")
+        if shortdesc and shortdesc not in (name, longdesc, self.ra_type):
+            return shortdesc
+        return ''
+
+    def meta_title(self):
+        s = self.ra_string()
+        shortdesc = self.get_shortdesc(self.ra_elem)
+        if shortdesc:
+            s = "%s (%s)" % (shortdesc, s)
+        return s
+
+    def meta_param_head(self, n):
+        name = n.get("name")
+        if not name:
+            return None
+        s = name
+        if n.get("required") == "1":
+            s = s + "*"
+        typ, default = self.param_type_default(n)
+        if typ and default:
+            s = "%s (%s, [%s])" % (s, typ, default)
+        elif typ:
+            s = "%s (%s)" % (s, typ)
+        shortdesc = self.get_shortdesc(n)
+        s = "%s: %s" % (s, shortdesc)
+        return s
+
+    def format_parameter(self, n):
+        l = []
+        head = self.meta_param_head(n)
+        if not head:
+            self.error("no name attribute for parameter")
+            return ""
+        l.append(head)
+        longdesc = get_nodes_text(n, "longdesc")
+        if longdesc:
+            longdesc = self.ra_tab + longdesc.replace("\n", "\n" + self.ra_tab) + '\n'
+            l.append(longdesc)
+        return '\n'.join(l)
+
+    def meta_parameter(self, param):
+        if self.mk_ra_node() is None:
+            return ''
+        for c in self.ra_elem.xpath("//parameters/parameter"):
+            if c.get("name") == param:
+                return self.format_parameter(c)
+
+    def meta_parameters(self):
+        if self.mk_ra_node() is None:
+            return ''
+        l = []
+        for c in self.ra_elem.xpath("//parameters/parameter"):
+            s = self.format_parameter(c)
+            if s:
+                l.append(s)
+        if l:
+            return "Parameters (*: required, []: default):\n\n" + '\n'.join(l)
+
+    def meta_action_head(self, n):
+        name = n.get("name")
+        if not name or name in self.skip_ops:
+            return ''
+        if name == "monitor":
+            name = monitor_name_node(n)
+        s = "%-13s" % name
+        for a in n.attrib.keys():
+            if a in self.skip_op_attr:
+                continue
+            v = n.get(a)
+            if v:
+                s = "%s %s=%s" % (s, a, v)
+        return s
+
+    def meta_actions(self):
+        l = []
+        for c in self.ra_elem.xpath("//actions/action"):
+            s = self.meta_action_head(c)
+            if s:
+                l.append(self.ra_tab + s)
+        if len(l) == 0:
+            return None
+        return "Operations' defaults (advisory minimum):\n\n" + '\n'.join(l)
+
+
+def get_ra(r):
+    return RAInfo(r.get("class"), r.get("type"), r.get("provider"))
+
+
+#
+# resource type definition
+#
+def ra_type_validate(s, ra_class, provider, rsc_type):
+    '''
+    Only ocf ra class supports providers.
+    '''
+    if not rsc_type:
+        common_err("bad resource type specification %s" % s)
+        return False
+    if ra_class == "ocf":
+        if not provider:
+            common_err("provider could not be determined for %s" % s)
+            return False
+    else:
+        if provider:
+            common_warn("ra class %s does not support providers" % ra_class)
+            return True
+    return True
+
+
+def pick_provider(providers):
+    '''
+    Pick the most appropriate choice from a
+    list of providers, falling back to
+    'heartbeat' if no good choice is found
+    '''
+    if not providers or 'heartbeat' in providers:
+        return 'heartbeat'
+    elif 'pacemaker' in providers:
+        return 'pacemaker'
+    return providers[0]
+
+
+def disambiguate_ra_type(s):
+    '''
+    Unravel [class:[provider:]]type
+    '''
+    l = s.split(':')
+    if not l or len(l) > 3:
+        return ["", "", ""]
+    if len(l) == 3:
+        return l
+    elif len(l) == 2:
+        cl, tp = l
+    else:
+        cl, tp = "ocf", l[0]
+    pr = pick_provider(ra_providers(tp, cl)) if cl == 'ocf' else ''
+    return cl, pr, tp
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/report.py b/modules/report.py
new file mode 100644
index 0000000..7bff564
--- /dev/null
+++ b/modules/report.py
@@ -0,0 +1,1676 @@
+# Copyright (C) 2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import sys
+import time
+import datetime
+import re
+import glob
+import ConfigParser
+
+import config
+import constants
+import userdir
+from msg import common_debug, common_warn, common_err, common_error, common_info, warn_once
+from xmlutil import file2cib_elem, get_rsc_children_ids, get_prim_children_ids
+from xmlutil import compressed_file_to_cib
+from utils import file2str, shortdate, acquire_lock, append_file, ext_cmd, shorttime
+from utils import page_string, release_lock, rmdir_r, parse_time, get_cib_attributes
+from utils import is_pcmk_118, pipe_cmd_nosudo, file_find_by_name
+
+_NO_PSSH = False
+
+try:
+    from crm_pssh import next_loglines, next_peinputs
+except:
+    _NO_PSSH = True
+
+
+YEAR = None
+
+
+#
+# hb_report interface
+#
+# read hb_report generated report, show interesting stuff, search
+# through logs, get PE input files, get log slices (perhaps even
+# coloured nicely!)
+#
+
+
+def mk_re_list(patt_l, repl):
+    'Build a list of regular expressions, replace "%%" with repl'
+    l = []
+    for re_l in patt_l:
+        l += [x.replace("%%", repl) for x in re_l]
+    if not repl:
+        l = [x.replace(".*.*", ".*") for x in l]
+    return l
+
+
+def set_year(ts=None):
+    '''
+    ts: optional time in seconds
+    '''
+    global YEAR
+    YEAR = time.strftime("%Y", time.localtime(ts))
+    common_debug("setting year to %s (ts: %s)" % (YEAR, str(ts)))
+
+
+def make_time(t):
+    '''
+    t: time in seconds / datetime / other
+    returns: time in floating point
+    '''
+    if t is None:
+        return None
+    elif isinstance(t, datetime.datetime):
+        return convert_dt(t)
+    return t
+
+
+def syslog_ts(s):
+    """
+    Finds the timestamp in the given line
+    Returns as floating point, seconds
+    """
+    try:
+        # strptime defaults year to 1900 (sigh)
+        # strptime returns a time_struct
+        tm = time.strptime(' '.join([YEAR] + s.split()[0:3]),
+                           "%Y %b %d %H:%M:%S")
+        return time.mktime(tm)
+    except:  # try the rfc5424
+        try:
+            return convert_dt(parse_time(s.split()[0]))
+        except Exception:
+            common_debug("malformed line: %s" % s)
+            return None
+
+
+def syslog2node(s):
+    '''Get the node from a syslog line.'''
+    try:
+        # strptime defaults year to 1900 (sigh)
+        time.strptime(' '.join(s.split()[0:3]),
+                      "%b %d %H:%M:%S")
+        return s.split()[3]
+    except:  # try the rfc5424
+        try:
+            parse_time(s.split()[0])
+            return s.split()[1]
+        except Exception:
+            return None
+
+
+def seek_to_edge(f, ts, to_end):
+    '''
+    f contains lines with exactly the timestamp ts.
+    Read forward (or backward) till we find the edge.
+    Linear search, but should be short.
+    '''
+    if not to_end:
+        beg = 0
+        while ts == get_timestamp(f):
+            if f.tell() < 1000:
+                f.seek(0)    # otherwise, the seek below throws an exception
+                if beg > 0:  # avoid infinite loop
+                    return   # goes all the way to the top
+                beg += 1
+            else:
+                f.seek(-1000, 1)  # go back 10 or so lines
+    while True:
+        pos = f.tell()
+        s = f.readline()
+        if not s:
+            break
+        curr_ts = syslog_ts(s)
+        if (to_end and curr_ts > ts) or \
+                (not to_end and curr_ts >= ts):
+            break
+    f.seek(pos)
+
+
+def log_seek(f, ts, to_end=False):
+    '''
+    f is an open log. Do binary search for the timestamp.
+    Return the position of the (more or less) first line with an
+    earlier (or later) time.
+    '''
+    first = 0
+    f.seek(0, 2)
+    last = f.tell()
+    if not ts:
+        return to_end and last or first
+    badline = 0
+    maxbadline = 10
+    common_debug("seek %s:%s in %s" %
+                 (time.ctime(ts),
+                  to_end and "end" or "start",
+                  f.name))
+    while first <= last:
+        # we can skip some iterations if it's about few lines
+        if abs(first-last) < 120:
+            break
+        mid = (first+last)/2
+        f.seek(mid)
+        log_ts = get_timestamp(f)
+        if not log_ts:
+            badline += 1
+            if badline > maxbadline:
+                common_warn("giving up on log %s" % f.name)
+                return -1
+            first += 120  # move forward a bit
+            continue
+        if log_ts > ts:
+            last = mid-1
+        elif log_ts < ts:
+            first = mid+1
+        else:
+            seek_to_edge(f, log_ts, to_end)
+            break
+    fpos = f.tell()
+    common_debug("sought to %s (%d)" % (f.readline(), fpos))
+    f.seek(fpos)
+    return fpos
+
+
+def get_timestamp(f):
+    '''
+    Get the whole line from f. The current file position is
+    usually in the middle of the line.
+    Then get timestamp and return it.
+    '''
+    step = 30  # no line should be longer than 30
+    cnt = 1
+    current_pos = f.tell()
+    s = f.readline()
+    if not s:  # EOF?
+        f.seek(-step, 1)  # backup a bit
+        current_pos = f.tell()
+        s = f.readline()
+    while s and current_pos < f.tell():
+        if cnt*step >= f.tell():  # at 0?
+            f.seek(0)
+            break
+        f.seek(-cnt*step, 1)
+        s = f.readline()
+        cnt += 1
+    pos = f.tell()     # save the position ...
+    s = f.readline()   # get the line
+    f.seek(pos)        # ... and move the cursor back there
+    if not s:          # definitely EOF (probably cannot happen)
+        return None
+    return syslog_ts(s)
+
+
+def is_our_log(s, node_l):
+    return syslog2node(s) in node_l
+
+
+def log2node(log):
+    return os.path.basename(os.path.dirname(log))
+
+
+def filter_log(sl, log_l):
+    '''
+    Filter list of messages to get only those from the given log
+    files list.
+    '''
+    node_l = [log2node(x) for x in log_l if x]
+    return [x for x in sl if is_our_log(x, node_l)]
+
+
+def first_log_lines(log_l):
+    '''
+    Return a list of all first lines of the logs.
+    '''
+    f_list = [open(x) for x in log_l if x]
+    l = [x.readline().rstrip() for x in f_list if x]
+    for x in f_list:
+        if x:
+            x.close()
+    return l
+
+
+def last_log_lines(log_l):
+    '''
+    Return a list of all last lines of the logs.
+    '''
+    f_list = [open(x) for x in log_l if x]
+    l = [x.readlines()[-1].rstrip() for x in f_list if x]
+    for x in f_list:
+        if x:
+            x.close()
+    return l
+
+
+def convert_dt(dt):
+    """
+    Convert a datetime object into a floating-point second value
+    """
+    try:
+        ts = time.mktime(dt.timetuple())
+        ts += dt.microsecond / 1000000.0
+        return ts
+    except:
+        return None
+
+
+class LogSyslog(object):
+    '''
+    Slice log, search log.
+    '''
+
+    def __init__(self, central_log, log_l, from_dt, to_dt):
+        self.log_l = log_l
+        self.central_log = central_log
+        self.f = {}
+        self.startpos = {}
+        self.endpos = {}
+        self.cache = {}
+        self.open_logs()
+        self.set_log_timeframe(from_dt, to_dt)
+
+    def open_log(self, log):
+        import bz2
+        import gzip
+        try:
+            if log.endswith(".bz2"):
+                self.f[log] = bz2.BZ2File(log)
+            elif log.endswith(".gz"):
+                self.f[log] = gzip.open(log)
+            else:
+                self.f[log] = open(log)
+        except IOError, msg:
+            common_err("open %s: %s" % (log, msg))
+
+    def open_logs(self):
+        if self.central_log:
+            common_debug("opening central log %s" % self.central_log)
+            self.open_log(self.central_log)
+        else:
+            for log in self.log_l:
+                common_debug("opening log %s" % log)
+                self.open_log(log)
+
+    def set_log_timeframe(self, from_dt, to_dt):
+        '''
+        Convert datetime to timestamps (i.e. seconds), then
+        find out start/end file positions. Logs need to be
+        already open.
+        '''
+        self.from_ts = make_time(from_dt)
+        self.to_ts = make_time(to_dt)
+        bad_logs = []
+        for log in self.f:
+            f = self.f[log]
+            start = log_seek(f, self.from_ts)
+            end = log_seek(f, self.to_ts, to_end=True)
+            if start == -1 or end == -1:
+                bad_logs.append(log)
+            else:
+                self.startpos[f] = start
+                self.endpos[f] = end
+        for log in bad_logs:
+            del self.f[log]
+            self.log_l.remove(log)
+
+    def get_match_line(self, f, patt):
+        '''
+        Get first line from f that matches re_s, but is not
+        behind endpos[f].
+        '''
+        while f.tell() < self.endpos[f]:
+            fpos = f.tell()
+            s = f.readline().rstrip()
+            if not s:
+                continue
+            if not patt or patt.search(s):
+                return s, fpos
+        return '', -1
+
+    def single_log_list(self, f, patt):
+        l = []
+        while True:
+            s = self.get_match_line(f, patt)[0]
+            if not s:
+                return l
+            l.append(s)
+        return l
+
+    def search_logs(self, log_l, re_s=''):
+        '''
+        Search logs for re_s and sort by time.
+        '''
+        try:
+            patt = None
+            if re_s:
+                patt = re.compile(re_s)
+        except re.error, e:
+            common_debug("RE compilation failed: %s" % (e))
+            raise ValueError("Error in search expression")
+
+        # if there's central log, there won't be merge
+        if self.central_log:
+            fl = [self.f[f] for f in self.f]
+        else:
+            fl = [self.f[f] for f in self.f if self.f[f].name in log_l]
+        for f in fl:
+            f.seek(self.startpos[f])
+        # get head lines of all nodes
+        top_line = [self.get_match_line(x, patt)[0] for x in fl]
+        top_line_ts = []
+        rm_idx_l = []
+        # calculate time stamps for head lines
+        for i in range(len(top_line)):
+            if not top_line[i]:
+                rm_idx_l.append(i)
+            else:
+                top_line_ts.append(syslog_ts(top_line[i]))
+        # remove files with no matches found
+        rm_idx_l.reverse()
+        for i in rm_idx_l:
+            del fl[i], top_line[i]
+        common_debug("search <%s> in %s" % (re_s, [f.name for f in fl]))
+        if len(fl) == 0:  # nothing matched ?
+            return []
+        if len(fl) == 1:
+            # no need to merge if there's only one log
+            return [top_line[0]] + self.single_log_list(fl[0], patt)
+        # search through multiple logs, merge sorted by time
+        l = []
+        first = 0
+        while True:
+            for i in range(len(fl)):
+                try:
+                    if i == first:
+                        continue
+                    if top_line_ts[i] and top_line_ts[i] < top_line_ts[first]:
+                        first = i
+                except:
+                    pass
+            if not top_line[first]:
+                break
+            l.append(top_line[first])
+            top_line[first] = self.get_match_line(fl[first], patt)[0]
+            if not top_line[first]:
+                top_line_ts[first] = time.time()
+            else:
+                top_line_ts[first] = syslog_ts(top_line[first])
+        return l
+
+    def get_matches(self, re_l, log_l=None):
+        '''
+        Return a list of log messages which
+        match one of the regexes in re_l.
+        '''
+        if not log_l:
+            log_l = self.log_l
+        re_s = '|'.join(re_l)
+        return filter_log(self.search_logs(log_l, re_s), log_l)
+        # caching is not ready!
+        # gets complicated because of different time frames
+        # (TODO)
+        #if not re_s: # just list logs
+        #    return filter_log(self.search_logs(log_l), log_l)
+        #if re_s not in self.cache: # cache regex search
+        #    self.cache[re_s] = self.search_logs(log_l, re_s)
+        #return filter_log(self.cache[re_s], log_l)
+
+
+def human_date(dt):
+    'Some human date representation. Date defaults to now.'
+    if not dt:
+        dt = datetime.datetime.now()
+    # drop microseconds
+    return re.sub("[.].*", "", "%s %s" % (dt.date(), dt.time()))
+
+
+def is_log(p):
+    return os.path.isfile(p) and os.path.getsize(p) > 0
+
+
+def pe_file_in_range(pe_f, a):
+    pe_num = get_pe_num(pe_f)
+    if not a or (a[0] <= int(pe_num) <= a[1]):
+        return pe_f
+    return None
+
+
+def read_log_info(log):
+    'Read <log>.info and return logfile and next pos'
+    s = file2str("%s.info" % log)
+    try:
+        logf, pos = s.split()
+        return logf, int(pos)
+    except:
+        warn_once("hb_report too old, you need to update cluster-glue")
+        return '', -1
+
+
+def update_loginfo(rptlog, logfile, oldpos, appended_file):
+    'Update <log>.info with new next pos'
+    newpos = oldpos + os.stat(appended_file).st_size
+    try:
+        f = open("%s.info" % rptlog, "w")
+        f.write("%s %d\n" % (logfile, newpos))
+        f.close()
+    except IOError, msg:
+        common_err("couldn't the update %s.info: %s" % (rptlog, msg))
+
+
+def get_pe_num(pe_file):
+    try:
+        return re.search("pe-[^-]+-([0-9]+)[.]", pe_file).group(1)
+    except:
+        return "-1"
+
+
+def run_graph_msg_actions(msg):
+    '''
+    crmd: [13667]: info: run_graph: Transition 399 (Complete=5,
+    Pending=1, Fired=1, Skipped=0, Incomplete=3,
+    Source=...
+    Returns dict: d[Pending]=np, d[Fired]=nf, ...
+    '''
+    d = {}
+    s = msg
+    while True:
+        r = re.search("([A-Z][a-z]+)=([0-9]+)", s)
+        if not r:
+            return d
+        d[r.group(1)] = int(r.group(2))
+        s = s[r.end():]
+
+
+def extract_pe_file(msg):
+    msg_a = msg.split()
+    if len(msg_a) < 5:
+        # this looks too short
+        common_warn("log message <%s> unexpected format, please report a bug" % msg)
+        return ""
+    return msg_a[-1]
+
+
+def get_matching_run_msg(te_invoke_msg, trans_msg_l):
+    run_msg = ""
+    pe_file = extract_pe_file(te_invoke_msg)
+    pe_num = get_pe_num(pe_file)
+    if pe_num == "-1":
+        common_warn("%s: strange, transition number not found" % pe_file)
+        return ""
+    run_patt = constants.transition_patt[1].replace("%%", pe_num)
+    for msg in trans_msg_l:
+        if re.search(run_patt, msg):
+            run_msg = msg
+            break
+    return run_msg
+
+
+def trans_str(node, pe_file):
+    '''Convert node,pe_file to transition sting.'''
+    return "%s:%s" % (node, os.path.basename(pe_file).replace(".bz2", ""))
+
+
+def rpt_pe2t_str(rpt_pe_file):
+    '''Convert report's pe_file path to transition sting.'''
+    node = os.path.basename(os.path.dirname(os.path.dirname(rpt_pe_file)))
+    return trans_str(node, rpt_pe_file)
+
+
+class Transition(object):
+    '''
+    Capture transition related information.
+    '''
+
+    def __init__(self, te_invoke_msg, run_msg):
+        self.te_invoke_msg = te_invoke_msg
+        self.run_msg = run_msg
+        self.parse_msgs()
+
+    def __str__(self):
+        return trans_str(self.dc, self.pe_file)
+
+    def parse_msgs(self):
+        self.pe_file = extract_pe_file(self.te_invoke_msg)
+        self.pe_num = get_pe_num(self.pe_file)
+        self.dc = syslog2node(self.te_invoke_msg)
+        self.start_ts = syslog_ts(self.te_invoke_msg)
+        if self.run_msg:
+            self.end_ts = syslog_ts(self.run_msg)
+        else:
+            common_warn("end of transition %s not found in logs (transition not complete yet?)" %
+                        self)
+            self.end_ts = self.start_ts
+
+    def actions_count(self):
+        if self.run_msg:
+            act_d = run_graph_msg_actions(self.run_msg)
+            return sum(act_d.values())
+        else:
+            return -1
+
+    def shortname(self):
+        return os.path.basename(self.pe_file).replace(".bz2", "")
+
+    def transition_info(self):
+        print "Transition %s (%s -" % (self, shorttime(self.start_ts)),
+        if self.run_msg:
+            print "%s):" % shorttime(self.end_ts)
+            act_d = run_graph_msg_actions(self.run_msg)
+            total = sum(act_d.values())
+            s = ", ".join(["%d %s" % (act_d[x], x) for x in act_d if act_d[x]])
+            print "\ttotal %d actions: %s" % (total, s)
+        else:
+            print "[unfinished])"
+
+
+def mkarchive(dir):
+    "Create an archive from a directory"
+    home = userdir.gethomedir()
+    if not home:
+        common_err("no home directory, nowhere to pack report")
+        return False
+    archive = '%s.tar.bz2' % os.path.join(home, os.path.basename(dir))
+    cmd = "tar -C '%s/..' -cj -f '%s' %s" % \
+        (dir, archive, os.path.basename(dir))
+    if pipe_cmd_nosudo(cmd) != 0:
+        common_err('could not pack report, command "%s" failed' % cmd)
+        return False
+    else:
+        print "Report saved in '%s'" % archive
+    return True
+
+CH_SRC, CH_TIME, CH_UPD = 1, 2, 3
+
+
+class Report(object):
+    '''
+    A hb_report class.
+    '''
+    live_recent = 6*60*60   # recreate live hb_report once every 6 hours
+    short_live_recent = 60  # update once a minute
+    nodecolors = ("NORMAL",
+                  "GREEN",
+                  "CYAN",
+                  "MAGENTA",
+                  "YELLOW",
+                  "WHITE",
+                  "BLUE",
+                  "RED")
+    session_sub = "session"
+    report_cache_dir = os.path.join(config.path.cache, 'history')
+    outdir = os.path.join(config.path.cache, 'history', "psshout")
+    errdir = os.path.join(config.path.cache, 'history', "pssherr")
+
+    def __init__(self):
+        # main source attributes
+        self._creation_time = "--:--:--"
+        self._creator = "unknown"
+        self.source = None
+        self.from_dt = None
+        self.to_dt = None
+        self.log_l = []
+        self.central_log = None
+        self.setnodes = []  # optional
+        # derived
+        self.loc = None
+        self.ready = False
+        self.nodecolor = {}
+        self.logobj = None
+        self.desc = None
+        self.peinputs_l = []
+        self.cibgrp_d = {}
+        self.cibcln_d = {}
+        self.cibrsc_l = []
+        self.cibnotcloned_l = []
+        self.cibcloned_l = []
+        self.node_l = []
+        self.last_live_update = 0
+        self.detail = 0
+        self.log_filter_out = []
+        self.log_filter_out_re = []
+        # change_origin may be CH_SRC, CH_TIME, CH_UPD
+        # depending on the change_origin, we update our attributes
+        self.change_origin = CH_SRC
+        set_year()
+
+    def error(self, s):
+        common_err("%s: %s" % (self.source, s))
+
+    def warn(self, s):
+        common_warn("%s: %s" % (self.source, s))
+
+    def rsc_list(self):
+        return self.cibgrp_d.keys() + self.cibcln_d.keys() + self.cibrsc_l
+
+    def node_list(self):
+        return self.node_l
+
+    def peinputs_list(self):
+        return [x.pe_num for x in self.peinputs_l]
+
+    def session_subcmd_list(self):
+        return ["save", "load", "pack", "delete", "list", "update"]
+
+    def session_list(self):
+        l = os.listdir(self.get_session_dir(None))
+        l.sort()
+        return l
+
+    def unpack_report(self, tarball):
+        '''
+        Unpack hb_report tarball.
+        Don't unpack if the directory already exists!
+        '''
+        bfname = os.path.basename(tarball)
+        parentdir = os.path.dirname(tarball)
+        common_debug("tarball: %s, in dir: %s" % (bfname, parentdir))
+        if bfname.endswith(".tar.bz2"):
+            loc = tarball.replace(".tar.bz2", "")
+            tar_unpack_option = "j"
+        elif bfname.endswith(".tar.gz"):  # hmm, must be ancient
+            loc = tarball.replace(".tar.gz", "")
+            tar_unpack_option = "z"
+        else:
+            self.error("this doesn't look like a report tarball")
+            return None
+        self.set_change_origin(CH_SRC)
+        if os.path.isdir(loc):
+            if (os.stat(tarball).st_mtime - os.stat(loc).st_mtime) < 60:
+                return loc
+            rmdir_r(loc)
+        cwd = os.getcwd()
+        if parentdir:
+            try:
+                os.chdir(parentdir)
+            except OSError, msg:
+                self.error(msg)
+                return None
+        import tarfile
+        try:
+            tf = tarfile.open(bfname)
+            tf_loc = tf.getmembers()[0].name
+            if tf_loc != loc:
+                common_debug("top directory in tarball: %s, doesn't match the tarball name: %s" %
+                             (tf_loc, loc))
+                loc = os.path.join(os.path.dirname(loc), tf_loc)
+        except Exception, msg:
+            common_err("%s: %s" % (tarball, msg))
+            return None
+        common_debug("tar -x%s < %s" % (tar_unpack_option, bfname))
+        rc = pipe_cmd_nosudo("tar -x%s < %s" % (tar_unpack_option, bfname))
+        if self.source == "live":
+            os.remove(bfname)
+        os.chdir(cwd)
+        if rc != 0:
+            return None
+        return loc
+
+    def pe_report_path(self, t_obj):
+        pe_base = os.path.basename(t_obj.pe_file)
+        return os.path.join(self.loc, t_obj.dc, "pengine", pe_base)
+
+    def short_pe_path(self, pe_file):
+        return pe_file.replace("%s/" % self.loc, "")
+
+    def get_nodes(self):
+        return sorted([os.path.basename(p)
+                       for p in os.listdir(self.loc)
+                       if self.find_node_log(p) is not None])
+
+    def check_nodes(self):
+        'Verify if the nodes in cib match the nodes in the report.'
+        nl = self.get_nodes()
+        if not nl:
+            self.error("no nodes in report")
+            return False
+        for n in self.node_l:
+            if n not in nl:
+                self.warn("node %s not in report" % n)
+            else:
+                nl.remove(n)
+        return True
+
+    def check_report(self):
+        '''
+        Check some basic properties of the report.
+        '''
+        if not self.loc:
+            return False
+        if not os.access(self.desc, os.F_OK):
+            self.error("no description file in the report")
+            return False
+        if not self.check_nodes():
+            return False
+        return True
+
+    def _live_loc(self):
+        return os.path.join(self.report_cache_dir, "live")
+
+    def is_live_recent(self):
+        '''
+        Look at the last live hb_report. If it's recent enough,
+        return True.
+        '''
+        try:
+            last_ts = os.stat(self.desc).st_mtime
+            return time.time() - last_ts <= self.live_recent
+        except:
+            return False
+
+    def is_live_very_recent(self):
+        '''
+        Look at the last live hb_report. If it's recent enough,
+        return True.
+        '''
+        return (time.time() - self.last_live_update) <= self.short_live_recent
+
+    def prevent_live_update(self):
+        '''
+        Don't update live report if to_time is set (not open end).
+        '''
+        return self.to_dt is not None
+
+    def find_node_log(self, node):
+        p = os.path.join(self.loc, node)
+        for lf in ("ha-log.txt", "messages", "journal.log"):
+            if is_log(os.path.join(p, lf)):
+                return os.path.join(p, lf)
+        return None
+
+    def find_central_log(self):
+        'Return common log, if found.'
+        central_log = os.path.join(self.loc, "ha-log.txt")
+        if is_log(central_log):
+            logf, pos = read_log_info(central_log)
+            if logf == '':
+                # assume it's not a central log (we don't
+                # know really)
+                return
+            if logf.startswith("synthetic"):
+                # not central log
+                return
+            common_debug("found central log %s" % logf)
+            self.central_log = central_log
+
+    def find_logs(self):
+        'Return a list of logs found (one per node).'
+        l = []
+        for node in self.node_l:
+            log = self.find_node_log(node)
+            if log:
+                l.append(log)
+            else:
+                self.warn("no log found for node %s" % node)
+        return l
+
+    def append_newlogs(self, a):
+        '''
+        Append new logs fetched from nodes.
+        '''
+        if not os.path.isdir(self.outdir):
+            return
+        for node, rptlog, logfile, nextpos in a:
+            fl = glob.glob("%s/*%s*" % (self.outdir, node))
+            if not fl:
+                continue
+            append_file(rptlog, fl[0])
+            update_loginfo(rptlog, logfile, nextpos, fl[0])
+
+    def unpack_new_peinputs(self, node, pe_l):
+        '''
+        Untar PE inputs fetched from nodes.
+        '''
+        if not os.path.isdir(self.outdir):
+            return
+        fl = glob.glob("%s/*%s*" % (self.outdir, node))
+        if not fl:
+            return -1
+        u_dir = os.path.join(self.loc, node)
+        return pipe_cmd_nosudo("tar -C %s -x < %s" % (u_dir, fl[0]))
+
+    def read_new_log(self, node):
+        '''
+        Get a list of log lines.
+        The log is put in self.outdir/node by pssh.
+        '''
+        if not os.path.isdir(self.outdir):
+            return []
+        fl = glob.glob("%s/*%s*" % (self.outdir, node))
+        if not fl:
+            return []
+        try:
+            f = open(fl[0])
+        except IOError, msg:
+            common_err("open %s: %s" % (fl[0], msg))
+            return []
+        return f.readlines()
+
+    def update_live_report(self):
+        '''
+        Update the existing live report, if it's older than
+        self.short_live_recent:
+        - append newer logs
+        - get new PE inputs
+        '''
+        a = []
+        common_info("fetching new logs, please wait ...")
+        for rptlog in self.log_l:
+            node = log2node(rptlog)
+            logf, pos = read_log_info(rptlog)
+            if logf:
+                a.append([node, rptlog, logf, pos])
+        if not a:
+            common_info("no elligible logs found")
+            return False
+        rmdir_r(self.outdir)
+        rmdir_r(self.errdir)
+        self.last_live_update = time.time()
+        rc1 = next_loglines(a, self.outdir, self.errdir)
+        self.append_newlogs(a)
+        node_pe_l = []
+        for node in [x[0] for x in a]:
+            log_l = self.read_new_log(node)
+            if not log_l:
+                continue
+            pe_l = []
+            for new_t_obj in self.list_transitions(log_l, future_pe=True):
+                self.new_peinput(new_t_obj)
+                pe_l.append(new_t_obj.pe_file)
+            if pe_l:
+                node_pe_l.append([node, pe_l])
+        rmdir_r(self.outdir)
+        rmdir_r(self.errdir)
+        if not node_pe_l:
+            return rc1
+        rc2 = next_peinputs(node_pe_l, self.outdir, self.errdir)
+        unpack_rc = 0
+        for node, pe_l in node_pe_l:
+            unpack_rc |= self.unpack_new_peinputs(node, pe_l)
+        rc2 |= (unpack_rc == 0)
+        rmdir_r(self.outdir)
+        rmdir_r(self.errdir)
+        return rc1 and rc2
+
+    def get_live_report(self):
+        if not acquire_lock(self.report_cache_dir):
+            return None
+        loc = self.new_live_report()
+        release_lock(self.report_cache_dir)
+        return loc
+
+    def manage_live_report(self, force=False, no_live_update=False):
+        '''
+        Update or create live report.
+        '''
+        d = self._live_loc()
+        if not d or not os.path.isdir(d):
+            return self.get_live_report()
+        if not self.loc:
+            # the live report is there, but we were just invoked
+            self.loc = d
+            self.report_setup()
+        if not force and self.is_live_recent():
+            # try just to refresh the live report
+            if self.to_dt or self.is_live_very_recent() or no_live_update:
+                return self._live_loc()
+            if not _NO_PSSH:
+                if not acquire_lock(self.report_cache_dir):
+                    return None
+                rc = self.update_live_report()
+                release_lock(self.report_cache_dir)
+                if rc:
+                    self.set_change_origin(CH_UPD)
+                    return self._live_loc()
+            else:
+                warn_once("pssh not installed, slow live updates ahead")
+        return self.get_live_report()
+
+    def new_live_report(self):
+        '''
+        Run hb_report to get logs now.
+        '''
+        d = self._live_loc()
+        rmdir_r(d)
+        tarball = "%s.tar.bz2" % d
+        to_option = ""
+        if self.to_dt:
+            to_option = "-t '%s'" % human_date(self.to_dt)
+        nodes_option = ""
+        if self.setnodes:
+            nodes_option = "'-n %s'" % ' '.join(self.setnodes)
+        if pipe_cmd_nosudo("mkdir -p %s" % os.path.dirname(d)) != 0:
+            return None
+        common_info("retrieving information from cluster nodes, please wait ...")
+        rc = pipe_cmd_nosudo("%s/hb_report -Z -Q -f '%s' %s %s %s" %
+                             (config.path.sharedir,
+                              self.from_dt.ctime(),
+                              to_option,
+                              nodes_option,
+                              d))
+        if rc != 0:
+            if os.path.isfile(tarball):
+                self.warn("hb_report thinks it failed, proceeding anyway")
+            else:
+                self.error("hb_report failed")
+                return None
+        self.last_live_update = time.time()
+        return self.unpack_report(tarball)
+
+    def set_source(self, src):
+        'Set our source.'
+        if self.source != src:
+            self.set_change_origin(CH_SRC)
+            self.source = src
+            self.loc = None
+            self.ready = False
+
+    def set_period(self, from_dt, to_dt):
+        '''
+        Set from/to_dt.
+        '''
+        common_debug("setting report times: <%s> - <%s>" % (from_dt, to_dt))
+        self.from_dt = from_dt
+        self.to_dt = to_dt
+
+        refresh = False
+        if self.source == "live" and self.ready:
+            top_dt = self.get_rpt_dt(None, "top")
+            if top_dt is None:
+                return False
+            refresh = from_dt and top_dt > from_dt
+        if refresh:
+            self.set_change_origin(CH_UPD)
+            self.refresh_source(force=True)
+        else:
+            self.set_change_origin(CH_TIME)
+            self.report_setup()
+        return True
+
+    def set_detail(self, detail_lvl):
+        '''
+        Set the detail level.
+        '''
+        self.detail = int(detail_lvl)
+
+    def set_nodes(self, *args):
+        '''
+        Allow user to set the node list (necessary if the host is
+        not part of the cluster).
+        '''
+        self.setnodes = args
+
+    def get_cib_loc(self):
+        if not self.node_l:
+            return ""
+        return os.path.join(self.loc, self.node_l[0], "cib.xml")
+
+    def read_cib(self):
+        '''
+        Get some information from the report's CIB (node list,
+        resource list, groups). If "live" and not central log,
+        then use cibadmin.
+        '''
+        cib_elem = None
+        cib_f = self.get_cib_loc()
+        if cib_f:
+            cib_elem = file2cib_elem(cib_f)
+        if cib_elem is None:
+            return  # no cib?
+        try:
+            conf = cib_elem.find("configuration")
+        except:  # bad cib?
+            return
+        self.cibrsc_l = [x.get("id")
+                         for x in conf.xpath("//resources//primitive")]
+        self.cibgrp_d = {}
+        for grp in conf.xpath("//resources/group"):
+            self.cibgrp_d[grp.get("id")] = get_rsc_children_ids(grp)
+        self.cibcln_d = {}
+        self.cibcloned_l = []
+        for cln in conf.xpath("//resources/clone") + \
+                conf.xpath("//resources/master"):
+            try:
+                self.cibcln_d[cln.get("id")] = get_prim_children_ids(cln)
+                self.cibcloned_l += self.cibcln_d[cln.get("id")]
+            except:
+                pass
+        self.cibnotcloned_l = [x for x in self.cibrsc_l if x not in self.cibcloned_l]
+
+    def new_peinput(self, new_pe):
+        t_obj = self.find_peinput(str(new_pe))
+        if t_obj:
+            common_debug("duplicate %s, replacing older PE file" % t_obj)
+            self.peinputs_l.remove(t_obj)
+        common_debug("appending new PE %s" % new_pe)
+        self.peinputs_l.append(new_pe)
+
+    def set_node_colors(self):
+        i = 0
+        for n in self.node_l:
+            self.nodecolor[n] = self.nodecolors[i]
+            i = (i+1) % len(self.nodecolors)
+
+    def get_invoke_trans_msgs(self, msg_l):
+        te_invoke_patt = constants.transition_patt[0].replace("%%", "[0-9]+")
+        return [x for x in msg_l if re.search(te_invoke_patt, x)]
+
+    def get_all_trans_msgs(self, msg_l=None):
+        trans_re_l = [x.replace("%%", "[0-9]+") for x in constants.transition_patt]
+        if not msg_l:
+            return self.logobj.get_matches(trans_re_l)
+        else:
+            re_s = '|'.join(trans_re_l)
+            return [x for x in msg_l if re.search(re_s, x)]
+
+    def is_empty_transition(self, t0, t1):
+        num_actions = t1.actions_count()
+        if not (t0 and t1):
+            return num_actions == 0
+        old_pe_l_file = self.pe_report_path(t0)
+        new_pe_l_file = self.pe_report_path(t1)
+        if not os.path.isfile(old_pe_l_file) or not os.path.isfile(new_pe_l_file):
+            return num_actions == 0
+        old_cib = compressed_file_to_cib(old_pe_l_file)
+        new_cib = compressed_file_to_cib(new_pe_l_file)
+        if old_cib is None or new_cib is None:
+            return num_actions == 0
+        prev_epoch = old_cib.attrib.get("epoch", "0")
+        epoch = new_cib.attrib.get("epoch", "0")
+        prev_admin_epoch = old_cib.attrib.get("admin_epoch", "0")
+        admin_epoch = new_cib.attrib.get("admin_epoch", "0")
+        return num_actions == 0 and epoch == prev_epoch and admin_epoch == prev_admin_epoch
+
+    def list_transitions(self, msg_l=None, future_pe=False):
+        '''
+        List transitions by reading logs.
+        Empty transitions are skipped.
+        Some callers need original PE file path (future_pe),
+        otherwise we produce the path within the report and check
+        if the transition files exist.
+        NB: future_pe means that the peinput has not been fetched yet.
+        If the caller doesn't provide the message list, then we
+        build it from the collected log files (self.logobj).
+        Otherwise, we get matches for transition patterns.
+
+        WARN: We rely here on the message format (syslog,
+        pacemaker).
+        '''
+        trans_msg_l = self.get_all_trans_msgs(msg_l)
+        trans_start_msg_l = self.get_invoke_trans_msgs(trans_msg_l)
+        common_debug("Num transition msgs: %s" % (len(trans_msg_l)))
+        common_debug("Num start transition msgs: %s" % (len(trans_start_msg_l)))
+        MANY_TRANSITIONS = 10000
+        progress = False
+        if len(trans_msg_l) > MANY_TRANSITIONS:
+            common_warn("Processing %s transitions..." %
+                        (len(trans_msg_l)))
+            progress = True
+        prev_transition = None
+        for msg in trans_start_msg_l:
+            run_msg = get_matching_run_msg(msg, trans_msg_l)
+            t_obj = Transition(msg, run_msg)
+            if self.is_empty_transition(prev_transition, t_obj):
+                common_debug("skipping empty transition (%s)" % t_obj)
+                continue
+            if not future_pe:
+                pe_l_file = self.pe_report_path(t_obj)
+                if not os.path.isfile(pe_l_file):
+                    warn_once("%s in the logs, but not in the report" % t_obj)
+                    continue
+            common_debug("found PE input: %s" % t_obj)
+            if progress:
+                sys.stdout.write('.')
+                sys.stdout.flush()
+            prev_transition = t_obj
+            yield t_obj
+
+    def report_setup(self):
+        if not self.change_origin:
+            return
+        if not self.loc:
+            return
+        if self.change_origin == CH_SRC:
+            constants.pcmk_version = None
+            # is this an hb_report or a crm_report?
+            for descname in ("description.txt", "report.summary"):
+                self.desc = os.path.join(self.loc, descname)
+                if os.path.isfile(self.desc):
+                    yr = os.stat(self.desc).st_mtime
+                    common_debug("Found %s, created %s" % (descname, yr))
+                    self._creation_time = time.strftime("%a %d %b %H:%M:%S %Z %Y",
+                                                        time.localtime(yr))
+                    if descname == 'report.summary':
+                        self._creator = "crm_report"
+                    else:
+                        self._creator = 'unknown'
+                    set_year(yr)
+                    break
+            else:
+                self.error("Invalid report: No description found")
+                return
+
+            self.node_l = self.get_nodes()
+            self.set_node_colors()
+            self.log_l = self.find_logs()
+            self.find_central_log()
+            self.read_cib()
+        elif self.change_origin == CH_UPD:
+            l = self.get_nodes()
+            if self.node_l != l:
+                self.node_l = l
+                self.set_node_colors()
+                self.log_l = self.find_logs()
+                self.read_cib()
+        self.logobj = LogSyslog(self.central_log,
+                                self.log_l,
+                                self.from_dt,
+                                self.to_dt)
+        if self.change_origin != CH_UPD:
+            common_debug("getting transitions from logs")
+            self.peinputs_l = []
+            for new_t_obj in self.list_transitions():
+                self.new_peinput(new_t_obj)
+        self.ready = self.check_report()
+        self.set_change_origin(0)
+
+    def prepare_source(self, no_live_update=False):
+        '''
+        Unpack a hb_report tarball.
+        For "live", create an ad-hoc hb_report and unpack it
+        somewhere in the cache area.
+        Parse the period.
+        '''
+        if not self.source:
+            common_error("no source set yet")
+            return False
+        if self.ready and (no_live_update or self.source != "live"):
+            return True
+        if self.source == "live":
+            self.loc = self.manage_live_report(no_live_update=no_live_update)
+        elif os.path.isfile(self.source):
+            self.loc = self.unpack_report(self.source)
+        elif os.path.isdir(self.source):
+            self.loc = self.source
+        if not self.loc:
+            return False
+        self.report_setup()
+        return self.ready
+
+    def refresh_source(self, force=False):
+        '''
+        Refresh report from live.
+        '''
+        if self.source != "live":
+            self.error("refresh not supported")
+            return False
+        self.last_live_update = 0
+        self.loc = self.manage_live_report(force=force)
+        self.report_setup()
+        return self.ready
+
+    def get_patt_l(self, type):
+        '''
+        get the list of patterns for this type, up to and
+        including current detail level
+        '''
+        cib_f = None
+        if self.source != "live" or self.central_log:
+            cib_f = self.get_cib_loc()
+        if is_pcmk_118(cib_f=cib_f):
+            from log_patterns_118 import log_patterns
+        else:
+            from log_patterns import log_patterns
+        if type not in log_patterns:
+            common_error("%s not featured in log patterns" % type)
+            return None
+        return log_patterns[type][0:self.detail+1]
+
+    def build_re(self, type, args):
+        '''
+        Prepare a regex string for the type and args.
+        For instance, "resource" and rsc1, rsc2, ...
+        '''
+        patt_l = self.get_patt_l(type)
+        if not patt_l:
+            return None
+        if not args:
+            re_l = mk_re_list(patt_l, "")
+        else:
+            re_l = mk_re_list(patt_l, r'(?:%s)' % "|".join(args))
+        return re_l
+
+    def _str_nodecolor(self, node, s):
+        try:
+            clr = self.nodecolor[node]
+        except:
+            return s
+        try:
+            return "${%s}%s${NORMAL}" % (clr, s)
+        except:
+            s = s.replace("${", "$.{")
+            return "${%s}%s${NORMAL}" % (clr, s)
+
+    def disp(self, s):
+        'color output'
+        node = syslog2node(s)
+        if node is None:
+            return s
+        return self._str_nodecolor(node, s)
+
+    def match_filter_out(self, s):
+        for regexp in self.log_filter_out_re:
+            if regexp.search(s):
+                return True
+        return False
+
+    def display_logs(self, l):
+        if self.log_filter_out_re:
+            l = [x for x in l if not self.match_filter_out(x)]
+        page_string('\n'.join([self.disp(x) for x in l]))
+
+    def show_logs(self, log_l=None, re_l=[]):
+        '''
+        Print log lines, either matched by re_l or all.
+        '''
+        if not log_l:
+            log_l = self.log_l
+        if not self.central_log and not log_l:
+            self.error("no logs found")
+            return
+        self.display_logs(self.logobj.get_matches(re_l, log_l))
+
+    def get_source(self):
+        return self.source
+
+    def get_desc_line(self, fld):
+        try:
+            f = open(self.desc)
+        except IOError, msg:
+            common_err("open %s: %s" % (self.desc, msg))
+            return
+        for s in f:
+            if s.startswith("%s: " % fld):
+                f.close()
+                s = s.replace("%s: " % fld, "").rstrip()
+                return s
+        f.close()
+
+    def short_peinputs_list(self):
+        '''There could be quite a few transitions, limit the
+        output'''
+        max_output = 20
+        s = ""
+        if len(self.peinputs_l) > max_output:
+            s = "... "
+        return "%s%s" % (s,
+                         ' '.join([self._str_nodecolor(x.dc, x.pe_num)
+                                   for x in self.peinputs_l[-max_output:]]))
+
+    def get_rpt_dt(self, dt, whence):
+        '''
+        Figure out the time of the start/end of the report.
+        The ts input is the time stamp set by user (it can be
+        empty). whence is set either to "top" or "bottom".
+        '''
+        if dt:
+            return dt
+        try:
+            if whence == "top":
+                myts = min([syslog_ts(x) for x in first_log_lines(self.log_l)])
+            elif whence == "bottom":
+                myts = max([syslog_ts(x) for x in last_log_lines(self.log_l)])
+            if myts:
+                return datetime.datetime.fromtimestamp(myts)
+            common_debug("No log lines with timestamps found in report")
+        except Exception, e:
+            common_debug("Error: %s" % (e))
+        return None
+
+    def _str_dt(self, dt):
+        return dt and human_date(dt) or "--/--/-- --:--:--"
+
+    def info(self):
+        '''
+        Print information about the source.
+        '''
+        if not self.prepare_source():
+            return False
+
+        created_on = self.get_desc_line("Date") or self._creation_time
+        created_by = self.get_desc_line("By") or self._creator
+
+        page_string('\n'.join(("Source: %s" % self.source,
+                               "Created on: %s" % (created_on),
+                               "By: %s" % (created_by),
+                               "Period: %s - %s" %
+                               (self._str_dt(self.get_rpt_dt(self.from_dt, "top")),
+                                self._str_dt(self.get_rpt_dt(self.to_dt, "bottom"))),
+                               "Nodes: %s" % ' '.join([self._str_nodecolor(x, x)
+                                                       for x in self.node_l]),
+                               "Groups: %s" % ' '.join(self.cibgrp_d.keys()),
+                               "Resources: %s" % ' '.join(self.cibrsc_l),
+                               "Transitions: %s" % self.short_peinputs_list()
+                               )))
+
+    def events(self):
+        '''
+        Show all events.
+        '''
+        rsc_l = self.cibnotcloned_l
+        rsc_l += ["%s(?::[0-9]+)?" % x for x in self.cibcloned_l]
+        all_re_l = self.build_re("resource", rsc_l) + \
+            self.build_re("node", self.node_l) + \
+            self.build_re("events", [])
+        if not all_re_l:
+            self.error("no resources or nodes found")
+            return False
+        self.show_logs(re_l=all_re_l)
+
+    def find_peinput(self, t_str):
+        for t_obj in self.peinputs_l:
+            if str(t_obj) == t_str:
+                return t_obj
+        return None
+
+    def show_transition_log(self, rpt_pe_file, full_log=False):
+        '''
+        Search for events within the given transition.
+        '''
+        if not self.prepare_source(no_live_update=self.prevent_live_update()):
+            return False
+        t_obj = self.find_peinput(rpt_pe2t_str(rpt_pe_file))
+        if not t_obj:
+            common_err("%s: transition not found" % rpt_pe_file)
+            return False
+        # limit the log scope temporarily
+        self.logobj.set_log_timeframe(t_obj.start_ts, t_obj.end_ts)
+        if full_log:
+            self.show_logs()
+        else:
+            t_obj.transition_info()
+            self.events()
+        self.logobj.set_log_timeframe(self.from_dt, self.to_dt)
+        return True
+
+    def resource(self, *args):
+        '''
+        Show resource relevant logs.
+        '''
+        if not self.prepare_source(no_live_update=self.prevent_live_update()):
+            return False
+        # expand groups (if any)
+        expanded_l = []
+        for a in args:
+            # add group members, groups aren't logged
+            if a in self.cibgrp_d:
+                expanded_l += self.cibgrp_d[a]
+            # add group members, groups aren't logged
+            elif a in self.cibcln_d:
+                expanded_l += self.cibcln_d[a]
+            else:
+                expanded_l.append(a)
+        exp_cloned_l = []
+        for rsc in expanded_l:
+            if rsc in self.cibcloned_l:
+                exp_cloned_l.append("%s(?::[0-9]+)?" % rsc)
+            else:
+                exp_cloned_l.append(rsc)
+        rsc_re_l = self.build_re("resource", exp_cloned_l)
+        if not rsc_re_l:
+            return False
+        self.show_logs(re_l=rsc_re_l)
+
+    def node(self, *args):
+        '''
+        Show node relevant logs.
+        '''
+        if not self.prepare_source(no_live_update=self.prevent_live_update()):
+            return False
+        node_re_l = self.build_re("node", args)
+        if not node_re_l:
+            return False
+        self.show_logs(re_l=node_re_l)
+
+    def log(self, *args):
+        '''
+        Show logs for a node or all nodes.
+        '''
+        if not self.prepare_source():
+            return False
+        if not args:
+            self.show_logs()
+        else:
+            l = []
+            for n in args:
+                if n not in self.node_l:
+                    self.warn("%s: no such node" % n)
+                    continue
+                l.append(self.find_node_log(n))
+            if not l:
+                return False
+            self.show_logs(log_l=l)
+
+    pe_details_header = \
+      "Date       Start    End       Filename      Client     User       Origin"
+    pe_details_separator = \
+      "====       =====    ===       ========      ======     ====       ======"
+
+    def pe_detail_format(self, t_obj):
+        l = [
+            shortdate(t_obj.start_ts),
+            shorttime(t_obj.start_ts),
+            t_obj.end_ts and shorttime(t_obj.end_ts) or "--:--:--",
+            # the format string occurs also below
+            self._str_nodecolor(t_obj.dc, '%-13s' % t_obj.shortname())
+        ]
+        l += get_cib_attributes(self.pe_report_path(t_obj), "cib",
+                                ("update-client", "update-user", "update-origin"),
+                                ("no-client", "no-user", "no-origin"))
+        return '%s %s %s  %-13s %-10s %-10s %s' % tuple(l)
+
+    def pelist(self, a=None, long=False):
+        if not self.prepare_source(no_live_update=self.prevent_live_update()):
+            return []
+        if isinstance(a, (tuple, list)):
+            if len(a) == 1:
+                a.append(a[0])
+        elif a is not None:
+            a = [a, a]
+        l = [long and self.pe_detail_format(x) or self.pe_report_path(x)
+             for x in self.peinputs_l if pe_file_in_range(x.pe_file, a)]
+        if long:
+            l = [self.pe_details_header, self.pe_details_separator] + l
+        return l
+
+    def dotlist(self, a=None):
+        l = [x.replace("bz2", "dot") for x in self.pelist(a)]
+        return [x for x in l if os.path.isfile(x)]
+
+    def find_pe_files(self, path):
+        'Find a PE or dot file matching part of the path.'
+        pe_l = path.endswith(".dot") and self.dotlist() or self.pelist()
+        return [x for x in pe_l if x.endswith(path)]
+
+    def pe2dot(self, f):
+        f = f.replace("bz2", "dot")
+        if os.path.isfile(f):
+            return f
+        return None
+
+    def find_file(self, f):
+        return file_find_by_name(self.loc, f)
+
+    def get_session_dir(self, name):
+        try:
+            return os.path.join(self.report_cache_dir, self.session_sub, name)
+        except:
+            return os.path.join(self.report_cache_dir, self.session_sub)
+    state_file = 'history_state.cfg'
+    rpt_section = 'report'
+
+    def save_state(self, dir):
+        '''
+        Save the current history state. It should include:
+        - directory
+        - timeframe
+        - detail
+        TODO
+        '''
+        p = ConfigParser.SafeConfigParser()
+        p.add_section(self.rpt_section)
+        p.set(self.rpt_section, 'dir',
+              self.source == "live" and dir or self.source)
+        p.set(self.rpt_section, 'from_time',
+              self.from_dt and human_date(self.from_dt) or '')
+        p.set(self.rpt_section, 'to_time',
+              self.to_dt and human_date(self.to_dt) or '')
+        p.set(self.rpt_section, 'detail', str(self.detail))
+        self.manage_excludes("save", p)
+        fname = os.path.join(dir, self.state_file)
+        try:
+            f = open(fname, "wb")
+        except IOError, msg:
+            common_err(msg)
+            return False
+        p.write(f)
+        f.close()
+        return True
+
+    def load_state(self, dir):
+        '''
+        Load the history state from a file.
+        '''
+        p = ConfigParser.SafeConfigParser()
+        fname = os.path.join(dir, self.state_file)
+        try:
+            p.read(fname)
+        except Exception, msg:
+            common_err(msg)
+            return False
+        rc = True
+        try:
+            for n, v in p.items(self.rpt_section):
+                if n == 'dir':
+                    self.source = self.loc = v
+                    if not os.path.exists(self.loc):
+                        common_err("session state file %s points to a non-existing directory: %s" %
+                                   (fname, self.loc))
+                        rc = False
+                elif n == 'from_time':
+                    self.from_dt = v and parse_time(v) or None
+                elif n == 'to_time':
+                    self.to_dt = v and parse_time(v) or None
+                elif n == 'detail':
+                    self.detail = int(v)
+                else:
+                    common_warn("unknown item %s in the session state file %s" %
+                                (n, fname))
+            rc |= self.manage_excludes("load", p)
+        except ConfigParser.NoSectionError, msg:
+            common_err("session state file %s: %s" % (fname, msg))
+            rc = False
+        except Exception, msg:
+            common_err("%s: bad value '%s' for '%s' in session state file %s" %
+                       (msg, v, n, fname))
+            rc = False
+        if rc:
+            self.set_change_origin(CH_SRC)
+        return rc
+
+    def set_change_origin(self, org):
+        '''Set origin only to a smaller value (if current > 0).
+        This prevents lesser change_origin overwriting a greater
+        one.
+        '''
+        if self.change_origin == 0 or org < self.change_origin:
+            self.change_origin = org
+
+    def manage_session(self, subcmd, name):
+        dir = self.get_session_dir(name)
+        if subcmd == "save" and os.path.exists(dir):
+            common_err("history session %s exists" % name)
+            return False
+        elif subcmd in ("load", "pack", "update", "delete") and not os.path.exists(dir):
+            common_err("history session %s does not exist" % name)
+            return False
+        if subcmd == "save":
+            if pipe_cmd_nosudo("mkdir -p %s" % dir) != 0:
+                return False
+            if self.source == "live":
+                rc = pipe_cmd_nosudo("tar -C '%s' -c . | tar -C '%s' -x" %
+                                     (self._live_loc(), dir))
+                if rc != 0:
+                    return False
+            return self.save_state(dir)
+        elif subcmd == "update":
+            return self.save_state(dir)
+        elif subcmd == "load":
+            return self.load_state(dir)
+        elif subcmd == "delete":
+            rmdir_r(dir)
+        elif subcmd == "list":
+            ext_cmd("ls %s" % self.get_session_dir(None))
+        elif subcmd == "pack":
+            return mkarchive(dir)
+        return True
+    log_section = 'log'
+
+    def manage_excludes(self, cmd, arg=None):
+        '''Exclude messages from log files.
+        arg: None (show, clear)
+             regex (add)
+             instance of ConfigParser.SafeConfigParser (load, save)
+        '''
+        if not self.prepare_source(no_live_update=True):
+            return False
+        rc = True
+        if cmd == "show":
+            print '\n'.join(self.log_filter_out)
+        elif cmd == "clear":
+            self.log_filter_out = []
+            self.log_filter_out_re = []
+        elif cmd == "add":
+            try:
+                regex = re.compile(arg)
+                self.log_filter_out.append(arg)
+                self.log_filter_out_re.append(regex)
+            except Exception, msg:
+                common_err("bad regex %s: %s" % (arg, msg))
+                rc = False
+        elif cmd == "save" and self.log_filter_out:
+            arg.add_section(self.log_section)
+            for i in range(len(self.log_filter_out)):
+                arg.set(self.log_section, 'exclude_%d' % i,
+                        self.log_filter_out[i])
+        elif cmd == "load":
+            self.manage_excludes("clear")
+            try:
+                for n, v in arg.items(self.log_section):
+                    if n.startswith('exclude_'):
+                        rc |= self.manage_excludes("add", v)
+                    else:
+                        common_warn("unknown item %s in the section %s" %
+                                    (n, self.log_section))
+            except ConfigParser.NoSectionError:
+                pass
+        return rc
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/rsctest.py b/modules/rsctest.py
new file mode 100644
index 0000000..dd167e1
--- /dev/null
+++ b/modules/rsctest.py
@@ -0,0 +1,433 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import sys
+from msg import common_err, common_debug, common_warn, common_info
+from utils import rmdir_r, quote
+from xmlutil import get_topmost_rsc, get_op_timeout, get_child_nvset_node, is_ms, is_cloned
+
+
+#
+# Resource testing suite
+#
+
+
+class RADriver(object):
+    '''
+    Execute operations on resources.
+    '''
+    pfx = {
+        "instance_attributes": "OCF_RESKEY_",
+        "meta_attributes": "OCF_RESKEY_CRM_meta_",
+    }
+    undef = -200
+    unused = -201
+
+    def __init__(self, rsc_node, nodes):
+        from tempfile import mkdtemp
+        self.rscdef_node = rsc_node
+        if rsc_node is not None:
+            self.ra_class = rsc_node.get("class")
+            self.ra_type = rsc_node.get("type")
+            self.ra_provider = rsc_node.get("provider")
+            self.id = rsc_node.get("id")
+        else:
+            self.ra_class = None
+            self.ra_type = None
+            self.ra_provider = None
+            self.id = None
+        self.nodes = nodes
+        self.outdir = mkdtemp(prefix="crmsh_out.")
+        self.errdir = mkdtemp(prefix="crmsh_err.")
+        self.ec_l = {}
+        self.ec_ok = self.unused
+        self.ec_stopped = self.unused
+        self.ec_master = self.unused
+        self.last_op = None
+        self.last_rec = {}
+        self.timeout = 20000
+
+    def __del__(self):
+        rmdir_r(self.outdir)
+        rmdir_r(self.errdir)
+
+    def id_str(self):
+        return self.last_op and "%s:%s" % (self.id, self.last_op) or self.id
+
+    def err(self, s):
+        common_err("%s: %s" % (self.id_str(), s))
+
+    def warn(self, s):
+        common_warn("%s: %s" % (self.id_str(), s))
+
+    def info(self, s):
+        common_info("%s: %s" % (self.id_str(), s))
+
+    def debug(self, s):
+        common_debug("%s: %s" % (self.id_str(), s))
+
+    def is_ms(self):
+        return is_ms(get_topmost_rsc(self.rscdef_node))
+
+    def nvset2env(self, set_n):
+        if set_n is None:
+            return
+        try:
+            pfx = self.pfx[set_n.tag]
+        except:
+            self.err("unknown attributes set: %s" % set_n.tag)
+            return
+        for nvpair in set_n.iterchildren():
+            if nvpair.tag != "nvpair":
+                continue
+            n = nvpair.get("name")
+            v = nvpair.get("value")
+            self.rscenv["%s%s" % (pfx, n)] = v
+
+    def set_rscenv(self, op):
+        '''
+        Setup the environment. Class specific.
+        '''
+        self.rscenv = {}
+        n = self.rscdef_node
+        self.timeout = get_op_timeout(n, op, "20s")
+        self.rscenv["%stimeout" % self.pfx["meta_attributes"]] = str(self.timeout)
+        if op == "monitor":
+            self.rscenv["%sinterval" % self.pfx["meta_attributes"]] = "10000"
+        if is_cloned(n):
+            # some of the meta attributes for clones/ms are used
+            # by resource agents
+            cn = get_topmost_rsc(n)
+            self.nvset2env(get_child_nvset_node(cn))
+
+    def op_status(self, host):
+        'Status of the last op.'
+        try:
+            return self.ec_l[host]
+        except:
+            return self.undef
+
+    def explain_op_status(self, host):
+        stat = self.op_status(host)
+        if stat == -9:
+            return "timed out"
+        elif stat == self.undef:
+            return "unknown reason (the RA couldn't run?)"
+        else:
+            return "exit code %d" % stat
+
+    def is_ok(self, host):
+        'Was last op successful?'
+        return self.op_status(host) == self.ec_ok
+
+    def is_master(self, host):
+        'Only if last op was probe/monitor.'
+        return self.op_status(host) == self.ec_master
+
+    def is_stopped(self, host):
+        'Only if last op was probe/monitor.'
+        return self.op_status(host) == self.ec_stopped
+
+    def show_log(self, host):
+        '''
+        Execute an operation.
+        '''
+        from crm_pssh import show_output
+        sys.stderr.write("host %s (%s)\n" %
+                         (host, self.explain_op_status(host)))
+        show_output(self.errdir, (host,), "stderr")
+        show_output(self.outdir, (host,), "stdout")
+
+    def run_on_all(self, op):
+        '''
+        In case of cloned resources, it doesn't make sense to run
+        (certain) operations on just one node. So, we run them
+        everywhere instead.
+        For instance, some clones require quorum.
+        '''
+        return is_cloned(self.rscdef_node) and op in ("start", "stop")
+
+    def exec_cmd(self, op):
+        '''defined in subclasses'''
+        pass
+
+    def runop(self, op, nodes=None):
+        '''
+        Execute an operation.
+        '''
+        from crm_pssh import do_pssh_cmd
+        if not nodes or self.run_on_all(op):
+            nodes = self.nodes
+        self.last_op = op
+        self.set_rscenv(op)
+        real_op = (op == "probe" and "monitor" or op)
+        cmd = self.exec_cmd(real_op)
+        common_debug("running %s on %s" % (real_op, nodes))
+        for attr in self.rscenv.keys():
+            # shell doesn't allow "-" in var names
+            envvar = attr.replace("-", "_")
+            cmd = "%s=%s %s" % (envvar, quote(self.rscenv[attr]), cmd)
+        statuses = do_pssh_cmd(cmd, nodes, self.outdir, self.errdir, self.timeout)
+        for i in range(len(nodes)):
+            try:
+                self.ec_l[nodes[i]] = statuses[i]
+            except:
+                self.ec_l[nodes[i]] = self.undef
+        return
+
+    def stop(self, node):
+        """
+        Make sure resource is stopped on node.
+        """
+        if self.is_ms():
+            self.runop("demote", (node,))
+        self.runop("stop", (node,))
+        ok = self.is_ok(node)
+        if not ok:
+            self.err("resource failed to stop on %s, clean it up!" % node)
+            self.show_log(node)
+        return ok
+
+    def test_resource(self, node):
+        """
+        Perform test of resource on node.
+        """
+        self.runop("start", (node,))
+        if self.is_ms() and self.is_ok(node):
+            self.runop("promote", (node,))
+        return self.is_ok(node)
+
+    def probe(self):
+        """
+        Execute probe (if possible)
+        """
+        self.runop("probe")
+
+    def verify_stopped(self, node):
+        """
+        Make sure resource is stopped on node.
+        """
+        stopped = self.is_stopped(node)
+        if not stopped:
+            if self.is_ok(node):
+                self.warn("resource running at %s" % (node))
+            elif self.is_ms() and self.is_master(node):
+                self.warn("resource is master at %s" % (node))
+            else:
+                self.warn("resource not clean at %s" % (node))
+                self.show_log(node)
+        return stopped
+
+
+class RAOCF(RADriver):
+    '''
+    Execute operations on OCF resources.
+    '''
+    # OCF exit codes
+    OCF_SUCCESS = 0
+    OCF_ERR_GENERIC = 1
+    OCF_ERR_ARGS = 2
+    OCF_ERR_UNIMPLEMENTED = 3
+    OCF_ERR_PERM = 4
+    OCF_ERR_INSTALLED = 5
+    OCF_ERR_CONFIGURED = 6
+    OCF_NOT_RUNNING = 7
+    OCF_RUNNING_MASTER = 8
+    OCF_FAILED_MASTER = 9
+
+    def __init__(self, *args):
+        RADriver.__init__(self, *args)
+        self.ec_ok = self.OCF_SUCCESS
+        self.ec_stopped = self.OCF_NOT_RUNNING
+        self.ec_master = self.OCF_RUNNING_MASTER
+
+    def set_rscenv(self, op):
+        RADriver.set_rscenv(self, op)
+        self.nvset2env(get_child_nvset_node(self.rscdef_node, "instance_attributes"))
+        self.rscenv["OCF_RESOURCE_INSTANCE"] = self.id
+        self.rscenv["OCF_ROOT"] = os.environ["OCF_ROOT"]
+
+    def exec_cmd(self, op):
+        cmd = "%s/resource.d/%s/%s %s" % \
+            (os.environ["OCF_ROOT"], self.ra_provider, self.ra_type, op)
+        return cmd
+
+
+class RALSB(RADriver):
+    '''
+    Execute operations on LSB resources (init scripts).
+    '''
+
+    # OCF exit codes
+    LSB_OK = 0
+    LSB_ERR_GENERIC = 1
+    LSB_ERR_ARGS = 2
+    LSB_ERR_UNIMPLEMENTED = 3
+    LSB_ERR_PERM = 4
+    LSB_ERR_INSTALLED = 5
+    LSB_ERR_CONFIGURED = 6
+    LSB_NOT_RUNNING = 7
+    LSB_STATUS_DEAD_PID = 1
+    LSB_STATUS_DEAD_LOCK = 2
+    LSB_STATUS_NOT_RUNNING = 3
+    LSB_STATUS_UNKNOWN = 4
+
+    def __init__(self, *args):
+        RADriver.__init__(self, *args)
+        self.ec_ok = self.LSB_OK
+        self.ec_stopped = self.LSB_STATUS_NOT_RUNNING
+        self.ec_master = self.unused
+
+    def set_rscenv(self, op):
+        RADriver.set_rscenv(self, op)
+
+    def exec_cmd(self, op):
+        if self.ra_type.startswith("/"):
+            prog = self.ra_type
+        else:
+            prog = "/etc/init.d/%s" % self.ra_type
+        cmd = "%s %s" % (prog, op == "monitor" and "status" or op)
+        return cmd
+
+
+class RAStonith(RADriver):
+    '''
+    Execute operations on Stonith resources.
+    '''
+
+    STONITH_OK = 0
+    STONITH_ERR = 1
+
+    def __init__(self, *args):
+        RADriver.__init__(self, *args)
+        self.ec_ok = self.STONITH_OK
+        self.ec_stopped = self.STONITH_ERR
+
+    def stop(self, node):
+        """
+        Disable for stonith resources.
+        """
+        return True
+
+    def verify_stopped(self, node):
+        """
+        Disable for stonith resources.
+        """
+        return True
+
+    def test_resource(self, node):
+        """
+        Run test for stonith resource
+        """
+        for prefix in ['rhcs/', 'fence_']:
+            if self.ra_type.startswith(prefix):
+                self.err("Cannot test RHCS STONITH resources!")
+                return False
+        return RADriver.test_resource(self, node)
+
+    def set_rscenv(self, op):
+        RADriver.set_rscenv(self, op)
+        for nv in self.rscdef_node.xpath("instance_attributes/nvpair"):
+            self.rscenv[nv.get('name')] = nv.get('value')
+
+    def exec_cmd(self, op):
+        """
+        Probe resource on each node.
+        """
+        return "stonith -t %s -E -S" % (self.ra_type)
+
+
+ra_driver = {
+    "ocf": RAOCF,
+    "lsb": RALSB,
+    "stonith": RAStonith
+}
+
+
+def check_test_support(resources):
+    rc = True
+    for r in resources:
+        ra_class = r.get("class")
+        if not ra_class:
+            common_warn("class attribute not found in %s" % r.get('id'))
+            rc = False
+        elif ra_class not in ra_driver:
+            common_warn("testing of class %s resources not supported" %
+                        ra_class)
+            rc = False
+    return rc
+
+
+def are_all_stopped(resources, nodes):
+    rc = True
+    sys.stderr.write("Probing resources ")
+    for r in resources:
+        ra_class = r.get("class")
+        drv = ra_driver[ra_class](r, nodes)
+        sys.stderr.write(".")
+        drv.probe()
+        for node in nodes:
+            if not drv.verify_stopped(node):
+                rc = False
+    sys.stderr.write("\n")
+    return rc
+
+
+def stop_all(started, node):
+    'Stop all started resources in reverse order on node.'
+    while started:
+        drv = started.pop()
+        drv.stop(node)
+
+
+def test_resources(resources, nodes, all_nodes):
+    def test_node(node):
+        started = []
+        sys.stderr.write("testing on %s:" % node)
+        for r in resources:
+            id = r.get("id")
+            ra_class = r.get("class")
+            drv = ra_driver[ra_class](r, (node,))
+            sys.stderr.write(" %s" % id)
+            if drv.test_resource(node):
+                started.append(drv)
+            else:
+                sys.stderr.write("\n")
+                drv.show_log(node)
+                stop_all(started, node)
+                return False
+        sys.stderr.write("\n")
+        stop_all(started, node)
+        return True
+
+    try:
+        import crm_pssh
+    except ImportError:
+        common_err("pssh not installed, rsctest can not be executed")
+        return False
+    if not check_test_support(resources):
+        return False
+    if not are_all_stopped(resources, all_nodes):
+        sys.stderr.write("Stop all resources before testing!\n")
+        return False
+    rc = True
+    for node in nodes:
+        rc |= test_node(node)
+    return rc
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/schema.py b/modules/schema.py
new file mode 100644
index 0000000..879f859
--- /dev/null
+++ b/modules/schema.py
@@ -0,0 +1,146 @@
+# Copyright (C) 2012 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import config
+from pacemaker import CrmSchema, PacemakerError
+from msg import common_err
+
+
+def get_attrs(schema, name):
+    return {
+        'a': schema.get_elem_attrs(name, 'a'),  # all
+        'r': schema.get_elem_attrs(name, 'r'),  # required
+        'o': schema.get_elem_attrs(name, 'o'),  # optional
+    }
+
+
+def get_subs(schema, name):
+    return {
+        'a': schema.get_sub_elems(name, 'a'),  # all
+        'r': schema.get_sub_elems(name, 'r'),  # required
+        'o': schema.get_sub_elems(name, 'o'),  # optional
+    }
+
+
+def get_attr_details_d(schema, name):
+    # some attributes' names don't repeat, can do a hash
+    # (op)
+    d = {}
+    for attr_obj in schema.get_elem_attr_objs(name):
+        attr_name = schema.get_obj_name(attr_obj)
+        d[attr_name] = {
+            't': schema.get_attr_type(attr_obj),     # type
+            'v': schema.get_attr_values(attr_obj),   # values
+            'd': schema.get_attr_default(attr_obj),  # default
+        }
+    return d
+
+
+def get_attr_details_l(schema, name):
+    # some attributes' names repeat, need a list
+    # (date_expression)
+    l = []
+    for attr_obj in schema.get_elem_attr_objs(name):
+        l.append({
+            'n': schema.get_obj_name(attr_obj),      # name
+            't': schema.get_attr_type(attr_obj),     # type
+            'v': schema.get_attr_values(attr_obj),   # values
+            'd': schema.get_attr_default(attr_obj),  # default
+        })
+    return l
+
+
+_cache_funcs = {
+    'attr': get_attrs,
+    'sub': get_subs,
+    'attr_det': get_attr_details_d,
+    'attr_det_l': get_attr_details_l,
+}
+
+
+_crm_schema = None
+_store = {}
+
+
+def reset():
+    global _store
+    _store = {}
+
+
+def _load_schema(cib):
+    return CrmSchema(cib, config.path.crm_dtd_dir)
+
+
+def init_schema(cib):
+    global _crm_schema
+    _crm_schema = _load_schema(cib)
+    reset()
+
+
+def test_schema(cib):
+    try:
+        crm_schema = _load_schema(cib)
+    except PacemakerError, msg:
+        common_err(msg)
+        return None
+    return crm_schema.validate_name
+
+
+def validate_name():
+    if _crm_schema is None:
+        return 'pacemaker-2.0'
+    return _crm_schema.validate_name
+
+
+def get(t, name, set=None):
+    if _crm_schema is None:
+        return []
+    if t not in _store:
+        _store[t] = {}
+    if name not in _store[t]:
+        _store[t][name] = _cache_funcs[t](_crm_schema, name)
+    if set:
+        return _store[t][name][set]
+    else:
+        return _store[t][name]
+
+
+def rng_attr_values(el_name, attr_name):
+    ""
+    try:
+        return get('attr_det', el_name)[attr_name]['v']
+    except:
+        return []
+
+
+def rng_attr_values_l(el_name, attr_name):
+    ""
+    l = get('attr_det_l', el_name)
+    l2 = []
+    for el in l:
+        if el['n'] == attr_name:
+            l2 += el['v']
+    return l2
+
+
+def rng_xpath(xpath, namespaces=None):
+    if _crm_schema is None:
+        return []
+    return _crm_schema.rng_xpath(xpath, namespaces=namespaces)
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/scripts.py b/modules/scripts.py
new file mode 100644
index 0000000..95fea0b
--- /dev/null
+++ b/modules/scripts.py
@@ -0,0 +1,743 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import sys
+import time
+import random
+import os
+import shutil
+import getpass
+import subprocess
+import config
+import options
+from msg import err_buf
+import userdir
+
+try:
+    from psshlib import api as pssh
+    has_pssh = True
+except ImportError:
+    has_pssh = False
+
+import utils
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+
+def script_dirs():
+    ret = []
+    for d in options.scriptdir.split(';'):
+        if d and os.path.isdir(d):
+            ret.append(d)
+    ret.append(os.path.join(userdir.CONFIG_HOME, 'scripts'))
+    ret.append(os.path.join(config.path.sharedir, 'scripts'))
+    return ret
+
+
+def _check_control_persist():
+    '''
+    Checks if ControlPersist is available. If so,
+    we'll use it to make things faster.
+    '''
+    cmd = 'ssh -o ControlPersist'.split()
+    if options.regression_tests:
+        print ".EXT", cmd
+    cmd = subprocess.Popen(cmd,
+                           stdout=subprocess.PIPE,
+                           stderr=subprocess.PIPE)
+    (out, err) = cmd.communicate()
+    return "Bad configuration option" not in err
+
+
+def _pssh_call(hosts, cmd, opts):
+    "pssh.call with debug logging"
+    if config.core.debug or options.regression_tests:
+        err_buf.debug("pssh.call(%s, %s)" % (repr(hosts), cmd))
+    return pssh.call(hosts, cmd, opts)
+
+
+def _pssh_copy(hosts, src, dst, opts):
+    "pssh.copy with debug logging"
+    if config.core.debug or options.regression_tests:
+        err_buf.debug("pssh.copy(%s, %s, %s)" % (repr(hosts), src, dst))
+    return pssh.copy(hosts, src, dst, opts)
+
+
+def _generate_workdir_name():
+    '''
+    Generate a temporary folder name to use while
+    running the script
+    '''
+    # TODO: make use of /tmp configurable
+    basefile = 'crm-tmp-%s-%s' % (time.time(), random.randint(0, 2**48))
+    basetmp = os.path.join(utils.get_tempdir(), basefile)
+    return basetmp
+
+
+def resolve_script(name):
+    for d in script_dirs():
+        script_main = os.path.join(d, name, 'main.yml')
+        if os.path.isfile(script_main):
+            return script_main
+    return None
+
+
+def list_scripts():
+    '''
+    List the available cluster installation scripts.
+    '''
+    l = []
+
+    def path_combine(p0, p1):
+        if p0:
+            return os.path.join(p0, p1)
+        return p1
+
+    def recurse(root, prefix):
+        try:
+            curdir = path_combine(root, prefix)
+            for f in os.listdir(curdir):
+                if os.path.isdir(os.path.join(curdir, f)):
+                    if os.path.isfile(os.path.join(curdir, f, 'main.yml')):
+                        l.append(path_combine(prefix, f))
+                    else:
+                        recurse(root, path_combine(prefix, f))
+        except OSError:
+            pass
+    for d in script_dirs():
+        recurse(d, '')
+    return sorted(l)
+
+
+def load_script(script):
+    main = resolve_script(script)
+    if main and os.path.isfile(main):
+        try:
+            import yaml
+            return yaml.load(open(main))[0]
+        except ImportError, e:
+            raise ValueError("PyYAML error: %s" % (e))
+    return None
+
+
+def _step_action(step):
+    name = step.get('name')
+    if 'type' in step:
+        return name, step.get('type'), step.get('call')
+    else:
+        for typ in ['collect', 'validate', 'apply', 'apply_local', 'report']:
+            if typ in step:
+                return name, typ, step[typ].strip()
+    return name, None, None
+
+
+def arg0(cmd):
+    return cmd.split()[0]
+
+
+def _verify_step(scriptdir, scriptname, step):
+    step_name, step_type, step_call = _step_action(step)
+    if not step_name:
+        raise ValueError("Error in %s: Step missing name" % (scriptname))
+    if not step_type:
+        raise ValueError("Error in %s: Step '%s' has no action defined" %
+                         (scriptname, step_name))
+    if not step_call:
+        raise ValueError("Error in %s: Step '%s' has no call defined" %
+                         (scriptname, step_name))
+    if not os.path.isfile(os.path.join(scriptdir, arg0(step_call))):
+        raise ValueError("Error in %s: Step '%s' file not found: %s" %
+                         (scriptname, step_name, step_call))
+
+
+def verify(name):
+    script = resolve_script(name)
+    if not script:
+        raise ValueError("%s not found" % (name))
+    script_dir = os.path.dirname(script)
+    main = load_script(name)
+    for key in ['name', 'description', 'parameters', 'steps']:
+        if key not in main:
+            raise ValueError("Error in %s: Missing %s" % (name, key))
+    for step in main.get('steps', []):
+        _verify_step(script_dir, name, step)
+    return main
+
+
+def common_params():
+    "Parameters common to all cluster scripts"
+    return [('nodes', None, 'List of nodes to execute the script for'),
+            ('dry_run', 'no', 'If set, only execute collecting and validating steps'),
+            ('step', None, 'If set, only execute the named step'),
+            ('statefile', None, 'When single-stepping, the state is saved in the given file'),
+            ('user', config.core.user or None, 'Run script as the given user'),
+            ('sudo', 'no',
+             'If set, crm will prompt for a sudo password and use sudo when appropriate'),
+            ('port', None, 'Port to connect on'),
+            ('timeout', '600', 'Execution timeout in seconds')]
+
+
+def common_param_default(name):
+    for param, default, _ in common_params():
+        if param == name:
+            return default
+    return None
+
+
+def describe(name):
+    '''
+    Prints information about the given script.
+    '''
+    script = load_script(name)
+    from help import HelpEntry
+
+    def rewrap(txt):
+        import textwrap
+        paras = []
+        for para in txt.split('\n'):
+            paras.append('\n'.join(textwrap.wrap(para)))
+        return '\n\n'.join(paras)
+    desc = rewrap(script.get('description', 'No description available'))
+
+    params = script.get('parameters', [])
+    desc += "Parameters (* = Required):\n"
+    for name, value, description in common_params():
+        if value is not None:
+            defval = ' (default: %s)' % (value)
+        else:
+            defval = ''
+        desc += "  %-24s %s%s\n" % (name, description, defval)
+    for p in params:
+        rq = ''
+        if p.get('required'):
+            rq = '*'
+        defval = p.get('default', None)
+        if defval is not None:
+            defval = ' (default: %s)' % (defval)
+        else:
+            defval = ''
+        desc += "  %-24s %s%s\n" % (p['name'] + rq, p.get('description', ''), defval)
+
+    desc += "\nSteps:\n"
+    for step in script.get('steps', []):
+        name = step.get('name')
+        if name:
+            desc += "  * %s\n" % (name)
+
+    e = HelpEntry(script.get('name', name), desc)
+    e.paginate()
+
+
+def param_completion_list(name):
+    "Returns completions for the given script"
+    try:
+        script = load_script(name)
+        ps = [p['name'] + '=' for p in script.get('parameters', [])]
+        ps += [p[0] + '=' for p in common_params()]
+        return ps
+    except Exception:
+        return [p[0] + '=' for p in common_params()]
+
+
+def _make_options(params):
+    "Setup pssh options."
+    opts = pssh.Options()
+    opts.timeout = int(params['timeout'])
+    opts.recursive = True
+    opts.ssh_options += [
+        'KbdInteractiveAuthentication=no',
+        'PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey',
+        'PasswordAuthentication=no',
+        'StrictHostKeyChecking=no',
+        'ControlPersist=no']
+    if options.regression_tests:
+        opts.ssh_extra += ['-vvv']
+    return opts
+
+
+def _open_script(name):
+    filename = resolve_script(name)
+    main = verify(name)
+    if main is None or filename is None:
+        raise ValueError('Loading script failed: ' + name)
+    script_dir = os.path.dirname(filename)
+    return main, filename, script_dir
+
+
+def _filter_dict(d, name, fn, *args):
+    'filter the given element in the dict through the function fn'
+    d[name] = fn(d[name], *args)
+
+
+def _filter_nodes(nodes, user, port):
+    'filter function for the nodes element'
+    if nodes:
+        nodes = nodes.replace(',', ' ').split()
+    else:
+        nodes = utils.list_cluster_nodes()
+    if not nodes:
+        raise ValueError("No hosts")
+    nodes = [(node, port or None, user or None) for node in nodes]
+    return nodes
+
+
+def _parse_parameters(name, args, main):
+    '''
+    Parse run parameters into a dict.
+    '''
+    args = utils.nvpairs2dict(args)
+    params = {}
+    for key, default, _ in common_params():
+        params[key] = default
+    for key, val in args.iteritems():
+        params[key] = val
+    for param in main['parameters']:
+        name = param['name']
+        if name not in params:
+            if 'default' not in param:
+                raise ValueError("Missing required parameter %s" % (name))
+            params[name] = param['default']
+
+    user = params['user']
+    port = params['port']
+    _filter_dict(params, 'nodes', _filter_nodes, user, port)
+    _filter_dict(params, 'dry_run', utils.is_boolean_true)
+    _filter_dict(params, 'sudo', utils.is_boolean_true)
+    _filter_dict(params, 'statefile', lambda x: (x and os.path.abspath(x)) or x)
+    if config.core.debug:
+        params['debug'] = True
+    return params
+
+
+def _extract_localnode(hosts):
+    """
+    Remove loal node from hosts list, so
+    we can treat it separately
+    """
+    this_node = utils.this_node()
+    hosts2 = []
+    local_node = None
+    for h, p, u in hosts:
+        if h != this_node:
+            hosts2.append((h, p, u))
+        else:
+            local_node = (h, p, u)
+    err_buf.debug("Local node: %s, Remote hosts: %s" % (
+        local_node,
+        ', '.join(h[0] for h in hosts2)))
+    return local_node, hosts2
+
+
+def _set_controlpersist(opts):
+    #_has_controlpersist = _check_control_persist()
+    #if _has_controlpersist:
+    #    opts.ssh_options += ["ControlMaster=auto",
+    #                         "ControlPersist=30s",
+    #                         "ControlPath=/tmp/crm-ssh-%r@%h:%p"]
+    # unfortunately, due to bad interaction between pssh and ssh,
+    # ControlPersist is broken
+    # See: http://code.google.com/p/parallel-ssh/issues/detail?id=67
+    # Fixed in openssh 6.3
+    pass
+
+
+def _create_script_workdir(scriptdir, workdir):
+    "Create workdir and copy contents of scriptdir into it"
+    cmd = ["mkdir", "-p", os.path.dirname(workdir)]
+    if options.regression_tests:
+        print ".EXT", cmd
+    if subprocess.call(cmd, shell=False) != 0:
+        raise ValueError("Failed to create temporary working directory")
+    try:
+        shutil.copytree(scriptdir, workdir)
+    except (IOError, OSError), e:
+        raise ValueError(e)
+
+
+def _copy_utils(dst):
+    '''
+    Copy run utils to the destination directory
+    '''
+    try:
+        import glob
+        for f in glob.glob(os.path.join(config.path.sharedir, 'utils/*.py')):
+            shutil.copy(os.path.join(config.path.sharedir, f), dst)
+    except (IOError, OSError), e:
+        raise ValueError(e)
+
+
+def _create_remote_workdirs(hosts, path, opts):
+    "Create workdirs on remote hosts"
+    ok = True
+    for host, result in _pssh_call(hosts,
+                                   "mkdir -p %s" % (os.path.dirname(path)),
+                                   opts).iteritems():
+        if isinstance(result, pssh.Error):
+            err_buf.error("[%s]: Start: %s" % (host, result))
+            ok = False
+    if not ok:
+        msg = "Failed to connect to one or more of these hosts via SSH: %s" % (
+            ', '.join(h[0] for h in hosts))
+        raise ValueError(msg)
+
+
+def _copy_to_remote_dirs(hosts, path, opts):
+    "Copy a local folder to same location on remote hosts"
+    ok = True
+    for host, result in _pssh_copy(hosts,
+                                   path,
+                                   path, opts).iteritems():
+        if isinstance(result, pssh.Error):
+            err_buf.error("[%s]: %s" % (host, result))
+            ok = False
+    if not ok:
+        raise ValueError("Failed when copying script data, aborting.")
+
+
+def _copy_to_all(workdir, hosts, local_node, src, dst, opts):
+    """
+    Copy src to dst both locally and remotely
+    """
+    ok = True
+    ret = _pssh_copy(hosts, src, dst, opts)
+    for host, result in ret.iteritems():
+        if isinstance(result, pssh.Error):
+            err_buf.error("[%s]: %s" % (host, result))
+            ok = False
+        else:
+            rc, out, err = result
+            if rc != 0:
+                err_buf.error("[%s]: %s" % (host, err))
+                ok = False
+    if local_node and not src.startswith(workdir):
+        try:
+            if os.path.abspath(src) != os.path.abspath(dst):
+                if os.path.isfile(src):
+                    shutil.copy(src, dst)
+                else:
+                    shutil.copytree(src, dst)
+        except (IOError, OSError, shutil.Error), e:
+            err_buf.error("[%s]: %s" % (local_node, e))
+            ok = False
+    return ok
+
+
+class RunStep(object):
+    def __init__(self, main, params, local_node, hosts, opts, workdir):
+        self.main = main
+        self.data = [params]
+        self.local_node = local_node
+        self.hosts = hosts
+        self.opts = opts
+        self.dry_run = params.get('dry_run', False)
+        self.sudo = params.get('sudo', False)
+        self.workdir = workdir
+        self.statefile = os.path.join(self.workdir, 'script.input')
+        self.dstfile = os.path.join(self.workdir, 'script.input')
+        self.in_progress = False
+        self.sudo_pass = None
+
+    def _build_cmdline(self, sname, stype, scall):
+        cmdline = 'cd "%s"; ./%s' % (self.workdir, scall)
+        if config.core.debug:
+            import pprint
+            print "** %s [%s] - %s" % (sname, stype, scall)
+            print cmdline
+            pprint.pprint(self.data)
+        return cmdline
+
+    def single_step(self, step_name, statefile):
+        self.statefile = statefile
+        for step in self.main['steps']:
+            name, action, call = _step_action(step)
+            if name == step_name:
+                # if this is not the first step, load step data
+                if step != self.main['steps'][0]:
+                    if os.path.isfile(statefile):
+                        self.data = json.load(open(statefile))
+                    else:
+                        raise ValueError("No state for step: %s" % (step_name))
+                result = self.run_step(name, action, call)
+                json.dump(self.data, open(self.statefile, 'w'))
+                return result
+        err_buf.error("%s: Step not found" % (step_name))
+        return False
+
+    def _update_state(self):
+        json.dump(self.data, open(self.statefile, 'w'))
+        return _copy_to_all(self.workdir,
+                            self.hosts,
+                            self.local_node,
+                            self.statefile,
+                            self.dstfile,
+                            self.opts)
+
+    def start(self, txt):
+        if not options.batch:
+            sys.stdout.write(txt)
+            sys.stdout.flush()
+            self.in_progress = True
+
+    def flush(self):
+        if self.in_progress:
+            self.in_progress = False
+            sys.stdout.write('\r')
+            sys.stdout.flush()
+
+    def ok(self, fmt, *args):
+        self.flush()
+        err_buf.ok(fmt % args)
+
+    def out(self, fmt, *args):
+        self.flush()
+        if args:
+            print fmt % args
+        else:
+            print fmt
+
+    def error(self, fmt, *args):
+        self.flush()
+        err_buf.error(fmt % args)
+
+    def debug(self, msg):
+        err_buf.debug(msg)
+
+    def run_step(self, name, action, call):
+        """
+        Execute a single step
+        """
+
+        self.start('%s...' % (name))
+        try:
+            cmdline = self._build_cmdline(name, action, call)
+            if not self._update_state():
+                raise ValueError("Failed when updating input, aborting.")
+            output = None
+            ok = False
+            if action in ('collect', 'apply'):
+                result = self._process_remote(cmdline)
+                if result is not None:
+                    self.data.append(result)
+                    ok = True
+            elif action == 'validate':
+                result = self._process_local(cmdline)
+                if result is not None:
+                    if result:
+                        result = json.loads(result)
+                    else:
+                        result = {}
+                    self.data.append(result)
+                    if isinstance(result, dict):
+                        for k, v in result.iteritems():
+                            self.data[0][k] = v
+                    ok = True
+            elif action == 'apply_local':
+                result = self._process_local(cmdline)
+                if result is not None:
+                    if result:
+                        result = json.loads(result)
+                    else:
+                        result = {}
+                    self.data.append(result)
+                    ok = True
+            elif action == 'report':
+                result = self._process_local(cmdline)
+                if result is not None:
+                    output = result
+                    ok = True
+            if ok:
+                self.ok(name)
+            if output:
+                self.out(output)
+            return ok
+        finally:
+            self.flush()
+
+    def all_steps(self):
+        # TODO: run asynchronously on remote nodes
+        # run on remote nodes
+        # run on local nodes
+        # TODO: wait for remote results
+        for step in self.main['steps']:
+            name, action, call = _step_action(step)
+            if action in ('apply', 'apply_local'):
+                if self.dry_run:
+                    break
+                self._check_sudo_pass()
+            if not self.run_step(name, action, call):
+                return False
+        return True
+
+    def _check_sudo_pass(self):
+        if self.sudo and not self.sudo_pass:
+            prompt = "sudo password: "
+            self.sudo_pass = getpass.getpass(prompt=prompt)
+
+    def _process_remote(self, cmdline):
+        """
+        Handle a step that executes on all nodes
+        """
+        ok = True
+        step_result = {}
+
+        if self.sudo_pass:
+            self.opts.input_stream = u'sudo: %s\n' % (self.sudo_pass)
+        else:
+            self.opts.input_stream = None
+
+        for host, result in _pssh_call(self.hosts,
+                                       cmdline,
+                                       self.opts).iteritems():
+            if isinstance(result, pssh.Error):
+                self.error("[%s]: %s", host, result)
+                ok = False
+            else:
+                rc, out, err = result
+                if rc != 0:
+                    self.error("[%s]: %s%s", host, out, err)
+                    ok = False
+                else:
+                    step_result[host] = json.loads(out)
+        if self.local_node:
+            ret = self._process_local(cmdline)
+            if ret is None:
+                ok = False
+            else:
+                step_result[self.local_node[0]] = json.loads(ret)
+        if ok:
+            self.debug("%s" % repr(step_result))
+            return step_result
+        return None
+
+    def _process_local(self, cmdline):
+        """
+        Handle a step that executes locally
+        """
+        if self.sudo_pass:
+            input_s = u'sudo: %s\n' % (self.sudo_pass)
+        else:
+            input_s = None
+        rc, out, err = utils.get_stdout_stderr(cmdline, input_s=input_s, shell=True)
+        if rc != 0:
+            self.error("[%s]: Error (%d): %s", self.local_node[0], rc, err)
+            return None
+        self.debug("%s" % repr(out))
+        return out
+
+
+def _cleanup_local(workdir):
+    "clean up the local tmp dir"
+    if workdir and os.path.isdir(workdir):
+        cleanscript = os.path.join(workdir, 'crm_clean.py')
+        if os.path.isfile(cleanscript):
+            if subprocess.call([cleanscript, workdir], shell=False) != 0:
+                shutil.rmtree(workdir)
+        else:
+            shutil.rmtree(workdir)
+
+
+def _print_output(host, rc, out, err):
+    "Print the output from a process that ran on host"
+    if out:
+        err_buf.ok("[%s]: %s" % (host, out))
+    if err:
+        err_buf.error("[%s]: %s" % (host, err))
+
+
+def _run_cleanup(local_node, hosts, workdir, opts):
+    "Clean up after the cluster script"
+    if hosts and workdir:
+        cleanscript = os.path.join(workdir, 'crm_clean.py')
+        for host, result in _pssh_call(hosts,
+                                       "%s %s" % (cleanscript,
+                                                  workdir),
+                                       opts).iteritems():
+            if isinstance(result, pssh.Error):
+                err_buf.debug("[%s]: Failed to clean up %s" % (host, workdir))
+                err_buf.error("[%s]: Clean: %s" % (host, result))
+            else:
+                _print_output(host, *result)
+    _cleanup_local(workdir)
+
+
+def _print_debug(local_node, hosts, workdir, opts):
+    "Print debug output (if any)"
+    dbglog = os.path.join(workdir, 'crm_script.debug')
+    for host, result in _pssh_call(hosts,
+                                   "[ -f '%s' ] && cat '%s'" % (dbglog, dbglog),
+                                   opts).iteritems():
+        if isinstance(result, pssh.Error):
+            err_buf.error("[%s]: %s" % (host, result))
+        else:
+            _print_output(host, *result)
+    if os.path.isfile(dbglog):
+        f = open(dbglog).read()
+        err_buf.ok("[%s]: %s" % (local_node, f))
+
+
+def run(name, args):
+    '''
+    Run the given script on the given set of hosts
+    name: a cluster script is a folder <name> containing a main.yml file
+    args: list of nvpairs
+    '''
+    if not has_pssh:
+        try:
+            from psshlib.task import Task
+        except ImportError:
+            raise ValueError("The pssh library is not installed or is not up to date.")
+        raise ValueError("The installed pssh library lacks the API patch.")
+    workdir = _generate_workdir_name()
+    main, filename, script_dir = _open_script(name)
+    params = _parse_parameters(name, args, main)
+    hosts = params['nodes']
+    err_buf.info(main['name'])
+    err_buf.info("Nodes: " + ', '.join([x[0] for x in hosts]))
+    local_node, hosts = _extract_localnode(hosts)
+    opts = _make_options(params)
+    _set_controlpersist(opts)
+
+    try:
+        _create_script_workdir(script_dir, workdir)
+        _copy_utils(workdir)
+        _create_remote_workdirs(hosts, workdir, opts)
+        _copy_to_remote_dirs(hosts, workdir, opts)
+        # make sure all path references are relative to the script directory
+        os.chdir(workdir)
+
+        stepper = RunStep(main, params, local_node, hosts, opts, workdir)
+        step = params['step']
+        statefile = params['statefile']
+        if step or statefile:
+            if not step or not statefile:
+                raise ValueError("Must set both step and statefile")
+            return stepper.single_step(step, statefile)
+        else:
+            return stepper.all_steps()
+
+    except (OSError, IOError), e:
+        import traceback
+        traceback.print_exc()
+        raise ValueError("Internal error while running %s: %s" % (name, e))
+    finally:
+        if not config.core.debug:
+            _run_cleanup(local_node, hosts, workdir, opts)
+        else:
+            _print_debug(local_node, hosts, workdir, opts)
diff --git a/modules/template.py b/modules/template.py
new file mode 100644
index 0000000..0716efe
--- /dev/null
+++ b/modules/template.py
@@ -0,0 +1,195 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import os
+import re
+import config
+import userdir
+from msg import common_err, common_info, common_warn
+
+
+def get_var(l, key):
+    for s in l:
+        a = s.split()
+        if len(a) == 2 and a[0] == key:
+            return a[1]
+    return ''
+
+
+def chk_var(l, key):
+    for s in l:
+        a = s.split()
+        if len(a) == 2 and a[0] == key and a[1]:
+            return True
+    return False
+
+
+def chk_key(l, key):
+    for s in l:
+        a = s.split()
+        if len(a) >= 1 and a[0] == key:
+            return True
+    return False
+
+
+def validate_template(l):
+    'Test for required stuff in a template.'
+    if not chk_var(l, '%name'):
+        common_err("invalid template: missing '%name'")
+        return False
+    if not chk_key(l, '%generate'):
+        common_err("invalid template: missing '%generate'")
+        return False
+    g = l.index('%generate')
+    if not (chk_key(l[0:g], '%required') or chk_key(l[0:g], '%optional')):
+        common_err("invalid template: missing '%required' or '%optional'")
+        return False
+    return True
+
+
+def fix_tmpl_refs(l, ident, pfx):
+    for i in range(len(l)):
+        l[i] = l[i].replace(ident, pfx)
+
+
+def fix_tmpl_refs_re(l, regex, repl):
+    for i in range(len(l)):
+        l[i] = re.sub(regex, repl, l[i])
+
+
+class LoadTemplate(object):
+    '''
+    Load a template and its dependencies, generate a
+    configuration file which should be relatively easy and
+    straightforward to parse.
+    '''
+    edit_instructions = '''# Edit instructions:
+#
+# Add content only at the end of lines starting with '%%'.
+# Only add content, don't remove or replace anything.
+# The parameters following '%required' are not optional,
+# unlike those following '%optional'.
+# You may also add comments for future reference.'''
+    no_more_edit = '''# Don't edit anything below this line.'''
+
+    def __init__(self, name):
+        self.name = name
+        self.all_pre_gen = []
+        self.all_post_gen = []
+        self.all_pfx = []
+
+    def new_pfx(self, name):
+        i = 1
+        pfx = name
+        while pfx in self.all_pfx:
+            pfx = "%s_%d" % (name, i)
+            i += 1
+        self.all_pfx.append(pfx)
+        return pfx
+
+    def generate(self):
+        return '\n'.join(
+            ["# Configuration: %s" % self.name,
+             '',
+             self.edit_instructions,
+             '',
+             '\n'.join(self.all_pre_gen),
+             self.no_more_edit,
+             '',
+             '%generate',
+             '\n'.join(self.all_post_gen)])
+
+    def write_config(self, name):
+        try:
+            f = open("%s/%s" % (userdir.CRMCONF_DIR, name), "w")
+        except IOError, msg:
+            common_err("open: %s" % msg)
+            return False
+        print >>f, self.generate()
+        f.close()
+        return True
+
+    def load_template(self, tmpl):
+        try:
+            f = open(os.path.join(config.path.sharedir, 'templates', tmpl))
+        except IOError, msg:
+            common_err("open: %s" % msg)
+            return ''
+        l = (''.join(f)).split('\n')
+        if not validate_template(l):
+            return ''
+        common_info("pulling in template %s" % tmpl)
+        g = l.index('%generate')
+        pre_gen = l[0:g]
+        post_gen = l[g+1:]
+        name = get_var(pre_gen, '%name')
+        for s in l[0:g]:
+            if s.startswith('%depends_on'):
+                a = s.split()
+                if len(a) != 2:
+                    common_warn("%s: wrong usage" % s)
+                    continue
+                tmpl_id = a[1]
+                tmpl_pfx = self.load_template(a[1])
+                if tmpl_pfx:
+                    fix_tmpl_refs(post_gen, '%'+tmpl_id, '%'+tmpl_pfx)
+        pfx = self.new_pfx(name)
+        fix_tmpl_refs(post_gen, '%_:', '%'+pfx+':')
+        # replace remaining %_, it may be useful at times
+        fix_tmpl_refs(post_gen, '%_', pfx)
+        v_idx = pre_gen.index('%required') or pre_gen.index('%optional')
+        pre_gen.insert(v_idx, '%pfx ' + pfx)
+        self.all_pre_gen += pre_gen
+        self.all_post_gen += post_gen
+        return pfx
+
+    def post_process(self, params):
+        pfx_re = '(%s)' % '|'.join(self.all_pfx)
+        for n in params:
+            fix_tmpl_refs(self.all_pre_gen, '%% '+n, "%% "+n+"  "+params[n])
+        fix_tmpl_refs_re(self.all_post_gen,
+                         '%' + pfx_re + '([^:]|$)', r'\1\2')
+        # process %if ... [%else] ... %fi
+        rmidx_l = []
+        if_seq = False
+        outcome = False  # unnecessary, but to appease lints
+        for i in range(len(self.all_post_gen)):
+            s = self.all_post_gen[i]
+            if if_seq:
+                a = s.split()
+                if len(a) >= 1 and a[0] == '%fi':
+                    if_seq = False
+                    rmidx_l.append(i)
+                elif len(a) >= 1 and a[0] == '%else':
+                    outcome = not outcome
+                    rmidx_l.append(i)
+                else:
+                    if not outcome:
+                        rmidx_l.append(i)
+                continue
+            if not s:
+                continue
+            a = s.split()
+            if len(a) == 2 and a[0] == '%if':
+                outcome = not a[1].startswith('%')  # not replaced -> false
+                if_seq = True
+                rmidx_l.append(i)
+        rmidx_l.reverse()
+        for i in rmidx_l:
+            del self.all_post_gen[i]
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/term.py b/modules/term.py
new file mode 100644
index 0000000..95b58e6
--- /dev/null
+++ b/modules/term.py
@@ -0,0 +1,186 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import sys
+import re
+
+# from: http://code.activestate.com/recipes/475116/
+
+"""
+A module that can be used to portably generate formatted output to
+a terminal.
+Defines a set of instance variables whose
+values are initialized to the control sequence necessary to
+perform a given action.  These can be simply included in normal
+output to the terminal:
+    >>> print 'This is '+term.colors.GREEN+'green'+term.colors.NORMAL
+Alternatively, the `render()` method can used, which replaces
+'${action}' with the string required to perform 'action':
+    >>> print term.render('This is ${GREEN}green${NORMAL}')
+If the terminal doesn't support a given action, then the value of
+the corresponding instance variable will be set to ''.  As a
+result, the above code will still work on terminals that do not
+support color, except that their output will not be colored.
+Also, this means that you can test whether the terminal supports a
+given action by simply testing the truth value of the
+corresponding instance variable:
+    >>> if term.colors.CLEAR_SCREEN:
+    ...     print 'This terminal supports clearning the screen.'
+Finally, if the width and height of the terminal are known, then
+they will be stored in the `COLS` and `LINES` attributes.
+"""
+
+
+class colors(object):
+    # Cursor movement:
+    BOL = ''             #: Move the cursor to the beginning of the line
+    UP = ''              #: Move the cursor up one line
+    DOWN = ''            #: Move the cursor down one line
+    LEFT = ''            #: Move the cursor left one char
+    RIGHT = ''           #: Move the cursor right one char
+    # Deletion:
+    CLEAR_SCREEN = ''    #: Clear the screen and move to home position
+    CLEAR_EOL = ''       #: Clear to the end of the line.
+    CLEAR_BOL = ''       #: Clear to the beginning of the line.
+    CLEAR_EOS = ''       #: Clear to the end of the screen
+    # Output modes:
+    BOLD = ''            #: Turn on bold mode
+    BLINK = ''           #: Turn on blink mode
+    DIM = ''             #: Turn on half-bright mode
+    REVERSE = ''         #: Turn on reverse-video mode
+    UNDERLINE = ''       #: Turn on underline mode
+    NORMAL = ''          #: Turn off all modes
+    # Cursor display:
+    HIDE_CURSOR = ''     #: Make the cursor invisible
+    SHOW_CURSOR = ''     #: Make the cursor visible
+    # Terminal size:
+    COLS = None          #: Width of the terminal (None for unknown)
+    LINES = None         #: Height of the terminal (None for unknown)
+    # Foreground colors:
+    BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
+    # Background colors:
+    BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
+    BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
+    RLIGNOREBEGIN = '\001'
+    RLIGNOREEND = '\002'
+
+_STRING_CAPABILITIES = """
+BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
+CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
+BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
+HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
+
+_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
+_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
+
+
+def _init():
+    """
+    Initialize attributes with appropriate values for the current terminal.
+
+    `_term_stream` is the stream that will be used for terminal
+    output; if this stream is not a tty, then the terminal is
+    assumed to be a dumb terminal (i.e., have no capabilities).
+    """
+    def _tigetstr(cap_name):
+        import curses
+        cap = curses.tigetstr(cap_name) or ''
+
+        # String capabilities can include "delays" of the form "$<2>".
+        # For any modern terminal, we should be able to just ignore
+        # these, so strip them out.
+        # terminof(5) states that:
+        #   A "/" suffix indicates that the padding is mandatory and forces a
+        #   delay of the given number of milliseconds even on devices for which
+        #   xon is present to indicate flow control.
+        # So let's respect that. But not the optional ones.
+        cap = re.sub(r'\$<\d+>[*]?', '', cap)
+
+        # To switch back to "NORMAL", we use sgr0, which resets "everything" to defaults.
+        # That on some terminals includes ending "alternate character set mode".
+        # Which is usually done by appending '\017'.  Unfortunately, less -R
+        # does not eat that, but shows it as an annoying inverse '^O'
+        # Instead of falling back to less -r, which would have other implications as well,
+        # strip off that '\017': we don't use the alternative character set,
+        # so we won't need to reset it either.
+        if cap_name == 'sgr0':
+            cap = re.sub(r'\017$', '', cap)
+
+        return cap
+
+    _term_stream = sys.stdout
+    # Curses isn't available on all platforms
+    try:
+        import curses
+    except:
+        sys.stderr.write("INFO: no curses support: you won't see colors\n")
+        return
+    # If the stream isn't a tty, then assume it has no capabilities.
+    if not _term_stream.isatty():
+        return
+    # Check the terminal type.  If we fail, then assume that the
+    # terminal has no capabilities.
+    try:
+        curses.setupterm()
+    except:
+        return
+
+    # Look up numeric capabilities.
+    colors.COLS = curses.tigetnum('cols')
+    colors.LINES = curses.tigetnum('lines')
+    # Look up string capabilities.
+    for capability in _STRING_CAPABILITIES:
+        (attrib, cap_name) = capability.split('=')
+        setattr(colors, attrib, _tigetstr(cap_name) or '')
+    # Colors
+    set_fg = _tigetstr('setf')
+    if set_fg:
+        for i, color in zip(range(len(_COLORS)), _COLORS):
+            setattr(colors, color, curses.tparm(set_fg, i) or '')
+    set_fg_ansi = _tigetstr('setaf')
+    if set_fg_ansi:
+        for i, color in zip(range(len(_ANSICOLORS)), _ANSICOLORS):
+            setattr(colors, color, curses.tparm(set_fg_ansi, i) or '')
+    set_bg = _tigetstr('setb')
+    if set_bg:
+        for i, color in zip(range(len(_COLORS)), _COLORS):
+            setattr(colors, 'BG_'+color, curses.tparm(set_bg, i) or '')
+    set_bg_ansi = _tigetstr('setab')
+    if set_bg_ansi:
+        for i, color in zip(range(len(_ANSICOLORS)), _ANSICOLORS):
+            setattr(colors, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
+
+_init()
+
+
+def render(template):
+    """
+    Replace each $-substitutions in the given template string with
+    the corresponding terminal control string (if it's defined) or
+    '' (if it's not).
+    """
+    def render_sub(match):
+        s = match.group()
+        return getattr(colors, s[2:-1].upper(), '')
+    return re.sub(r'\${\w+}', render_sub, template)
+
+
+def is_color(s):
+    return hasattr(colors, s.upper())
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/tmpfiles.py b/modules/tmpfiles.py
new file mode 100644
index 0000000..9f94312
--- /dev/null
+++ b/modules/tmpfiles.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+'''
+Files added to tmpfiles are removed at program exit.
+'''
+
+import os
+import shutil
+import atexit
+from tempfile import mkstemp, mkdtemp
+
+import utils
+
+_FILES = []
+_DIRS = []
+
+
+def _exit_handler():
+    "Called at program exit"
+    for f in _FILES:
+        try:
+            os.unlink(f)
+        except OSError:
+            pass
+    for d in _DIRS:
+        try:
+            shutil.rmtree(d)
+        except OSError:
+            pass
+
+
+def add(filename):
+    '''
+    Remove the named file at program exit.
+    '''
+    if len(_FILES) + len(_DIRS) == 0:
+        atexit.register(_exit_handler)
+    _FILES.append(filename)
+
+
+def create(dir=utils.get_tempdir(), prefix='crmsh_'):
+    '''
+    Create a temporary file and remove it at program exit.
+    Returns (fd, filename)
+    '''
+    fd, fname = mkstemp(dir=dir, prefix=prefix)
+    add(fname)
+    return fd, fname
+
+
+def create_dir(dir=utils.get_tempdir(), prefix='crmsh_'):
+    ret = mkdtemp(dir=dir, prefix=prefix)
+    if len(_FILES) + len(_DIRS) == 0:
+        atexit.register(_exit_handler)
+    _DIRS.append(ret)
+    return ret
diff --git a/modules/ui_assist.py b/modules/ui_assist.py
new file mode 100644
index 0000000..0145ad6
--- /dev/null
+++ b/modules/ui_assist.py
@@ -0,0 +1,146 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import utils
+import command
+import completers as compl
+import xmlutil
+from cibconfig import cib_factory
+
+
+def rmattrs(e, *attrs):
+    "remove the given attributes from an XML element"
+    for attr in attrs:
+        if attr in e.attrib:
+            del e.attrib[attr]
+
+
+class Assist(command.UI):
+    '''
+    The assist UI collects what could be called
+    configuration macros. Things like merging
+    multiple resources into a template, or building
+    a colocated set with a relation to a dummy
+    resource.
+    '''
+    name = "assist"
+
+    def __init__(self):
+        command.UI.__init__(self)
+
+    def requires(self):
+        return cib_factory.initialize()
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.call(cib_factory.prim_id_list))
+    def do_template(self, context, *primitives):
+        '''
+        Create a shared template for the given primitives
+        '''
+        if len(primitives) < 1:
+            context.fatal_error("Expected at least one primitive argument")
+        objs = [cib_factory.find_object(p) for p in primitives]
+        for prim, obj in zip(primitives, objs):
+            if obj is None:
+                context.fatal_error("Primitive %s not found" % (prim))
+        if objs and all(obj.obj_type == 'primitive' for obj in objs):
+            return self._template_primitives(context, objs)
+        context.fatal_error("Cannot create a template for the given resources")
+
+    def _template_primitives(self, context, primitives):
+        """
+        Try to template the given primitives:
+        Templating means creating a rsc_template and moving
+        shared attributes and other commonalities into that template
+        (this second step is currently not available)
+        """
+        shared_template = None
+        if all('template' in obj.node.attrib for obj in primitives):
+            return True
+        if len(set(xmlutil.mk_rsc_type(obj.node) for obj in primitives)) != 1:
+            context.fatal_error("Cannot template the given primitives")
+
+        node = primitives[0].node
+        template_name = self.make_unique_name('template-%s-' % (node.get('type').lower()))
+        shared_template = cib_factory.create_object('rsc_template', template_name,
+                                                    xmlutil.mk_rsc_type(node))
+        if not shared_template:
+            context.fatal_error("Error creating template")
+        for obj in primitives:
+            obj.node.set('template', template_name)
+            rmattrs(obj.node, 'class', 'provider', 'type')
+            obj.set_updated()
+
+        if not self._pull_attributes(context, shared_template, primitives):
+            context.fatal_error("Error when copying attributes into template")
+
+        context.info("Template created: %s" % (template_name))
+        return True
+
+    def _pull_attributes(self, context, template, primitives):
+        '''
+        TODO: take any attributes shared by all primitives and
+        move them into the shared template
+        '''
+        return True
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.call(cib_factory.prim_id_list))
+    @command.name('weak-bond')
+    @command.alias('weak_bond')
+    def do_weak_bond(self, context, *nodes):
+        '''
+        Create a 'weak' colocation:
+        Colocating a non-sequential resource set with
+        a dummy resource which is not monitored creates,
+        in effect, a colocation which does not imply any
+        internal relationship between resources.
+        '''
+        if len(nodes) < 2:
+            context.fatal_error("Need at least two arguments")
+
+        for node in nodes:
+            obj = cib_factory.find_object(node)
+            if not obj:
+                context.fatal_error("Object not found: %s" % (node))
+            if not xmlutil.is_primitive(obj.node):
+                context.fatal_error("Object not primitive: %s" % (node))
+
+        constraint_name = self.make_unique_name('place-constraint-')
+        dummy_name = self.make_unique_name('place-dummy-')
+        print "Create weak bond / independent colocation"
+        print "The following elements will be created:"
+        print "   * Colocation constraint, ID: %s" % (constraint_name)
+        print "   * Dummy resource, ID: %s" % (dummy_name)
+        if not utils.can_ask() or utils.ask("Create resources?"):
+            cib_factory.create_object('primitive', dummy_name, 'ocf:heartbeat:Dummy')
+            colo = ['colocation', constraint_name, 'inf:', '(']
+            colo.extend(nodes)
+            colo.append(')')
+            colo.append(dummy_name)
+            cib_factory.create_object(*colo)
+
+    def make_unique_name(self, prefix):
+        n = 0
+        while n < 1000:
+            n += 1
+            name = "%s%s" % (prefix, n)
+            for _id in cib_factory.id_list():
+                if name == _id.lower():
+                    continue
+            return name
+        raise ValueError("Failed to generate unique resource ID with prefix '%s'" % (prefix))
diff --git a/modules/ui_cib.py b/modules/ui_cib.py
new file mode 100644
index 0000000..165f2e2
--- /dev/null
+++ b/modules/ui_cib.py
@@ -0,0 +1,234 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import glob
+import command
+import xmlutil
+import utils
+import ui_cibstatus
+import constants
+import config
+import options
+from msg import no_prog_err
+from cibstatus import cib_status
+from cibconfig import cib_factory
+import tmpfiles
+
+import completers as compl
+
+_NEWARGS = ('force', '--force', 'withstatus', 'empty')
+
+
+class CibShadow(command.UI):
+    '''
+    CIB shadow management class
+    '''
+    name = "cib"
+    extcmd = ">/dev/null </dev/null crm_shadow"
+    extcmd_stdout = "</dev/null crm_shadow"
+
+    def requires(self):
+        if not utils.is_program('crm_shadow'):
+            no_prog_err('crm_shadow')
+            return False
+        return True
+
+    @command.level(ui_cibstatus.CibStatusUI)
+    def do_cibstatus(self):
+        pass
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, compl.choice(_NEWARGS))
+    def do_new(self, context, *args):
+        "usage: new [<shadow_cib>] [withstatus] [force] [empty]"
+        argl = list(args)
+        opt_l = utils.fetch_opts(argl, ["force", "--force", "withstatus", "empty"])
+        if len(argl) > 1:
+            context.fatal_error("Unexpected argument(s): " + ','.join(argl))
+
+        name = None
+        if argl:
+            name = argl[0]
+            if not utils.is_filename_sane(name):
+                context.fatal_error("Bad filename: " + name)
+            if name in (constants.tmp_cib_prompt, constants.live_cib_prompt):
+                context.fatal_error("Shadow name '%s' is not allowed" % (name))
+            del argl[0]
+            constants.tmp_cib = False
+        else:
+            fd, fname = tmpfiles.create(dir=xmlutil.cib_shadow_dir(), prefix="shadow.crmsh_")
+            name = os.path.basename(fname).replace("shadow.", "")
+            constants.tmp_cib = True
+
+        if "empty" in opt_l:
+            new_cmd = "%s -e '%s'" % (self.extcmd, name)
+        else:
+            new_cmd = "%s -c '%s'" % (self.extcmd, name)
+        if constants.tmp_cib or config.core.force or "force" in opt_l or "--force" in opt_l:
+            new_cmd = "%s --force" % new_cmd
+        if utils.ext_cmd(new_cmd) == 0:
+            context.info("%s shadow CIB created" % name)
+            self.do_use(context, name)
+            if "withstatus" in opt_l:
+                cib_status.load("shadow:%s" % name)
+
+    def _find_pe(self, context, infile):
+        'Find a pe input'
+        for p in ("%s/%s", "%s/%s.bz2", "%s/pe-*-%s.bz2"):
+            fl = glob.glob(p % (config.path.pe_state_dir, infile))
+            if fl:
+                break
+        if not fl:
+            context.fatal_error("no %s pe input file" % infile)
+        if len(fl) > 1:
+            context.fatal_error("more than one %s pe input file: %s" %
+                                (infile, ' '.join(fl)))
+        if not fl[0]:
+            context.fatal_error("bad %s pe input file" % infile)
+        return fl[0]
+
+    @command.skill_level('administrator')
+    @command.completers(compl.null, compl.shadows)
+    def do_import(self, context, infile, name=None):
+        "usage: import {<file>|<number>} [<shadow>]"
+        if name and not utils.is_filename_sane(name):
+            context.fatal_error("Bad filename: " + name)
+        # where's the input?
+        if not os.access(infile, os.F_OK):
+            if "/" in infile:
+                context.fatal_error(str(infile) + ": no such file")
+            infile = self._find_pe(context, infile)
+        if not name:
+            name = os.path.basename(infile).replace(".bz2", "")
+        if not xmlutil.pe2shadow(infile, name):
+            context.fatal_error("Error copying PE file to shadow: %s -> %s" % (infile, name))
+        # use the shadow and load the status from there
+        return self.do_use(context, name, "withstatus")
+
+    @command.skill_level('administrator')
+    @command.completers(compl.shadows)
+    def do_delete(self, context, name):
+        "usage: delete <shadow_cib>"
+        if not utils.is_filename_sane(name):
+            context.fatal_error("Bad filename: " + name)
+        if utils.get_cib_in_use() == name:
+            context.fatal_error("%s shadow CIB is in use" % name)
+        if utils.ext_cmd("%s -D '%s' --force" % (self.extcmd, name)) == 0:
+            context.info("%s shadow CIB deleted" % name)
+        else:
+            context.fatal_error("failed to delete %s shadow CIB" % name)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.shadows)
+    def do_reset(self, context, name):
+        "usage: reset <shadow_cib>"
+        if not utils.is_filename_sane(name):
+            context.fatal_error("Bad filename: " + name)
+        if utils.ext_cmd("%s -r '%s'" % (self.extcmd, name)) == 0:
+            context.info("copied live CIB to %s" % name)
+        else:
+            context.fatal_error("failed to copy live CIB to %s" % name)
+
+    @command.skill_level('administrator')
+    @command.wait
+    @command.completers(compl.shadows)
+    def do_commit(self, context, name=None):
+        "usage: commit [<shadow_cib>]"
+        if name and not utils.is_filename_sane(name):
+            context.fatal_error("Bad filename: " + name)
+        if not name:
+            name = utils.get_cib_in_use()
+        if not name:
+            context.fatal_error("There is nothing to commit")
+        if utils.ext_cmd("%s -C '%s' --force" % (self.extcmd, name)) == 0:
+            context.info("committed '%s' shadow CIB to the cluster" % name)
+        else:
+            context.fatal_error("failed to commit the %s shadow CIB" % name)
+        if constants.tmp_cib:
+            self._use('', '')
+
+    @command.skill_level('administrator')
+    def do_diff(self, context):
+        "usage: diff"
+        rc, s = utils.get_stdout(utils.add_sudo("%s -d" % self.extcmd_stdout))
+        utils.page_string(s)
+
+    @command.skill_level('administrator')
+    def do_list(self, context):
+        "usage: list"
+        if options.regression_tests:
+            for t in xmlutil.listshadows():
+                print t
+        else:
+            utils.multicolumn(xmlutil.listshadows())
+
+    def _use(self, name, withstatus):
+        # Choose a shadow cib for further changes. If the name
+        # provided is empty, then choose the live (cluster) cib.
+        # Don't allow ' in shadow names
+        if not name or name == "live":
+            if withstatus:
+                cib_status.load("live")
+            if constants.tmp_cib:
+                utils.ext_cmd("%s -D '%s' --force" % (self.extcmd, utils.get_cib_in_use()))
+                constants.tmp_cib = False
+            utils.clear_cib_in_use()
+        else:
+            utils.set_cib_in_use(name)
+            if withstatus:
+                cib_status.load("shadow:%s" % name)
+        return True
+
+    @command.skill_level('administrator')
+    @command.completers(compl.join(compl.shadows, compl.choice(['live'])),
+                        compl.choice(['withstatus']))
+    def do_use(self, context, name='', withstatus=''):
+        "usage: use [<shadow_cib>] [withstatus]"
+        # check the name argument
+        if name and not utils.is_filename_sane(name):
+            context.fatal_error("Bad filename: " + name)
+        if name and name != "live":
+            if not os.access(xmlutil.shadowfile(name), os.F_OK):
+                context.fatal_error("%s: no such shadow CIB" % name)
+        if withstatus and withstatus != "withstatus":
+            context.fatal_error("Expected 'withstatus', got '%s'" % (withstatus))
+        # If invoked from configure
+        # take special precautions
+        if not context.previous_level_is("cibconfig"):
+            return self._use(name, withstatus)
+        if not cib_factory.has_cib_changed():
+            ret = self._use(name, withstatus)
+            # new CIB: refresh the CIB factory
+            cib_factory.refresh()
+            return ret
+        saved_cib = utils.get_cib_in_use()
+        self._use(name, '')  # don't load the status yet
+        if not cib_factory.is_current_cib_equal(silent=True):
+            # user made changes and now wants to switch to a
+            # different and unequal CIB; we refuse to cooperate
+            context.error_message("the requested CIB is different from the current one")
+            if config.core.force:
+                context.info("CIB overwrite forced")
+            elif not utils.ask("All changes will be dropped. Do you want to proceed?"):
+                self._use(saved_cib, '')  # revert to the previous CIB
+                return False
+        return self._use(name, withstatus)  # now load the status too
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/ui_cibstatus.py b/modules/ui_cibstatus.py
new file mode 100644
index 0000000..122e1ca
--- /dev/null
+++ b/modules/ui_cibstatus.py
@@ -0,0 +1,114 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import command
+import completers as compl
+import utils
+import ui_utils
+import constants
+from cibstatus import cib_status
+
+
+_status_node_list = compl.call(cib_status.status_node_list)
+
+
+class CibStatusUI(command.UI):
+    '''
+    The CIB status section management user interface class
+    '''
+    name = "cibstatus"
+
+    @command.skill_level('expert')
+    def do_load(self, context, org):
+        "usage: load {<file>|shadow:<cib>|live}"
+        return cib_status.load(org)
+
+    @command.skill_level('expert')
+    def do_save(self, context, dest=None):
+        "usage: save [<file>|shadow:<cib>]"
+        return cib_status.save(dest)
+
+    @command.skill_level('administrator')
+    def do_origin(self, context):
+        "usage: origin"
+        state = cib_status.modified and " (modified)" or ""
+        print "%s%s" % (cib_status.origin, state)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.choice(['changed']))
+    def do_show(self, context, changed=""):
+        "usage: show [changed]"
+        if changed:
+            if changed != "changed":
+                context.fatal_error("Expected 'changed', got '%s'" % (changed))
+            return cib_status.list_changes()
+        return cib_status.show()
+
+    @command.skill_level('administrator')
+    @command.completers(compl.booleans)
+    def do_quorum(self, context, opt):
+        "usage: quorum <bool>"
+        if not utils.verify_boolean(opt):
+            context.fatal_error("%s: bad boolean option" % opt)
+        return cib_status.set_quorum(utils.is_boolean_true(opt))
+
+    @command.skill_level('expert')
+    @command.completers(_status_node_list, compl.choice(constants.node_states))
+    def do_node(self, context, node, state):
+        "usage: node <node> {online|offline|unclean}"
+        return cib_status.edit_node(node, state)
+
+    @command.skill_level('expert')
+    @command.completers(compl.null, compl.choice(cib_status.ticket_ops.keys()))
+    def do_ticket(self, context, ticket, subcmd):
+        "usage: ticket <ticket> {grant|revoke|activate|standby}"
+        return cib_status.edit_ticket(ticket, subcmd)
+
+    @command.skill_level('expert')
+    @command.completers(compl.choice(constants.ra_operations),
+                        compl.call(cib_status.status_rsc_list),
+                        compl.choice(constants.lrm_exit_codes.keys()),
+                        compl.choice(constants.lrm_status_codes.keys()),
+                        compl.choice(constants.node_states))
+    def do_op(self, context, op, rsc, rc, op_status=None, node=''):
+        "usage: op <operation> <resource> <exit_code> [<op_status>] [<node>]"
+        if rc in constants.lrm_exit_codes:
+            num_rc = constants.lrm_exit_codes[rc]
+        else:
+            num_rc = rc
+        if not num_rc.isdigit():
+            context.fatal_error("Invalid exit code '%s'" % num_rc)
+        num_op_status = op_status
+        if op_status:
+            if op_status in constants.lrm_status_codes:
+                num_op_status = constants.lrm_status_codes[op_status]
+            if not num_op_status.isdigit():
+                context.fatal_error("Invalid operation status '%s'" % num_op_status)
+        return cib_status.edit_op(op, rsc, num_rc, num_op_status, node)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.choice(['nograph']))
+    def do_run(self, context, *args):
+        "usage: run [nograph] [v...] [scores] [utilization]"
+        return ui_utils.ptestlike(cib_status.run, '', context.get_command_name(), args)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.choice(['nograph']))
+    def do_simulate(self, context, *args):
+        "usage: simulate [nograph] [v...] [scores] [utilization]"
+        return ui_utils.ptestlike(cib_status.simulate, '', context.get_command_name(), args)
diff --git a/modules/ui_cluster.py b/modules/ui_cluster.py
new file mode 100644
index 0000000..4198a92
--- /dev/null
+++ b/modules/ui_cluster.py
@@ -0,0 +1,214 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import command
+import utils
+from msg import err_buf
+import scripts
+import completers as compl
+
+
+def _remove_completer(args):
+    try:
+        n = utils.list_cluster_nodes()
+    except:
+        n = []
+    return scripts.param_completion_list('remove') + n
+
+
+class Cluster(command.UI):
+    '''
+    Whole cluster management.
+
+    - Package installation
+    - System configuration
+    - Network troubleshooting
+    - Perform other callouts/cluster-wide devops operations
+    '''
+    name = "cluster"
+
+    def requires(self):
+        stack = utils.cluster_stack()
+        if len(stack) > 0 and stack != 'corosync':
+            err_buf.warning("Unsupported cluster stack %s detected." % (stack))
+            return False
+        return True
+
+    def __init__(self):
+        command.UI.__init__(self)
+        # ugly hack to allow overriding the node list
+        # for the cluster commands that operate before
+        # there is an actual cluster
+        self._inventory_nodes = None
+        self._inventory_target = None
+
+    @command.skill_level('administrator')
+    def do_start(self, context):
+        '''
+        Starts the cluster services on this node
+        '''
+        rc, out, err = utils.get_stdout_stderr('service corosync start')
+        if rc != 0:
+            context.fatal_error("Failed to start corosync service: %s" % (err))
+        rc, out, err = utils.get_stdout_stderr('service pacemaker start')
+        if rc != 0:
+            context.fatal_error("Failed to start pacemaker service: %s" % (err))
+        err_buf.info("Cluster services started")
+
+        # TODO: optionally start services on all nodes or specific node
+
+    @command.skill_level('administrator')
+    def do_stop(self, context):
+        '''
+        Stops the cluster services on this node
+        '''
+        rc, out, err = utils.get_stdout_stderr('service pacemaker stop')
+        if rc != 0:
+            context.fatal_error("Failed to stop pacemaker service: %s" % (err))
+        rc, out, err = utils.get_stdout_stderr('service corosync stop')
+        if rc != 0:
+            context.fatal_error("Failed to stop corosync service: %s" % (err))
+        err_buf.info("Cluster services stopped")
+
+        # TODO: optionally stop services on all nodes or specific node
+
+    def _args_implicit(self, context, args, name):
+        '''
+        handle early non-nvpair arguments as
+        values in an implicit list
+        '''
+        args = list(args)
+        vals = []
+        while args and args[0].find('=') == -1:
+            vals.append(args[0])
+            args = args[1:]
+        if vals:
+            return args + ['%s=%s' % (name, ','.join(vals))]
+        return args
+
+    @command.completers_repeating(compl.choice(scripts.param_completion_list('init')))
+    @command.skill_level('administrator')
+    def do_init(self, context, *args):
+        '''
+        Initialize a cluster with the given hosts as nodes.
+        '''
+        return scripts.run('init', self._args_implicit(context, args, 'nodes'))
+
+    @command.completers_repeating(compl.choice(scripts.param_completion_list('add')))
+    @command.skill_level('administrator')
+    def do_add(self, context, *args):
+        '''
+        Add the given node(s) to the cluster.
+        Installs packages, sets up corosync and pacemaker, etc.
+        Must be executed from a node in the existing cluster.
+        '''
+        params = self._args_implicit(context, args, 'node')
+        paramdict = utils.nvpairs2dict(params)
+        node = paramdict.get('node')
+        if node:
+            node = node.replace(',', ' ').split()
+        else:
+            node = []
+        nodes = paramdict.get('nodes')
+        if not nodes:
+            nodes = utils.list_cluster_nodes()
+        nodes += node
+        params += ['nodes=%s' % (','.join(nodes))]
+        return scripts.run('add', params)
+
+    @command.completers_repeating(_remove_completer)
+    @command.skill_level('administrator')
+    def do_remove(self, context, *args):
+        '''
+        Remove the given node(s) from the cluster.
+        '''
+        params = self._args_implicit(context, args, 'node')
+        return scripts.run('remove', params)
+
+    @command.completers_repeating(compl.choice(scripts.param_completion_list('health')))
+    def do_health(self, context, *args):
+        '''
+        Extensive health check.
+        '''
+        params = self._args_implicit(context, args, 'nodes')
+        return scripts.run('health', params)
+
+    def _node_in_cluster(self, node):
+        return node in utils.list_cluster_nodes()
+
+    def do_status(self, context):
+        '''
+        Quick cluster health status. Corosync status, DRBD status...
+        '''
+        stack = utils.cluster_stack()
+        if not stack:
+            err_buf.error("Cluster stack not detected!")
+        if utils.cluster_stack() == 'corosync':
+            print "Services:"
+            for svc in ["corosync", "pacemaker"]:
+                info = utils.service_info(svc)
+                if info:
+                    print "%-16s %s" % (svc, info)
+                else:
+                    print "%-16s unknown" % (svc)
+
+            rc, outp = utils.get_stdout(['corosync-cfgtool', '-s'], shell=False)
+            if rc == 0:
+                print ""
+                print outp
+            else:
+                print "Failed to get corosync status"
+
+    @command.completers_repeating(compl.choice(['10', '60', '600']))
+    def do_wait_for_startup(self, context, timeout='10'):
+        "usage: wait_for_startup [<timeout>]"
+        import time
+        t0 = time.time()
+        timeout = float(timeout)
+        cmd = 'crm_mon -bD1 2&>1 >/dev/null'
+        ret = utils.ext_cmd(cmd)
+        while ret in (107, 64) and time.time() < t0 + timeout:
+            time.sleep(1)
+            ret = utils.ext_cmd(cmd)
+        if ret != 0:
+            context.fatal_error("Timed out waiting for cluster (rc = %s)" % (ret))
+
+    @command.skill_level('expert')
+    def do_run(self, context, cmd):
+        '''
+        Execute the given command on all nodes, report outcome
+        '''
+        try:
+            from psshlib import api as pssh
+            _has_pssh = True
+        except ImportError:
+            _has_pssh = False
+
+        if not _has_pssh:
+            context.fatal_error("PSSH not found")
+
+        hosts = utils.list_cluster_nodes()
+        opts = pssh.Options()
+        for host, result in pssh.call(hosts, cmd, opts).iteritems():
+            if isinstance(result, pssh.Error):
+                err_buf.error("[%s]: %s" % (host, result))
+            else:
+                if result[0] != 0:
+                    err_buf.error("[%s]: rc=%s\n%s\n%s" % (host, result[0], result[1], result[2]))
+                else:
+                    err_buf.ok("[%s]\n%s" % (host, result[1]))
diff --git a/modules/ui_configure.py b/modules/ui_configure.py
new file mode 100644
index 0000000..e3e0ec3
--- /dev/null
+++ b/modules/ui_configure.py
@@ -0,0 +1,806 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import time
+import command
+import completers as compl
+import config
+import utils
+import constants
+import userdir
+import xmlutil
+import ra
+from cibconfig import mkset_obj, cib_factory
+import clidisplay
+import term
+import options
+from msg import common_err, common_info, common_warn
+from msg import err_buf, syntax_err
+import rsctest
+import schema
+import ui_cib
+import ui_cibstatus
+import ui_ra
+import ui_template
+import ui_history
+import ui_utils
+import ui_assist
+from crm_gv import gv_types
+
+
+def _type_completions():
+    "completer for type: use in show"
+    typelist = cib_factory.type_list()
+    return ['type:%s' % (t) for t in typelist]
+
+
+# Tab completion helpers
+_id_list = compl.call(cib_factory.id_list)
+_id_xml_list = compl.join(_id_list, compl.choice(['xml']))
+_id_show_list = compl.join(_id_list,
+                           compl.choice(['xml', 'changed']),
+                           compl.call(_type_completions))
+_prim_id_list = compl.call(cib_factory.prim_id_list)
+_f_prim_free_id_list = compl.call(cib_factory.f_prim_free_id_list)
+_f_group_id_list = compl.call(cib_factory.f_group_id_list)
+_f_children_id_list = compl.call(cib_factory.f_children_id_list)
+_rsc_id_list = compl.call(cib_factory.rsc_id_list)
+_top_rsc_id_list = compl.call(cib_factory.top_rsc_id_list)
+_node_id_list = compl.call(cib_factory.node_id_list)
+_rsc_template_list = compl.call(cib_factory.rsc_template_list)
+_group_completer = compl.join(_f_prim_free_id_list, compl.choice(['params', 'meta']))
+_clone_completer = compl.choice(['params', 'meta'])
+_ms_completer = compl.choice(['params', 'meta'])
+
+
+def top_rsc_tmpl_id_list(args):
+    return cib_factory.top_rsc_id_list() + cib_factory.rsc_template_list()
+
+
+def ra_classes_or_tmpl(args):
+    if args[-1].startswith('@'):
+        return cib_factory.rsc_template_list()
+    return ui_ra.complete_class_provider_type(args)
+
+
+def op_attr_list(args):
+    schema_attr = [schema.g_schema.get('attr', 'op', 'o') + '=']
+    extra_attrs = [s + '=' for s in constants.op_extra_attrs]
+    return schema_attr + extra_attrs
+
+
+def node_id_colon_list(args):
+    return [s + ':' for s in _node_id_list(args)]
+
+
+def stonith_resource_list(args):
+    return [x.obj_id for x in
+            cib_factory.get_elems_on_type("type:primitive")
+            if x.node.get("class") == "stonith"]
+
+
+def _load_2nd_completer(args):
+    if args[1] == 'xml':
+        return ['replace', 'update']
+    return []
+
+
+# completion for primitives including help for parameters
+# (help also available for properties)
+
+def get_prim_token(words, n):
+    for key in ("primitive", "rsc_template"):
+        try:
+            return words[words.index(key) + n - 1]
+        except IndexError:
+            pass
+    return ''
+
+
+def ra_agent_for_template(tmpl):
+    '''@template -> ra.agent'''
+    obj = cib_factory.find_object(tmpl[1:])
+    if obj is None:
+        return None
+    return ra.get_ra(obj.node)
+
+
+def ra_agent_for_cpt(cpt):
+    '''class:provider:type -> ra.agent'''
+    agent = None
+    ra_class, provider, rsc_type = ra.disambiguate_ra_type(cpt)
+    if ra.ra_type_validate(cpt, ra_class, provider, rsc_type):
+        agent = ra.RAInfo(ra_class, rsc_type, provider)
+    return agent
+
+
+class CompletionHelp(object):
+    '''
+    Print some help on whatever last word in the line.
+    '''
+    timeout = 60  # don't print again and again
+    laststamp = 0
+    lasttopic = ''
+
+    @classmethod
+    def help(cls, topic, helptxt):
+        if cls.lasttopic == topic and \
+                time.time() - cls.laststamp < cls.timeout:
+            return
+        if helptxt:
+            import readline
+            cmdline = readline.get_line_buffer()
+            print "\n%s" % helptxt
+            if clidisplay.colors_enabled():
+                print "%s%s" % (term.render(clidisplay.prompt_noreadline(constants.prompt)),
+                                cmdline),
+            else:
+                print "%s%s" % (constants.prompt, cmdline),
+            cls.laststamp = time.time()
+            cls.lasttopic = topic
+
+
+def _prim_params_completer(agent, args):
+    completing = args[-1]
+    if completing == 'params':
+        return ['params']
+    if completing.endswith('='):
+        if len(completing) > 1 and options.interactive:
+            topic = completing[:-1]
+            CompletionHelp.help(topic, agent.meta_parameter(topic))
+        return []
+    elif '=' in completing:
+        return []
+    return [s+'=' for s in agent.completion_params()]
+
+
+def _prim_meta_completer(agent, args):
+    completing = args[-1]
+    if completing == 'meta':
+        return ['meta']
+    if '=' in completing:
+        return []
+    return [s+'=' for s in constants.rsc_meta_attributes]
+
+
+def _prim_op_completer(agent, args):
+    completing = args[-1]
+    if completing == 'op':
+        return ['op']
+    if args[-2] == 'op':
+        return constants.op_cli_names
+
+    return []
+
+
+def last_keyword(words, keyw):
+    '''returns the last occurance of an element in keyw in words'''
+    for w in reversed(words):
+        if w in keyw:
+            return w
+    return None
+
+
+def _property_completer(args):
+    '''context-sensitive completer'''
+    agent = ra.get_properties_meta()
+    return _prim_params_completer(agent, args)
+
+
+def primitive_complete_complex(args):
+    '''
+    This completer depends on the content of the line, i.e. on
+    previous tokens, in particular on the type of the RA.
+    '''
+    cmd = get_prim_token(args, 1)
+    type_word = get_prim_token(args, 3)
+    with_template = cmd == 'primitive' and type_word.startswith('@')
+
+    if with_template:
+        agent = ra_agent_for_template(type_word)
+    else:
+        agent = ra_agent_for_cpt(type_word)
+    if agent is None:
+        return []
+
+    completers_set = {
+        "params": _prim_params_completer,
+        "meta": _prim_meta_completer,
+        "op": _prim_op_completer,
+    }
+
+    keywords = completers_set.keys()
+    if len(args) == 4:  # <cmd> <id> <type> <?>
+        return keywords
+
+    last_keyw = last_keyword(args, keywords)
+    if last_keyw is None:
+        return []
+
+    return completers_set[last_keyw](agent, args) + keywords
+
+
+class CibConfig(command.UI):
+    '''
+    The configuration class
+    '''
+    name = "configure"
+
+    def __init__(self):
+        command.UI.__init__(self)
+        # for interactive use, we want to populate the CIB
+        # immediately so that tab completion works
+
+    def requires(self):
+        if not cib_factory.initialize():
+            return False
+        # see the configure ptest/simulate command
+        has_ptest = utils.is_program('ptest')
+        has_simulate = utils.is_program('crm_simulate')
+        if not has_ptest:
+            constants.simulate_programs["ptest"] = "crm_simulate"
+        if not has_simulate:
+            constants.simulate_programs["simulate"] = "ptest"
+        if not (has_ptest or has_simulate):
+            common_warn("neither ptest nor crm_simulate exist, check your installation")
+            constants.simulate_programs["ptest"] = ""
+            constants.simulate_programs["simulate"] = ""
+        return True
+
+    @command.name('_test')
+    @command.skill_level('administrator')
+    def do_check_structure(self, context):
+        return cib_factory.check_structure()
+
+    @command.name('_regtest')
+    @command.skill_level('administrator')
+    def do_regression_testing(self, context, param):
+        return cib_factory.regression_testing(param)
+
+    @command.name('_objects')
+    @command.skill_level('administrator')
+    def do_showobjects(self, context):
+        cib_factory.showobjects()
+
+    @command.level(ui_ra.RA)
+    def do_ra(self):
+        pass
+
+    @command.level(ui_cib.CibShadow)
+    def do_cib(self):
+        pass
+
+    @command.level(ui_cibstatus.CibStatusUI)
+    def do_cibstatus(self):
+        pass
+
+    @command.level(ui_template.Template)
+    def do_template(self):
+        pass
+
+    @command.level(ui_history.History)
+    def do_history(self):
+        pass
+
+    @command.level(ui_assist.Assist)
+    def do_assist(self):
+        pass
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(_id_show_list)
+    def do_show(self, context, *args):
+        "usage: show [xml] [<id>...]"
+        set_obj = mkset_obj(*args)
+        return set_obj.show()
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, _id_xml_list, _id_list)
+    def do_filter(self, context, filterprog, *args):
+        "usage: filter <prog> [xml] [<id>...]"
+        set_obj = mkset_obj(*args)
+        return set_obj.filter(filterprog)
+
+    @command.skill_level('administrator')
+    @command.completers(_f_group_id_list, compl.choice(['add', 'remove']),
+                        _prim_id_list, compl.choice(['after', 'before']), _prim_id_list)
+    def do_modgroup(self, context, group_id, subcmd, prim_id, *args):
+        """usage: modgroup <id> add <id> [after <id>|before <id>]
+        modgroup <id> remove <id>"""
+        if subcmd not in ("add", "remove"):
+            common_err("modgroup subcommand %s unknown" % subcmd)
+            return False
+        after_before = None
+        if args:
+            if subcmd != 'add':
+                context.fatal_error("Expected add (found %s)" % subcmd)
+            if args[0] not in ("after", "before"):
+                context.fatal_error("Expected after|before (found %s)" % args[0])
+            if len(args) != 2:
+                context.fatal_error("Expected 'after|before <id>' (%d arguments given)" %
+                                    len(args))
+            after_before = args[0]
+            ref_member_id = args[1]
+        g = cib_factory.find_object(group_id)
+        if not g:
+            context.fatal_error("group %s does not exist" % group_id)
+        if not xmlutil.is_group(g.node):
+            context.fatal_error("element %s is not a group" % group_id)
+        children = xmlutil.get_rsc_children_ids(g.node)
+        if after_before and ref_member_id not in children:
+            context.fatal_error("%s is not member of %s" % (ref_member_id, group_id))
+        if subcmd == "remove" and prim_id not in children:
+            context.fatal_error("%s is not member of %s" % (prim_id, group_id))
+        # done checking arguments
+        # have a group and children
+        if not after_before:
+            after_before = "after"
+            ref_member_id = children[-1]
+        # just do the filter
+        # (i wonder if this is a feature abuse?)
+        if subcmd == "add":
+            if after_before == "after":
+                sed_s = r's/ %s( |$)/& %s /' % (ref_member_id, prim_id)
+            else:
+                sed_s = r's/ %s( |$)/ %s& /' % (ref_member_id, prim_id)
+        else:
+            sed_s = r's/ %s( |$)/ /' % prim_id
+        l = (group_id,)
+        set_obj = mkset_obj(*l)
+        return set_obj.filter("sed -r '%s'" % sed_s)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(_id_xml_list, _id_list)
+    def do_edit(self, context, *args):
+        "usage: edit [xml] [<id>...]"
+        err_buf.buffer()  # keep error messages
+        set_obj = mkset_obj(*args)
+        err_buf.release()  # show them, but get an ack from the user
+        return set_obj.edit()
+
+    def _verify(self, set_obj_semantic, set_obj_all):
+        rc1 = set_obj_all.verify()
+        if config.core.check_frequency != "never":
+            rc2 = set_obj_semantic.semantic_check(set_obj_all)
+        else:
+            rc2 = 0
+        return rc1 and rc2 <= 1
+
+    @command.skill_level('administrator')
+    def do_verify(self, context):
+        "usage: verify"
+        set_obj_all = mkset_obj("xml")
+        return self._verify(set_obj_all, set_obj_all)
+
+    @command.skill_level('administrator')
+    def do_save(self, context, *args):
+        "usage: save [xml] <filename>"
+        if not args:
+            context.fatal_error("Expected 1 argument (0 given)")
+        if args[0] == "xml":
+            if len(args) != 2:
+                context.fatal_error("Expected 2 arguments (%d given)" % (len(args)))
+            filename = args[1]
+            set_obj = mkset_obj("xml")
+        else:
+            filename = args[0]
+            set_obj = mkset_obj()
+        return set_obj.save_to_file(filename)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.choice(['xml', 'replace', 'update']), _load_2nd_completer)
+    def do_load(self, context, *args):
+        "usage: load [xml] {replace|update} {<url>|<path>}"
+        if len(args) < 2:
+            context.fatal_error("Expected 2 arguments (0 given)")
+        if args[0] == "xml":
+            if len(args) != 3:
+                context.fatal_error("Expected 3 arguments (%d given)" % len(args))
+            url = args[2]
+            method = args[1]
+            xml = True
+        else:
+            if len(args) != 2:
+                context.fatal_error("Expected 2 arguments (%d given)" % len(args))
+            url = args[1]
+            method = args[0]
+            xml = False
+        if method not in ("replace", "update"):
+            context.fatal_error("Unknown method %s" % method)
+        if method == "replace":
+            if options.interactive and cib_factory.has_cib_changed():
+                if not utils.ask("This operation will erase all changes. Do you want to proceed?"):
+                    return False
+            cib_factory.erase()
+        if xml:
+            set_obj = mkset_obj("xml")
+        else:
+            set_obj = mkset_obj()
+        return set_obj.import_file(method, url)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.choice(gv_types.keys() + ['exportsettings']))
+    def do_graph(self, context, *args):
+        "usage: graph [<gtype> [<file> [<img_format>]]]"
+        if args and args[0] == "exportsettings":
+            return utils.save_graphviz_file(userdir.GRAPHVIZ_USER_FILE, constants.graph)
+        rc, gtype, outf, ftype = ui_utils.graph_args(args)
+        if not rc:
+            context.fatal_error("Failed to create graph")
+        rc, d = utils.load_graphviz_file(userdir.GRAPHVIZ_USER_FILE)
+        if rc and d:
+            constants.graph = d
+        set_obj = mkset_obj()
+        if not outf:
+            rc = set_obj.show_graph(gtype)
+        elif gtype == ftype:
+            rc = set_obj.save_graph(gtype, outf)
+        else:
+            rc = set_obj.graph_img(gtype, outf, ftype)
+        return rc
+
+    def _stop_if_running(self, rscs):
+        rscstate = xmlutil.RscState()
+        to_stop = [rsc for rsc in rscs if rscstate.is_running(rsc)]
+        from ui_resource import set_deep_meta_attr
+        if len(to_stop) > 0:
+            ok = all(set_deep_meta_attr(rsc, 'target-role', 'Stopped',
+                                        commit=False) for rsc in to_stop)
+            if not ok or not cib_factory.commit():
+                raise ValueError("Failed to stop one or more running resources: %s" %
+                                 (', '.join(to_stop)))
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(_id_list)
+    def do_delete(self, context, *args):
+        "usage: delete [-f|--force] <id> [<id>...]"
+        argl = list(args)
+        arg_force = argl and argl[0] == '--force'
+        if arg_force:
+            del argl[0]
+        if arg_force or config.core.force:
+            self._stop_if_running(argl)
+        return cib_factory.delete(*argl)
+
+    @command.name('default-timeouts')
+    @command.alias('default_timeouts')
+    @command.completers_repeating(_id_list)
+    def do_default_timeouts(self, context, *args):
+        "usage: default-timeouts <id> [<id>...]"
+        return cib_factory.default_timeouts(*args)
+
+    @command.skill_level('administrator')
+    @command.completers(_id_list, _id_list)
+    def do_rename(self, context, old_id, new_id):
+        "usage: rename <old_id> <new_id>"
+        return cib_factory.rename(old_id, new_id)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.choice(['nodes']))
+    def do_erase(self, context, nodes=None):
+        "usage: erase [nodes]"
+        if nodes is None:
+            return cib_factory.erase()
+        if nodes != 'nodes':
+            context.fatal_error("Expected 'nodes' (found '%s')" % (nodes))
+        return cib_factory.erase_nodes()
+
+    @command.skill_level('administrator')
+    def do_refresh(self, context):
+        "usage: refresh"
+        if options.interactive and cib_factory.has_cib_changed():
+            if not utils.ask("All changes will be dropped. Do you want to proceed?"):
+                return
+        cib_factory.refresh()
+
+    @command.alias('simulate')
+    @command.completers(compl.choice(['nograph']))
+    def do_ptest(self, context, *args):
+        "usage: ptest [nograph] [v...] [scores] [utilization] [actions]"
+        # use ptest/crm_simulate depending on which command was
+        # used
+        config.core.ptest = constants.simulate_programs[context.get_command_name()]
+        if not config.core.ptest:
+            return False
+        set_obj = mkset_obj("xml")
+        return ui_utils.ptestlike(set_obj.ptest, 'vv', context.get_command_name(), args)
+
+    def _commit(self, force=None):
+        if force and force != "force":
+            syntax_err(('configure.commit', force))
+            return False
+        if not cib_factory.has_cib_changed():
+            common_info("apparently there is nothing to commit")
+            common_info("try changing something first")
+            return True
+        rc1 = True
+        if not (force or utils.cibadmin_can_patch()):
+            rc1 = cib_factory.is_current_cib_equal()
+        rc2 = cib_factory.has_no_primitives() or \
+            self._verify(mkset_obj("xml", "changed"), mkset_obj("xml"))
+        if rc1 and rc2:
+            return cib_factory.commit()
+        if force or config.core.force:
+            common_info("commit forced")
+            return cib_factory.commit(force=True)
+        if utils.ask("Do you still want to commit?"):
+            return cib_factory.commit(force=True)
+        return False
+
+    @command.skill_level('administrator')
+    @command.wait
+    @command.completers(compl.choice(['force']))
+    def do_commit(self, context, force=None):
+        "usage: commit [force]"
+        return self._commit(force=force)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.choice(['force']))
+    def do_upgrade(self, context, force=None):
+        "usage: upgrade [force]"
+        if force and force != "force":
+            syntax_err((context.get_command_name(), force))
+            return False
+        if config.core.force or force:
+            return cib_factory.upgrade_cib_06to10(True)
+        else:
+            return cib_factory.upgrade_cib_06to10()
+
+    @command.skill_level('administrator')
+    def do_schema(self, context, schema_st=None):
+        "usage: schema [<schema>]"
+        if not schema_st:
+            print cib_factory.get_schema()
+            return True
+        return cib_factory.change_schema(schema_st)
+
+    def __conf_object(self, cmd, *args):
+        "The configure object command."
+        if cmd in constants.cib_cli_map.values() and \
+                not cib_factory.is_elem_supported(cmd):
+            common_err("%s not supported by the RNG schema" % cmd)
+            return False
+        return cib_factory.create_object(cmd, *args)
+
+    @command.skill_level('administrator')
+    @command.completers(_node_id_list, compl.choice(constants.node_attributes_keyw))
+    def do_node(self, context, *args):
+        """usage: node <uname>[:<type>]
+           [attributes <param>=<value> [<param>=<value>...]]
+           [utilization <param>=<value> [<param>=<value>...]]"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, ra_classes_or_tmpl, primitive_complete_complex)
+    def do_primitive(self, context, *args):
+        """usage: primitive <rsc> {[<class>:[<provider>:]]<type>|@<template>}
+        [params <param>=<value> [<param>=<value>...]]
+        [meta <attribute>=<value> [<attribute>=<value>...]]
+        [utilization <attribute>=<value> [<attribute>=<value>...]]
+        [operations id_spec
+            [op op_type [<attribute>=<value>...] ...]]"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, _group_completer)
+    def do_group(self, context, *args):
+        """usage: group <name> <rsc> [<rsc>...]
+        [params <param>=<value> [<param>=<value>...]]
+        [meta <attribute>=<value> [<attribute>=<value>...]]"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, _f_children_id_list, _clone_completer)
+    def do_clone(self, context, *args):
+        """usage: clone <name> <rsc>
+        [params <param>=<value> [<param>=<value>...]]
+        [meta <attribute>=<value> [<attribute>=<value>...]]"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.alias('master')
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, _f_children_id_list, _ms_completer)
+    def do_ms(self, context, *args):
+        """usage: ms <name> <rsc>
+        [params <param>=<value> [<param>=<value>...]]
+        [meta <attribute>=<value> [<attribute>=<value>...]]"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, ui_ra.complete_class_provider_type,
+                                  primitive_complete_complex)
+    def do_rsc_template(self, context, *args):
+        """usage: rsc_template <name> [<class>:[<provider>:]]<type>
+        [params <param>=<value> [<param>=<value>...]]
+        [meta <attribute>=<value> [<attribute>=<value>...]]
+        [utilization <attribute>=<value> [<attribute>=<value>...]]
+        [operations id_spec
+            [op op_type [<attribute>=<value>...] ...]]"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.null, _top_rsc_id_list)
+    def do_location(self, context, *args):
+        """usage: location <id> <rsc> {node_pref|rules}
+
+        node_pref :: <score>: <node>
+
+        rules ::
+          rule [id_spec] [$role=<role>] <score>: <expression>
+          [rule [id_spec] [$role=<role>] <score>: <expression> ...]
+
+        id_spec :: $id=<id> | $id-ref=<id>
+        score :: <number> | <attribute> | [-]inf
+        expression :: <simple_exp> [bool_op <simple_exp> ...]
+        bool_op :: or | and
+        simple_exp :: <attribute> [type:]<binary_op> <value>
+                      | <unary_op> <attribute>
+                      | date <date_expr>
+        type :: string | version | number
+        binary_op :: lt | gt | lte | gte | eq | ne
+        unary_op :: defined | not_defined"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.alias('collocation')
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, compl.null, top_rsc_tmpl_id_list)
+    def do_colocation(self, context, *args):
+        """usage: colocation <id> <score>: <rsc>[:<role>] <rsc>[:<role>] ...
+        [node-attribute=<node_attr>]"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null,
+                                  compl.call(schema.rng_attr_values, 'rsc_order', 'kind'),
+                                  top_rsc_tmpl_id_list)
+    def do_order(self, context, *args):
+        """usage: order <id> {kind|<score>}: <rsc>[:<action>] <rsc>[:<action>] ...
+        [symmetrical=<bool>]"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, compl.null, top_rsc_tmpl_id_list)
+    def do_rsc_ticket(self, context, *args):
+        """usage: rsc_ticket <id> <ticket_id>: <rsc>[:<role>] [<rsc>[:<role>] ...]
+        [loss-policy=<loss_policy_action>]"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(_property_completer)
+    def do_property(self, context, *args):
+        "usage: property [$id=<set_id>] <option>=<value>"
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(_prim_meta_completer)
+    def do_rsc_defaults(self, context, *args):
+        "usage: rsc_defaults [$id=<set_id>] <option>=<value>"
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(op_attr_list)
+    def do_op_defaults(self, context, *args):
+        "usage: op_defaults [$id=<set_id>] <option>=<value>"
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(node_id_colon_list, stonith_resource_list)
+    def do_fencing_topology(self, context, *args):
+        "usage: fencing_topology [<node>:] stonith_resources [stonith_resources ...]"
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    def do_xml(self, context, *args):
+        "usage: xml <xml>"
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers(_f_children_id_list)
+    def do_monitor(self, context, *args):
+        "usage: monitor <rsc>[:<role>] <interval>[:<timeout>]"
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('expert')
+    @command.completers_repeating(compl.null, compl.choice(["role:", "read", "write", "deny"]))
+    def do_user(self, context, *args):
+        """user <uid> {roles|rules}
+
+        roles :: role:<role-ref> [role:<role-ref> ...]
+        rules :: rule [rule ...]
+
+        (See the role command for details on rules.)"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('expert')
+    @command.completers_repeating(compl.null, compl.choice(["read", "write", "deny"]))
+    def do_role(self, context, *args):
+        """role <role-id> rule [rule ...]
+
+        rule :: acl-right cib-spec [attribute:<attribute>]
+
+        acl-right :: read | write | deny
+
+        cib-spec :: xpath-spec | tag-ref-spec
+        xpath-spec :: xpath:<xpath> | shortcut
+        tag-ref-spec :: tag:<tag> | ref:<id> | tag:<tag> ref:<id>
+
+        shortcut :: meta:<rsc>[:<attr>]
+                    params:<rsc>[:<attr>]
+                    utilization:<rsc>
+                    location:<rsc>
+                    property[:<attr>]
+                    node[:<node>]
+                    nodeattr[:<attr>]
+                    nodeutil[:<node>]
+                    status"""
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('expert')
+    def do_acl_target(self, context, *args):
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, top_rsc_tmpl_id_list)
+    def do_tag(self, context, *args):
+        return self.__conf_object(context.get_command_name(), *args)
+
+    @command.skill_level('expert')
+    @command.completers_repeating(_rsc_id_list)
+    def do_rsctest(self, context, *args):
+        "usage: rsctest <rsc_id> [<rsc_id> ...] [<node_id> ...]"
+        rc = True
+        rsc_l = []
+        node_l = []
+        current = "r"
+        for ident in args:
+            el = cib_factory.find_object(ident)
+            if not el:
+                common_err("element %s does not exist" % ident)
+                rc = False
+            elif current == "r" and xmlutil.is_resource(el.node):
+                if xmlutil.is_container(el.node):
+                    rsc_l += el.node.findall("primitive")
+                else:
+                    rsc_l.append(el.node)
+            elif xmlutil.is_normal_node(el.node):
+                current = "n"
+                node_l.append(el.node.get("uname"))
+            else:
+                syntax_err((context.get_command_name(), ident), context='rsctest')
+                return False
+        if not rc:
+            return False
+        if not rsc_l:
+            common_err("specify at least one resource")
+            return False
+        all_nodes = cib_factory.node_id_list()
+        if not node_l:
+            node_l = all_nodes
+        return rsctest.test_resources(rsc_l, node_l, all_nodes)
+
+    def should_wait(self):
+        return cib_factory.has_cib_changed()
+
+    def end_game(self, no_questions_asked=False):
+        ok = True
+        if cib_factory.has_cib_changed():
+            if no_questions_asked or not options.interactive:
+                ok = self._commit()
+            elif utils.ask("There are changes pending. Do you want to commit them?"):
+                ok = self._commit()
+        cib_factory.reset()
+        return ok
diff --git a/modules/ui_context.py b/modules/ui_context.py
new file mode 100644
index 0000000..c3ef623
--- /dev/null
+++ b/modules/ui_context.py
@@ -0,0 +1,371 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import shlex
+import sys
+import config
+import utils
+import options
+from msg import common_err, common_info, common_warn
+import ui_utils
+import userdir
+
+
+#import logging
+#logging.basicConfig(level=logging.DEBUG,
+#                    filename='/tmp/crm-completion.log',
+#                    filemode='a')
+
+class Context(object):
+    """
+    Context is a cursor that marks the current
+    location of the user in the UI hierarchy.
+    It maintains a stack of UILevel objects, so
+    level_stack[-1] is the current level.
+
+    The Context is passed as the first parameter
+    to any command.
+    """
+    def __init__(self, root):
+        self.stack = [root]
+        self._mark = 0
+        self._in_transit = False
+        self._wait_for_dc = False
+
+        # holds information about the currently
+        # executing command
+        self.command_name = None
+        self.command_args = None
+        self.command_info = None
+
+        # readline cache
+        self._rl_line = None
+        self._rl_words = []
+
+    def run(self, line):
+        '''
+        Execute the given command line.
+        '''
+        line = line.strip()
+        if not line or line.startswith('#'):
+            return True
+
+        self._mark = len(self.stack)
+        self._in_transit = False
+        self._wait_for_dc = False
+
+        rv = True
+        cmd = False
+        try:
+            tokens = shlex.split(line)
+            while tokens:
+                token, tokens = tokens[0], tokens[1:]
+                self.command_name = token
+                self.command_args = tokens
+                self.command_info = self.current_level().get_child(token)
+                if not self.command_info:
+                    self.fatal_error("No such command")
+                if self.command_info.type == 'level':
+                    self.enter_level(self.command_info.level)
+                else:
+                    cmd = True
+                    break
+            if cmd:
+                rv = self.execute_command() is not False
+        except ValueError, msg:
+            common_err("%s: %s" % (self.get_qualified_name(), msg))
+            rv = False
+        except IOError, msg:
+            common_err("%s: %s" % (self.get_qualified_name(), msg))
+            rv = False
+        if cmd or (rv is False):
+            rv = self._back_out() and rv
+
+        # wait for dc if wait flag set
+        if rv and self._wait_for_dc:
+            return utils.wait4dc(self.command_name, not options.batch)
+        return rv
+
+    def complete(self, line):
+        '''
+        Given a (partial) command line, returns
+        a list of potential completions.
+        A space at the end of the line is significant.
+        '''
+        complete_next = line.endswith(' ')
+        # if complete_next:
+        #    print >>sys.stderr, "complete_next is on"
+
+        # copy current state
+        prev_stack = list(self.stack)
+        prev_name = self.command_name
+        prev_args = self.command_args
+        prev_info = self.command_info
+        try:
+            line = line.strip()
+            if not line or line.startswith('#'):
+                return self.current_level().get_completions()
+
+            try:
+                tokens = shlex.split(line)
+                if complete_next:
+                    tokens += ['']
+                while tokens:
+                    token, tokens = tokens[0], tokens[1:]
+                    self.command_name = token
+                    self.command_args = tokens
+                    self.command_info = self.current_level().get_child(token)
+
+                    if not self.command_info:
+                        return self.current_level().get_completions()
+                    if self.command_info.type == 'level':
+                        self.enter_level(self.command_info.level)
+                    else:
+                        # use the completer for the command
+                        ret = self.command_info.complete(self, tokens)
+                        if tokens:
+                            ret = [t for t in ret if t.startswith(tokens[-1])]
+                        return ret
+                # reached the end on a valid level.
+                # return the completions for the previous level.
+                if self.previous_level():
+                    return self.previous_level().get_completions()
+                # not sure this is the right thing to do
+                return self.current_level().get_completions()
+            except ValueError:
+                #common_err("%s: %s" % (self.get_qualified_name(), msg))
+                pass
+            except IOError:
+                #common_err("%s: %s" % (self.get_qualified_name(), msg))
+                pass
+            return []
+        finally:
+            # restore level stack
+            self.stack = prev_stack
+            self.command_name = prev_name
+            self.command_args = prev_args
+            self.command_info = prev_info
+
+    def setup_readline(self):
+        import readline
+        readline.set_history_length(100)
+        for v in ('tab: complete',
+                  #'set bell-style visible',
+                  #'set menu-complete-display-prefix on',
+                  #'set show-all-if-ambiguous on',
+                  #'set show-all-if-unmodified on',
+                  'set skip-completed-text on'):
+            readline.parse_and_bind(v)
+        readline.set_completer(self.readline_completer)
+        readline.set_completer_delims(' \t\n,')
+        try:
+            readline.read_history_file(userdir.HISTORY_FILE)
+        except IOError:
+            pass
+
+    def clear_readline_cache(self):
+        self._rl_line = None
+        self._rl_words = []
+
+    def readline_completer(self, text, state):
+        import readline
+
+        def matching(word):
+            'we are only completing the last word in the line'
+            return word.split()[-1].startswith(text)
+
+        line = utils.get_line_buffer() + readline.get_line_buffer()
+        if line != self._rl_line:
+            try:
+                self._rl_line = line
+                completions = self.complete(line)
+                if text:
+                    self._rl_words = [w for w in completions if matching(w)]
+                else:
+                    self._rl_words = completions
+            except Exception, msg:
+                #logging.exception(msg)
+                self.clear_readline_cache()
+
+        try:
+            ret = self._rl_words[state]
+        except IndexError:
+            ret = None
+        #logging.debug("line:%s, text:%s, ret:%s, state:%s", repr(line), repr(text), ret, state)
+        if not text or (ret and line.split()[-1].endswith(ret)):
+            return ret + ' '
+        return ret
+
+    def current_level(self):
+        return self.stack[-1]
+
+    def previous_level(self):
+        if len(self.stack) > 1:
+            return self.stack[-2]
+        return None
+
+    def enter_level(self, level):
+        '''
+        Pushes an instance of the given UILevel
+        subclass onto self.stack. Checks prerequirements
+        for the level (if any).
+        '''
+        # on entering new level we need to set the
+        # interactive option _before_ creating the level
+        if not options.interactive and not self.command_args:
+            self._set_interactive()
+
+        # not sure what this is all about
+        self._in_transit = True
+
+        entry = level()
+        if 'requires' in dir(entry) and not entry.requires():
+            self.fatal_error("Missing requirements")
+        self.stack.append(entry)
+        self.clear_readline_cache()
+
+    def _set_interactive(self):
+        '''Set the interactive option only if we're on a tty.'''
+        if utils.can_ask():
+            options.interactive = True
+
+    def execute_command(self):
+        # build argument list
+        arglist = [self.current_level(), self] + self.command_args
+        # nskip = 2 to skip self and context when reporting errors
+        ui_utils.validate_arguments(self.command_info.function, arglist, nskip=2)
+        self.check_skill_level(self.command_info.skill_level)
+        rv = self.command_info.function(*arglist)
+
+        # should we wait till the command takes effect?
+        if rv and self.should_wait():
+            self._wait_for_dc = True
+        return rv
+
+    def should_wait(self):
+        if not config.core.wait:
+            return False
+
+        if self.command_info.wait:
+            return True
+
+        by_level = self.current_level().should_wait()
+        transit_or_noninteractive = self.is_in_transit() or not options.interactive
+        return by_level and transit_or_noninteractive
+
+    def is_in_transit(self):
+        '''
+        TODO
+        FIXME
+        '''
+        return self._in_transit
+
+    def check_skill_level(self, skill_level):
+        levels_to = {0: 'operator', 1: 'administrator', 2: 'expert'}
+        levels_from = {'operator': 0, 'administrator': 1, 'expert': 2}
+        if levels_from.get(config.core.skill_level, 0) < skill_level:
+            self.fatal_error("ACL %s skill level required" %
+                             (levels_to.get(skill_level, 'other')))
+
+    def get_command_name(self):
+        "Returns name used to call the current command"
+        return self.command_name
+
+    def get_qualified_name(self):
+        "Returns level.command if level is not root"
+        names = '.'.join([l.name for l in self.stack[1:]])
+        if names:
+            return "%s.%s" % (names, self.get_command_name())
+        return self.get_command_name()
+
+    def get_command_info(self):
+        "Returns the ChildInfo object for the current command or level"
+        return self.command_info
+
+    def up(self):
+        '''
+        Navigate up in the levels hierarchy
+        '''
+        ok = True
+        if len(self.stack) > 1:
+            ok = self.current_level().end_game(no_questions_asked=self._in_transit) is not False
+            self.stack.pop()
+            self.clear_readline_cache()
+        return ok
+
+    def _back_out(self):
+        '''
+        Restore the stack to the marked position
+        '''
+        ok = True
+        while self._mark > 0 and len(self.stack) > self._mark:
+            ok = self.up() and ok
+        return ok
+
+    def save_stack(self):
+        self._mark = len(self.stack)
+
+    def quit(self, rc=0):
+        '''
+        Exit from the top level
+        '''
+        ok = self.current_level().end_game()
+        if options.interactive and not options.batch:
+            print "bye"
+        if ok is False and rc == 0:
+            rc = 1
+        sys.exit(rc)
+
+    def level_name(self):
+        '''
+        Returns the name of the current level.
+        Returns 'root' if at the root level.
+        '''
+        return self.current_level().name
+
+    def prompt(self):
+        'returns a prompt generated from the level stack'
+        return ' '.join(l.name for l in self.stack[1:])
+
+    def previous_level_is(self, level_name):
+        '''
+        Check call stack for previous level name
+        '''
+        prev = self.previous_level()
+        return prev and prev.name == level_name
+
+    def fatal_error(self, msg):
+        """
+        TODO: Better error messages, with full context information
+        Raise exception to get thrown out to run()
+        """
+        raise ValueError(msg)
+
+    def error_message(self, msg):
+        """
+        Error message only, don't cancel execution of command
+        """
+        common_err("%s: %s" % (self.get_qualified_name(), msg))
+
+    def warning(self, msg):
+        common_warn("%s: %s" % (self.get_qualified_name(), msg))
+
+    def info(self, msg):
+        common_info("%s: %s" % (self.get_qualified_name(), msg))
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/ui_corosync.py b/modules/ui_corosync.py
new file mode 100644
index 0000000..0378908
--- /dev/null
+++ b/modules/ui_corosync.py
@@ -0,0 +1,158 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import command
+import completers
+import utils
+from msg import err_buf
+import corosync
+
+
+def _push_completer(args):
+    try:
+        n = utils.list_cluster_nodes()
+        n.remove(utils.this_node())
+        return n
+    except:
+        n = []
+
+
+def _all_nodes(args):
+    try:
+        return utils.list_cluster_nodes()
+    except:
+        return []
+
+
+class Corosync(command.UI):
+    '''
+    Corosync is the underlying messaging layer for most HA clusters.
+    This level provides commands for editing and managing the corosync
+    configuration.
+    '''
+    name = "corosync"
+
+    def requires(self):
+        stack = utils.cluster_stack()
+        if len(stack) > 0 and stack != 'corosync':
+            err_buf.warning("Unsupported cluster stack %s detected." % (stack))
+            return False
+        return True
+
+    def do_status(self, context):
+        '''
+        Quick cluster health status. Corosync status...
+        '''
+        print corosync.cfgtool('-s')[1]
+        print corosync.quorumtool('-s')[1]
+
+    @command.skill_level('administrator')
+    def do_reload(self, context):
+        '''
+        Reload the corosync configuration
+        '''
+        return corosync.cfgtool('-R')[0] == 0
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(_push_completer)
+    def do_push(self, context, *nodes):
+        '''
+        Push corosync configuration to other cluster nodes.
+        If no nodes are provided, configuration is pushed to
+        all other cluster nodes.
+        '''
+        if not nodes:
+            nodes = utils.list_cluster_nodes()
+            nodes.remove(utils.this_node())
+        return corosync.push_configuration(nodes)
+
+    @command.skill_level('administrator')
+    @command.completers(_push_completer)
+    def do_pull(self, context, node):
+        '''
+        Pull corosync configuration from another node.
+        '''
+        return corosync.pull_configuration(node)
+
+    @command.completers_repeating(_all_nodes)
+    def do_diff(self, context, *nodes):
+        '''
+        Compare corosync configuration between nodes.
+        '''
+        checksum = False
+        if nodes and nodes[0] == '--checksum':
+            checksum = True
+            nodes = nodes[1:]
+        if not nodes:
+            nodes = utils.list_cluster_nodes()
+        return corosync.diff_configuration(nodes, checksum=checksum)
+
+    @command.skill_level('administrator')
+    def do_edit(self, context):
+        '''
+        Edit the corosync configuration.
+        '''
+        cfg = corosync.conf()
+        try:
+            utils.edit_file_ext(cfg, template='')
+        except IOError, e:
+            context.fatal_error(str(e))
+
+    def do_show(self, context):
+        '''
+        Display the corosync configuration.
+        '''
+        cfg = corosync.conf()
+        if not os.path.isfile(cfg):
+            context.fatal_error("No corosync configuration found on this node.")
+        utils.page_string(open(cfg).read())
+
+    def do_log(self, context):
+        '''
+        Display the corosync log file (if any).
+        '''
+        logfile = corosync.get_value('logging.logfile')
+        if not logfile:
+            context.fatal_error("No corosync log file configured")
+        utils.page_file(logfile)
+
+    @command.name('add-node')
+    @command.alias('add_node')
+    @command.skill_level('administrator')
+    def do_addnode(self, context, name):
+        "Add a node to the corosync nodelist"
+        corosync.add_node(name)
+
+    @command.name('del-node')
+    @command.alias('del_node')
+    @command.skill_level('administrator')
+    def do_delnode(self, context, name):
+        "Remove a node from the corosync nodelist"
+        corosync.del_node(name)
+
+    @command.skill_level('administrator')
+    @command.completers(completers.call(corosync.get_all_paths))
+    def do_get(self, context, path):
+        "Get a corosync configuration value"
+        for v in corosync.get_values(path):
+            print v
+
+    @command.skill_level('administrator')
+    def do_set(self, context, path, value):
+        "Set a corosync configuration value"
+        corosync.set_value(path, value)
diff --git a/modules/ui_history.py b/modules/ui_history.py
new file mode 100644
index 0000000..468b959
--- /dev/null
+++ b/modules/ui_history.py
@@ -0,0 +1,666 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import sys
+import time
+import re
+import bz2
+import config
+import command
+import completers as compl
+import utils
+import ui_utils
+import userdir
+import xmlutil
+import constants
+import options
+from cibconfig import mkset_obj, cib_factory
+from msg import common_err, common_debug, common_info
+from msg import syntax_err, bad_usage
+import report
+import cmd_status
+
+
+ptest_options = ["@v+", "nograph", "scores", "actions", "utilization"]
+
+crm_report = report.Report()
+
+
+class History(command.UI):
+    '''
+    The history class
+    '''
+    name = "history"
+
+    def __init__(self):
+        command.UI.__init__(self)
+        self.current_session = None
+        self._source_inited = False
+
+    def _init_source(self):
+        if self._source_inited:
+            return True
+        self._source_inited = True
+        return self._set_source(options.history)
+
+    def _set_period(self, from_time='', to_time=''):
+        '''
+        parse time specs and set period
+        '''
+        from_dt = to_dt = None
+        if from_time:
+            from_dt = utils.parse_time(from_time)
+            if not from_dt:
+                return False
+        if to_time:
+            to_dt = utils.parse_time(to_time)
+            if not to_dt:
+                return False
+        if to_dt and to_dt <= from_dt:
+            common_err("%s - %s: bad period" % (from_time, to_time))
+            return False
+        return crm_report.set_period(from_dt, to_dt)
+
+    def _check_source(self, src):
+        'a (very) quick source check'
+        if src == "live" or os.path.isfile(src) or os.path.isdir(src):
+            return True
+        else:
+            common_err("source %s doesn't exist" % src)
+            return False
+
+    def _set_source(self, src, live_from_time=None):
+        '''
+        Have the last history source survive the History
+        and Report instances
+        '''
+        common_debug("setting source to %s" % src)
+        if not self._check_source(src):
+            return False
+        crm_report.set_source(src)
+        options.history = src
+        self.current_session = None
+        to_time = ''
+        if src == "live":
+            from_time = time.ctime(live_from_time and live_from_time or (time.time() - 60*60))
+        else:
+            from_time = ''
+        return self._set_period(from_time, to_time)
+
+    @command.skill_level('administrator')
+    def do_source(self, context, src=None):
+        "usage: source {<dir>|<file>|live}"
+        if src is None:
+            print "Current source: %s" % (options.history)
+            return True
+        self._init_source()
+        if src != options.history:
+            return self._set_source(src)
+
+    @command.skill_level('administrator')
+    @command.alias('timeframe')
+    def do_limit(self, context, from_time='', to_time=''):
+        "usage: limit [<from_time> [<to_time>]]"
+        self._init_source()
+        if options.history == "live" and not from_time:
+            from_time = time.ctime(time.time() - 60*60)
+        return self._set_period(from_time, to_time)
+
+    @command.skill_level('administrator')
+    def do_refresh(self, context, force=''):
+        "usage: refresh"
+        self._init_source()
+        if options.history != "live":
+            common_info("nothing to refresh if source isn't live")
+            return False
+        if force:
+            if force != "force" and force != "--force":
+                context.fatal_error("Expected 'force' or '--force' (was '%s')" % (force))
+            force = True
+        return crm_report.refresh_source(force)
+
+    @command.skill_level('administrator')
+    def do_detail(self, context, detail_lvl):
+        "usage: detail <detail_level>"
+        self._init_source()
+        detail_num = utils.convert2ints(detail_lvl)
+        if not (isinstance(detail_num, int) and int(detail_num) >= 0):
+            bad_usage(context.get_command_name(), detail_lvl)
+            return False
+        return crm_report.set_detail(detail_lvl)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.call(crm_report.node_list))
+    def do_setnodes(self, context, *args):
+        "usage: setnodes <node> [<node> ...]"
+        self._init_source()
+        if options.history != "live":
+            common_info("setting nodes not necessary for existing reports, proceeding anyway")
+        return crm_report.set_nodes(*args)
+
+    @command.skill_level('administrator')
+    def do_info(self, context):
+        "usage: info"
+        self._init_source()
+        return crm_report.info()
+
+    @command.skill_level('administrator')
+    def do_latest(self, context):
+        "usage: latest"
+        self._init_source()
+        if not utils.wait4dc("transition", not options.batch):
+            return False
+        self._set_source("live")
+        crm_report.refresh_source()
+        f = self._get_pe_byidx(-1)
+        if not f:
+            return False
+        crm_report.show_transition_log(f)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.call(crm_report.rsc_list))
+    def do_resource(self, context, *args):
+        "usage: resource <rsc> [<rsc> ...]"
+        self._init_source()
+        return crm_report.resource(*args)
+
+    @command.skill_level('administrator')
+    @command.wait
+    @command.completers_repeating(compl.call(crm_report.node_list))
+    def do_node(self, context, *args):
+        "usage: node <node> [<node> ...]"
+        self._init_source()
+        return crm_report.node(*args)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.call(crm_report.node_list))
+    def do_log(self, context, *args):
+        "usage: log [<node> ...]"
+        self._init_source()
+        return crm_report.log(*args)
+
+    def ptest(self, nograph, scores, utilization, actions, verbosity):
+        'Send a decompressed self.pe_file to ptest'
+        try:
+            f = open(self.pe_file)
+        except IOError, msg:
+            common_err("open: %s" % msg)
+            return False
+        s = bz2.decompress(''.join(f))
+        f.close()
+        return utils.run_ptest(s, nograph, scores, utilization, actions, verbosity)
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.join(compl.call(crm_report.peinputs_list),
+                                             compl.choice(['v'])))
+    def do_peinputs(self, context, *args):
+        """usage: peinputs [{<range>|<number>} ...] [v]"""
+        self._init_source()
+        argl = list(args)
+        opt_l = utils.fetch_opts(argl, ["v"])
+        if argl:
+            l = []
+            for s in argl:
+                a = utils.convert2ints(s.split(':'))
+                if a and len(a) == 2 and not utils.check_range(a):
+                    common_err("%s: invalid peinputs range" % a)
+                    return False
+                l += crm_report.pelist(a, long=("v" in opt_l))
+        else:
+            l = crm_report.pelist(long=("v" in opt_l))
+        if not l:
+            return False
+        s = '\n'.join(l)
+        utils.page_string(s)
+
+    def _get_pe_byname(self, s):
+        l = crm_report.find_pe_files(s)
+        if len(l) == 0:
+            common_err("%s: path not found" % s)
+            return None
+        elif len(l) > 1:
+            common_err("%s: path ambiguous" % s)
+            return None
+        return l[0]
+
+    def _get_pe_byidx(self, idx):
+        l = crm_report.pelist()
+        if len(l) < abs(idx):
+            if idx == -1:
+                common_err("no transitions found in the source")
+            else:
+                common_err("PE input file for index %d not found" % (idx+1))
+            return None
+        return l[idx]
+
+    def _get_pe_bynum(self, n):
+        l = crm_report.pelist([n])
+        if len(l) == 0:
+            common_err("PE file %d not found" % n)
+            return None
+        elif len(l) > 1:
+            common_err("PE file %d ambiguous" % n)
+            return None
+        return l[0]
+
+    def _get_pe_input(self, pe_spec):
+        '''Get PE input file from the <number>|<index>|<file>
+        spec.'''
+        if re.search('pe-', pe_spec):
+            f = self._get_pe_byname(pe_spec)
+        elif utils.is_int(pe_spec):
+            n = int(pe_spec)
+            if n <= 0:
+                f = self._get_pe_byidx(n-1)
+            else:
+                f = self._get_pe_bynum(n)
+        else:
+            f = self._get_pe_byidx(-1)
+        return f
+
+    def _show_pe(self, f, opt_l):
+        self.pe_file = f  # self.pe_file needed by self.ptest
+        ui_utils.ptestlike(self.ptest, 'vv', "transition", opt_l)
+        return crm_report.show_transition_log(f)
+
+    def _display_dot(self, f):
+        if not config.core.dotty:
+            common_err("install graphviz to draw transition graphs")
+            return False
+        f = crm_report.pe2dot(f)
+        if not f:
+            common_err("dot file not found in the report")
+            return False
+        utils.show_dot_graph(f, keep_file=True, desc="configuration graph")
+        return True
+
+    def _pe2shadow(self, f, argl):
+        try:
+            name = argl[0]
+        except:
+            name = os.path.basename(f).replace(".bz2", "")
+        common_info("transition %s saved to shadow %s" % (f, name))
+        return xmlutil.pe2shadow(f, name)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.join(compl.call(crm_report.peinputs_list),
+                                   compl.choice(['log', 'showdot', 'save'])))
+    def do_transition(self, context, *args):
+        """usage: transition [<number>|<index>|<file>] [nograph] [v...] [scores] [actions] [utilization]
+        transition showdot [<number>|<index>|<file>]
+        transition log [<number>|<index>|<file>]
+        transition save [<number>|<index>|<file> [name]]"""
+        self._init_source()
+        argl = list(args)
+        subcmd = "show"
+        if argl and argl[0] in ("showdot", "log", "save"):
+            subcmd = argl[0]
+            del argl[0]
+        if subcmd == "show":
+            opt_l = utils.fetch_opts(argl, ptest_options)
+        if argl:
+            f = self._get_pe_input(argl[0])
+            del argl[0]
+        else:
+            f = self._get_pe_byidx(-1)
+        if (subcmd == "save" and len(argl) > 1) or \
+                (subcmd in ("show", "showdot", "log") and argl):
+            syntax_err(args, context="transition")
+            return False
+        if not f:
+            return False
+        if subcmd == "show":
+            common_info("running ptest with %s" % f)
+            rc = self._show_pe(f, opt_l)
+        elif subcmd == "showdot":
+            rc = self._display_dot(f)
+        elif subcmd == "save":
+            rc = self._pe2shadow(f, argl)
+        else:
+            rc = crm_report.show_transition_log(f, True)
+        return rc
+
+    def _save_cib_env(self):
+        try:
+            self._cib_f_save = os.environ["CIB_file"]
+        except:
+            self._cib_f_save = None
+
+    def _reset_cib_env(self):
+        if self._cib_f_save:
+            os.environ["CIB_file"] = self._cib_f_save
+        else:
+            try:
+                del os.environ["CIB_file"]
+            except:
+                pass
+
+    def _setup_cib_env(self, pe_f):
+        '''Setup the CIB_file environment variable.
+        Alternatively, we could (or should) use shadows, but the
+        file/shadow management would be a bit involved.'''
+        if pe_f != "live":
+            os.environ["CIB_file"] = pe_f
+        else:
+            self._reset_cib_env()
+
+    def _pe_config_obj(self, pe_f):
+        '''Return set_obj of the configuration. It can later be
+        rendered using the repr() method.'''
+        self._setup_cib_env(pe_f)
+        if not cib_factory.refresh():
+            set_obj = mkset_obj("NOOBJ")
+        else:
+            set_obj = mkset_obj()
+        return set_obj
+
+    def _pe_config_noclr(self, pe_f):
+        '''Configuration with no formatting (no colors).'''
+        return self._pe_config_obj(pe_f).repr_nopretty()
+
+    def _pe_config_plain(self, pe_f):
+        '''Configuration with no formatting (but with colors).'''
+        return self._pe_config_obj(pe_f).repr(format=0)
+
+    def _pe_config(self, pe_f):
+        '''Formatted configuration.'''
+        return self._pe_config_obj(pe_f).repr()
+
+    def _pe_status(self, pe_f):
+        '''Return status as a string.'''
+        self._setup_cib_env(pe_f)
+        rc, s = cmd_status.crm_mon()
+        if rc != 0:
+            if s:
+                common_err("crm_mon exited with code %d and said: %s" %
+                           (rc, s))
+            else:
+                common_err("crm_mon exited with code %d" % rc)
+            return None
+        return s
+
+    def _pe_status_nohdr(self, pe_f):
+        '''Return status (without header) as a string.'''
+        self._setup_cib_env(pe_f)
+        rc, s = cmd_status.crm_mon()
+        if rc != 0:
+            common_err("crm_mon exited with code %d and said: %s" %
+                       (rc, s))
+            return None
+        l = s.split('\n')
+        for i, ln in enumerate(l):
+            if ln == "":
+                break
+        try:
+            while l[i] == "":
+                i += 1
+        except:
+            pass
+        return '\n'.join(l[i:])
+
+    def _get_diff_pe_input(self, t):
+        if t != "live":
+            return self._get_pe_input(t)
+        if not utils.get_dc():
+            common_err("cluster not running")
+            return None
+        return "live"
+
+    def _render_pe(self, pe_fun, t):
+        pe_f = self._get_diff_pe_input(t)
+        if not pe_f:
+            return None
+        self._save_cib_env()
+        s = pe_fun(pe_f)
+        self._reset_cib_env()
+        return s
+
+    def _worddiff(self, s1, s2):
+        s = None
+        f1 = utils.str2tmp(s1)
+        f2 = utils.str2tmp(s2)
+        if f1 and f2:
+            rc, s = utils.get_stdout("wdiff %s %s" % (f1, f2))
+        try:
+            os.unlink(f1)
+        except:
+            pass
+        try:
+            os.unlink(f2)
+        except:
+            pass
+        return s
+
+    def _unidiff(self, s1, s2, t1, t2):
+        s = None
+        f1 = utils.str2tmp(s1)
+        f2 = utils.str2tmp(s2)
+        if f1 and f2:
+            rc, s = utils.get_stdout("diff -U 0 -d -b --label %s --label %s %s %s" %
+                                     (t1, t2, f1, f2))
+        try:
+            os.unlink(f1)
+        except:
+            pass
+        try:
+            os.unlink(f2)
+        except:
+            pass
+        return s
+
+    def _diffhtml(self, s1, s2, t1, t2):
+        import difflib
+        fromlines = s1.split('\n')
+        tolines = s2.split('\n')
+        diff_l = difflib.HtmlDiff(wrapcolumn=60).make_table(
+            fromlines, tolines, t1, t2)
+        return ''.join(diff_l)
+
+    def _diff(self, pe_fun, t1, t2, html=False, wdiff=False):
+        s1 = self._render_pe(pe_fun, t1)
+        s2 = self._render_pe(pe_fun, t2)
+        if not s1 or not s2:
+            return None
+        if html:
+            s = self._diffhtml(s1, s2, t1, t2)
+        elif wdiff:
+            s = self._worddiff(s1, s2)
+        else:
+            s = self._unidiff(s1, s2, t1, t2)
+        return s
+
+    def _common_pe_render_check(self, context, opt_l, *args):
+        if context.previous_level_is("cibconfig") and cib_factory.has_cib_changed():
+            common_err("please try again after committing CIB changes")
+            return False
+        argl = list(args)
+        supported_l = ["status"]
+        if context.get_command_name() == "diff":
+            supported_l.append("html")
+        opt_l += utils.fetch_opts(argl, supported_l)
+        if argl:
+            syntax_err(' '.join(argl), context=context.get_command_name())
+            return False
+        return True
+
+    @command.skill_level('administrator')
+    @command.name('_dump')
+    def do_dump(self, context, t, *args):
+        '''dump configuration or status to a file and print file
+        name.
+        NB: The configuration is color rendered, but note that
+        that depends on the current value of the TERM variable.
+        '''
+        self._init_source()
+        opt_l = []
+        if not self._common_pe_render_check(context, opt_l, *args):
+            return False
+        if "status" in opt_l:
+            s = self._render_pe(self._pe_status_nohdr, t)
+        else:
+            s = utils.term_render(self._render_pe(self._pe_config_plain, t))
+        if context.previous_level_is("cibconfig"):
+            cib_factory.refresh()
+        if not s:
+            return False
+        print utils.str2tmp(s)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.join(compl.call(crm_report.peinputs_list), compl.choice(['live'])),
+                        compl.choice(['status']))
+    def do_show(self, context, t, *args):
+        "usage: show <pe> [status]"
+        self._init_source()
+        opt_l = []
+        if not self._common_pe_render_check(context, opt_l, *args):
+            return False
+        showfun = self._pe_config
+        if "status" in opt_l:
+            showfun = self._pe_status
+        s = self._render_pe(showfun, t)
+        if context.previous_level_is("cibconfig"):
+            cib_factory.refresh()
+        if not s:
+            return False
+        utils.page_string(s)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.join(compl.call(crm_report.peinputs_list), compl.choice(['live'])))
+    def do_graph(self, context, t, *args):
+        "usage: graph <pe> [<gtype> [<file> [<img_format>]]]"
+        self._init_source()
+        pe_f = self._get_diff_pe_input(t)
+        if not pe_f:
+            return False
+        rc, gtype, outf, ftype = ui_utils.graph_args(args)
+        if not rc:
+            return False
+        rc, d = utils.load_graphviz_file(userdir.GRAPHVIZ_USER_FILE)
+        if rc and d:
+            constants.graph = d
+        set_obj = self._pe_config_obj(pe_f)
+        if not outf:
+            rc = set_obj.show_graph(gtype)
+        elif gtype == ftype:
+            rc = set_obj.save_graph(gtype, outf)
+        else:
+            rc = set_obj.graph_img(gtype, outf, ftype)
+        if context.previous_level_is("cibconfig"):
+            cib_factory.refresh()
+        return rc
+
+    @command.skill_level('administrator')
+    @command.completers(compl.join(compl.call(crm_report.peinputs_list), compl.choice(['live'])),
+                        compl.join(compl.call(crm_report.peinputs_list), compl.choice(['live'])))
+    def do_diff(self, context, t1, t2, *args):
+        "usage: diff <pe> <pe> [status] [html]"
+        self._init_source()
+        opt_l = []
+        if not self._common_pe_render_check(context, opt_l, *args):
+            return False
+        showfun = self._pe_config_plain
+        mkhtml = "html" in opt_l
+        if "status" in opt_l:
+            showfun = self._pe_status_nohdr
+        elif mkhtml:
+            showfun = self._pe_config_noclr
+        s = self._diff(showfun, t1, t2, html=mkhtml)
+        if context.previous_level_is("cibconfig"):
+            cib_factory.refresh()
+        if s is None:
+            return False
+        if not mkhtml:
+            utils.page_string(s)
+        else:
+            sys.stdout.writelines(s)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.join(compl.call(crm_report.peinputs_list), compl.choice(['live'])),
+                        compl.join(compl.call(crm_report.peinputs_list), compl.choice(['live'])))
+    def do_wdiff(self, context, t1, t2, *args):
+        "usage: wdiff <pe> <pe> [status]"
+        self._init_source()
+        opt_l = []
+        if not self._common_pe_render_check(context, opt_l, *args):
+            return False
+        showfun = self._pe_config_plain
+        if "status" in opt_l:
+            showfun = self._pe_status_nohdr
+        s = self._diff(showfun, t1, t2, wdiff=True)
+        if context.previous_level_is("cibconfig"):
+            cib_factory.refresh()
+        if s is None:
+            return False
+        utils.page_string(s)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.call(crm_report.session_subcmd_list),
+                        compl.call(crm_report.session_list))
+    def do_session(self, context, subcmd=None, name=None):
+        "usage: session [{save|load|delete} <name> | pack [<name>] | update | list]"
+        self._init_source()
+        if not subcmd:
+            print "current session: %s" % self.current_session
+            return True
+        # verify arguments
+        if subcmd not in ("save", "load", "pack", "delete", "list", "update"):
+            common_err("unknown history session subcmd: %s" % subcmd)
+            return False
+        if name:
+            if subcmd not in ("save", "load", "pack", "delete"):
+                syntax_err(subcmd, context='session')
+                return False
+            if not utils.is_filename_sane(name):
+                return False
+        elif subcmd not in ("list", "update", "pack"):
+            syntax_err(subcmd, context='session')
+            return False
+        elif subcmd in ("update", "pack") and not self.current_session:
+            common_err("need to load a history session before update/pack")
+            return False
+        # do work
+        if not name:
+            # some commands work on the existing session
+            name = self.current_session
+        rc = crm_report.manage_session(subcmd, name)
+        # set source appropriately
+        if rc and subcmd in ("save", "load"):
+            options.history = crm_report.get_source()
+            crm_report.prepare_source()
+            self.current_session = name
+        elif rc and subcmd == "delete":
+            if name == self.current_session:
+                common_info("current history session deleted, setting source to live")
+                self._set_source("live")
+        return rc
+
+    @command.skill_level('administrator')
+    @command.completers(compl.choice(['clear']))
+    def do_exclude(self, context, arg=None):
+        "usage: exclude [<regex>|clear]"
+        self._init_source()
+        if not arg:
+            rc = crm_report.manage_excludes("show")
+        elif arg == "clear":
+            rc = crm_report.manage_excludes("clear")
+        else:
+            rc = crm_report.manage_excludes("add", arg)
+        return rc
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/ui_node.py b/modules/ui_node.py
new file mode 100644
index 0000000..80176f9
--- /dev/null
+++ b/modules/ui_node.py
@@ -0,0 +1,321 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import config
+import command
+import completers as compl
+import ui_utils
+import utils
+import xmlutil
+from msg import common_err, syntax_err, no_prog_err, common_info, common_warn
+from cliformat import cli_nvpairs, nvpairs2list
+import term
+
+
+def _oneline(s):
+    'join s into a single line of space-separated tokens'
+    return ' '.join(l.strip() for l in s.splitlines())
+
+
+def unpack_node_xmldata(node, is_offline):
+    """
+    takes an XML element defining a node, and
+    returns the data to pass to print_node
+    is_offline: function(uname) -> offline?
+    """
+    type = uname = id = ""
+    inst_attr = []
+    other = {}
+    for attr in node.keys():
+        v = node.get(attr)
+        if attr == "type":
+            type = v
+        elif attr == "uname":
+            uname = v
+        elif attr == "id":
+            id = v
+        else:
+            other[attr] = v
+    inst_attr = [cli_nvpairs(nvpairs2list(elem))
+                 for elem in node.xpath('./instance_attributes')]
+    return uname, id, type, other, inst_attr, is_offline(uname)
+
+
+def print_node(uname, id, node_type, other, inst_attr, offline):
+    """
+    Try to pretty print a node from the cib. Sth like:
+    uname(id): node_type
+        attr1=v1
+        attr2=v2
+    """
+    s_offline = offline and "(offline)" or ""
+    if not node_type:
+        node_type = "normal"
+    if uname == id:
+        print term.render("%s: %s%s" % (uname, node_type, s_offline))
+    else:
+        print term.render("%s(%s): %s%s" % (uname, id, node_type, s_offline))
+    for a in other:
+        print term.render("\t%s: %s" % (a, other[a]))
+    for s in inst_attr:
+        print term.render("\t%s" % (s))
+
+
+class NodeMgmt(command.UI):
+    '''
+    Nodes management class
+    '''
+    name = "node"
+
+    node_standby = "crm_attribute -t nodes -N '%s' -n standby -v '%s' %s"
+    node_maint = "crm_attribute -t nodes -N '%s' -n maintenance -v '%s'"
+    node_delete = """cibadmin -D -o nodes -X '<node uname="%s"/>'"""
+    node_delete_status = """cibadmin -D -o status -X '<node_state uname="%s"/>'"""
+    node_clear_state = _oneline("""cibadmin %s
+      -o status --xml-text
+      '<node_state id="%s"
+                   uname="%s"
+                   ha="active"
+                   in_ccm="false"
+                   crmd="offline"
+                   join="member"
+                   expected="down"
+                   crm-debug-origin="manual_clear"
+                   shutdown="0"
+       />'""")
+    node_clear_state_118 = "stonith_admin --confirm %s"
+    hb_delnode = config.path.hb_delnode + " '%s'"
+    crm_node = "crm_node"
+    node_fence = "crm_attribute -t status -U '%s' -n terminate -v true"
+    dc = "crmadmin -D"
+    node_attr = {
+        'set': "crm_attribute -t nodes -U '%s' -n '%s' -v '%s'",
+        'delete': "crm_attribute -D -t nodes -U '%s' -n '%s'",
+        'show': "crm_attribute -G -t nodes -U '%s' -n '%s'",
+    }
+    node_status = {
+        'set': "crm_attribute -t status -U '%s' -n '%s' -v '%s'",
+        'delete': "crm_attribute -D -t status -U '%s' -n '%s'",
+        'show': "crm_attribute -G -t status -U '%s' -n '%s'",
+    }
+    node_utilization = {
+        'set': "crm_attribute -z -t nodes -U '%s' -n '%s' -v '%s'",
+        'delete': "crm_attribute -z -D -t nodes -U '%s' -n '%s'",
+        'show': "crm_attribute -z -G -t nodes -U '%s' -n '%s'",
+    }
+
+    def requires(self):
+        for p in ('cibadmin', 'crm_attribute'):
+            if not utils.is_program(p):
+                no_prog_err(p)
+                return False
+        return True
+
+    @command.completers(compl.nodes)
+    def do_status(self, context, node=None):
+        'usage: status [<node>]'
+        a = node and ('--xpath "//nodes/node[@uname=\'%s\']"' % node) or \
+            '-o nodes'
+        return utils.ext_cmd("%s %s" % (xmlutil.cib_dump, a)) == 0
+
+    @command.alias('list')
+    @command.completers(compl.nodes)
+    def do_show(self, context, node=None):
+        'usage: show [<node>]'
+        cib_elem = xmlutil.cibdump2elem()
+        if cib_elem is None:
+            return False
+        try:
+            nodes_node = cib_elem.xpath("//configuration/nodes")[0]
+            status = cib_elem.findall("status")[0]
+            node_state = status.xpath(".//node_state")
+        except:
+            return False
+
+        def is_offline(uname):
+            for state in node_state:
+                if state.get("uname") == uname and state.get("crmd") == "offline":
+                    return True
+            return False
+
+        for c in nodes_node.iterchildren():
+            if c.tag != "node" or (node is not None and c.get("uname") != node):
+                continue
+            print_node(*unpack_node_xmldata(c, is_offline))
+
+    @command.wait
+    @command.completers(compl.nodes)
+    def do_standby(self, context, *args):
+        'usage: standby [<node>] [<lifetime>]'
+        argl = list(args)
+        node = None
+        lifetime = utils.fetch_lifetime_opt(argl, iso8601=False)
+        if not argl:
+            node = utils.this_node()
+        elif len(argl) == 1:
+            node = args[0]
+            if not xmlutil.is_our_node(node):
+                common_err("%s: node name not recognized" % node)
+                return False
+        else:
+            syntax_err(args, context=context.get_command_name())
+            return False
+        opts = ''
+        if lifetime:
+            opts = "--lifetime='%s'" % lifetime
+        else:
+            opts = "--lifetime='forever'"
+        return utils.ext_cmd(self.node_standby % (node, "on", opts)) == 0
+
+    @command.wait
+    @command.completers(compl.nodes)
+    def do_online(self, context, node=None):
+        'usage: online [<node>]'
+        if not node:
+            node = utils.this_node()
+        if not utils.is_name_sane(node):
+            return False
+        return utils.ext_cmd(self.node_standby % (node, "off", "--lifetime='forever'")) == 0
+
+    @command.wait
+    @command.completers(compl.nodes)
+    def do_maintenance(self, context, node=None):
+        'usage: maintenance [<node>]'
+        if not node:
+            node = utils.this_node()
+        if not utils.is_name_sane(node):
+            return False
+        return utils.ext_cmd(self.node_maint % (node, "on")) == 0
+
+    @command.wait
+    @command.completers(compl.nodes)
+    def do_ready(self, context, node=None):
+        'usage: ready [<node>]'
+        if not node:
+            node = utils.this_node()
+        if not utils.is_name_sane(node):
+            return False
+        return utils.ext_cmd(self.node_maint % (node, "off")) == 0
+
+    @command.wait
+    @command.completers(compl.nodes)
+    def do_fence(self, context, node):
+        'usage: fence <node>'
+        if not utils.is_name_sane(node):
+            return False
+        if not config.core.force and \
+                not utils.ask("Do you really want to shoot %s?" % node):
+            return False
+        return utils.ext_cmd(self.node_fence % (node)) == 0
+
+    @command.wait
+    @command.completers(compl.nodes)
+    def do_clearstate(self, context, node=None):
+        'usage: clearstate <node>'
+        if not node:
+            node = utils.this_node()
+        if not utils.is_name_sane(node):
+            return False
+        if not config.core.force and \
+                not utils.ask("Do you really want to drop state for node %s?" % node):
+            return False
+        if utils.is_pcmk_118():
+            return utils.ext_cmd(self.node_clear_state_118 % node) == 0
+        else:
+            return utils.ext_cmd(self.node_clear_state % ("-M -c", node, node)) == 0 and \
+                utils.ext_cmd(self.node_clear_state % ("-R", node, node)) == 0
+
+    def _call_delnode(self, node):
+        "Remove node (how depends on cluster stack)"
+        rc = True
+        if utils.cluster_stack() == "heartbeat":
+            cmd = (self.hb_delnode % node)
+        else:
+            ec, s = utils.get_stdout("%s -p" % self.crm_node)
+            if not s:
+                common_err('%s -p could not list any nodes (rc=%d)' %
+                           (self.crm_node, ec))
+                rc = False
+            else:
+                partition_l = s.split()
+                if node in partition_l:
+                    common_err("according to %s, node %s is still active" %
+                               (self.crm_node, node))
+                    rc = False
+            cmd = "%s --force -R %s" % (self.crm_node, node)
+        if not rc:
+            if config.core.force:
+                common_info('proceeding with node %s removal' % node)
+            else:
+                return False
+        ec = utils.ext_cmd(cmd)
+        if ec != 0:
+            common_warn('"%s" failed, rc=%d' % (cmd, ec))
+            return False
+        return True
+
+    @command.completers(compl.nodes)
+    def do_delete(self, context, node):
+        'usage: delete <node>'
+        if not utils.is_name_sane(node):
+            return False
+        if not xmlutil.is_our_node(node):
+            common_err("node %s not found in the CIB" % node)
+            return False
+        if not self._call_delnode(node):
+            return False
+        if utils.ext_cmd(self.node_delete % node) != 0 or \
+                utils.ext_cmd(self.node_delete_status % node) != 0:
+            common_err("%s removed from membership, but not from CIB!" % node)
+            return False
+        common_info("node %s deleted" % node)
+        return True
+
+    @command.wait
+    @command.completers(compl.nodes, compl.choice(['set', 'delete', 'show']), compl.resources)
+    def do_attribute(self, context, node, cmd, rsc, value=None):
+        """usage:
+        attribute <node> set <rsc> <value>
+        attribute <node> delete <rsc>
+        attribute <node> show <rsc>"""
+        return ui_utils.manage_attr(context.get_command_name(), self.node_attr,
+                                    node, cmd, rsc, value)
+
+    @command.wait
+    @command.completers(compl.nodes, compl.choice(['set', 'delete', 'show']), compl.resources)
+    def do_utilization(self, context, node, cmd, rsc, value=None):
+        """usage:
+        utilization <node> set <rsc> <value>
+        utilization <node> delete <rsc>
+        utilization <node> show <rsc>"""
+        return ui_utils.manage_attr(context.get_command_name(), self.node_utilization,
+                                    node, cmd, rsc, value)
+
+    @command.wait
+    @command.name('status-attr')
+    @command.completers(compl.nodes, compl.choice(['set', 'delete', 'show']), compl.resources)
+    def do_status_attr(self, context, node, cmd, rsc, value=None):
+        """usage:
+        status-attr <node> set <rsc> <value>
+        status-attr <node> delete <rsc>
+        status-attr <node> show <rsc>"""
+        return ui_utils.manage_attr(context.get_command_name(), self.node_status,
+                                    node, cmd, rsc, value)
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/ui_options.py b/modules/ui_options.py
new file mode 100644
index 0000000..014b72b
--- /dev/null
+++ b/modules/ui_options.py
@@ -0,0 +1,191 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import command
+import completers
+import config
+import options
+
+_yesno = completers.choice(['yes', 'no'])
+
+_legacy_map = {
+    'editor': ('core', 'editor'),
+    'pager': ('core', 'pager'),
+    'user': ('core', 'user'),
+    'skill_level': ('core', 'skill_level'),
+    'sort_elements': ('core', 'sort_elements'),
+    'check_frequency': ('core', 'check_frequency'),
+    'check_mode': ('core', 'check_mode'),
+    'wait': ('core', 'wait'),
+    'add_quotes': ('core', 'add_quotes'),
+    'manage_children': ('core', 'manage_children'),
+    'force': ('core', 'force'),
+    'debug': ('core', 'debug'),
+    'ptest': ('core', 'ptest'),
+    'dotty': ('core', 'dotty'),
+    'dot': ('core', 'dot'),
+    'output': ('color', 'style'),
+    'colorscheme': ('color', 'colorscheme'),
+}
+
+
+def _legacy_set_pref(name, value):
+    'compatibility with old versions'
+    name = name.replace('-', '_')
+    if name == 'colorscheme':
+        return  # TODO
+    opt = _legacy_map.get(name)
+    if opt:
+        config.set_option(opt[0], opt[1], value)
+
+
+def _getprefs(opt):
+    'completer for legacy options'
+    opt = opt.replace('-', '_')
+    if opt == 'colorscheme':
+        return ('black', 'blue', 'green', 'cyan',
+                'red', 'magenta', 'yellow', 'white', 'normal')
+    opt = _legacy_map.get(opt)
+    if opt:
+        return config.complete(*opt)
+    return []
+
+
+def _set_completer(args):
+    opt = args[-1]
+    opts = opt.split('.')
+    if len(opts) != 2:
+        return []
+    return config.complete(*opts)
+
+
+class CliOptions(command.UI):
+    '''
+    Manage user preferences
+    '''
+    name = "options"
+
+    @command.completers(completers.choice(config.get_all_options()), _set_completer)
+    def do_set(self, context, option, value):
+        '''usage: set <option> <value>'''
+        parts = option.split('.')
+        if len(parts) != 2:
+            context.fatal_error("Unknown option: " + option)
+        config.set_option(parts[0], parts[1], value)
+
+    @command.name('skill-level')
+    @command.alias('skill_level')
+    @command.completers(_getprefs('skill_level'))
+    def do_skill_level(self, context, level):
+        """usage: skill-level <level>
+        level: operator | administrator | expert"""
+        return _legacy_set_pref('skill-level', level)
+
+    def do_editor(self, context, program):
+        "usage: editor <program>"
+        return _legacy_set_pref('editor', program)
+
+    def do_pager(self, context, program):
+        "usage: pager <program>"
+        return _legacy_set_pref('pager', program)
+
+    def do_user(self, context, crm_user=''):
+        "usage: user [<crm_user>]"
+        return _legacy_set_pref('user', crm_user)
+
+    @command.completers(_getprefs('output'))
+    def do_output(self, context, output_type):
+        "usage: output <type>"
+        return _legacy_set_pref("output", output_type)
+
+    def do_colorscheme(self, context, colors):
+        "usage: colorscheme <colors>"
+        return _legacy_set_pref("colorscheme", colors)
+
+    @command.name('check-frequency')
+    @command.alias('check_frequency')
+    @command.completers(_getprefs('check_frequency'))
+    def do_check_frequency(self, context, freq):
+        "usage: check-frequency <freq>"
+        return _legacy_set_pref("check-frequency", freq)
+
+    @command.name('check-mode')
+    @command.alias('check_mode')
+    @command.completers(_getprefs('check_mode'))
+    def do_check_mode(self, context, mode):
+        "usage: check-mode <mode>"
+        return _legacy_set_pref("check-mode", mode)
+
+    @command.name('sort-elements')
+    @command.alias('sort_elements')
+    @command.completers(_yesno)
+    def do_sort_elements(self, context, opt):
+        "usage: sort-elements {yes|no}"
+        return _legacy_set_pref("sort-elements", opt)
+
+    @command.completers(_yesno)
+    def do_wait(self, context, opt):
+        "usage: wait {yes|no}"
+        return _legacy_set_pref("wait", opt)
+
+    @command.name('add-quotes')
+    @command.alias('add_quotes')
+    @command.completers(_yesno)
+    def do_add_quotes(self, context, opt):
+        "usage: add-quotes {yes|no}"
+        return _legacy_set_pref("add-quotes", opt)
+
+    @command.name('manage-children')
+    @command.alias('manage_children')
+    @command.completers(_getprefs('manage_children'))
+    def do_manage_children(self, context, opt):
+        "usage: manage-children <option>"
+        return _legacy_set_pref("manage-children", opt)
+
+    @command.completers(completers.choice(config.get_all_options()))
+    def do_show(self, context, option=None):
+        "usage: show [all | <option>]"
+        if option is None or option == 'all':
+            opts = None
+            if option == 'all':
+                opts = config.get_all_options()
+            else:
+                opts = config.get_configured_options()
+            for opt in opts:
+                parts = opt.split('.')
+                print "%s = %s" % (opt, config.get_option(parts[0], parts[1], raw=True))
+        else:
+            parts = option.split('.')
+            if len(parts) != 2:
+                context.fatal_error("Unknown option: " + option)
+            val = config.get_option(parts[0], parts[1], raw=True)
+            print "%s = %s" % (option, val)
+
+    def do_save(self, context):
+        "usage: save"
+        config.save()
+
+    def do_reset(self, context):
+        "usage: reset"
+        config.reset()
+
+    def end_game(self, no_questions_asked=False):
+        if no_questions_asked and not options.interactive:
+            self.do_save(None)
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/ui_ra.py b/modules/ui_ra.py
new file mode 100644
index 0000000..d3d2cf6
--- /dev/null
+++ b/modules/ui_ra.py
@@ -0,0 +1,108 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import command
+import completers as compl
+import utils
+import ra
+import constants
+import options
+
+
+def complete_class_provider_type(args):
+    '''
+    This is just too complicated to complete properly...
+    '''
+    ret = set([])
+    classes = ra.ra_classes()
+    for c in classes:
+        if c != 'ocf':
+            types = ra.ra_types(c)
+            for t in types:
+                ret.add('%s:%s' % (c, t))
+
+    providers = ra.ra_providers_all('ocf')
+    for p in providers:
+        types = ra.ra_types('ocf', p)
+        for t in types:
+            ret.add('ocf:%s:%s' % (p, t))
+    return list(ret)
+
+
+class RA(command.UI):
+    '''
+    CIB shadow management class
+    '''
+    name = "ra"
+    provider_classes = ["ocf"]
+
+    def do_classes(self, context):
+        "usage: classes"
+        for c in ra.ra_classes():
+            if c in self.provider_classes:
+                print "%s / %s" % (c, ' '.join(ra.ra_providers_all(c)))
+            else:
+                print "%s" % c
+
+    @command.skill_level('administrator')
+    def do_providers(self, context, ra_type, ra_class="ocf"):
+        "usage: providers <ra> [<class>]"
+        print ' '.join(ra.ra_providers(ra_type, ra_class))
+
+    @command.skill_level('administrator')
+    @command.completers(compl.call(ra.ra_classes), lambda args: ra.ra_providers_all(args[1]))
+    def do_list(self, context, class_, provider_=None):
+        "usage: list <class> [<provider>]"
+        if class_ not in ra.ra_classes():
+            context.fatal_error("class %s does not exist" % class_)
+        if provider_ and provider_ not in ra.ra_providers_all(class_):
+            context.fatal_error("there is no provider %s for class %s" % (provider_, class_))
+        types = ra.ra_types(class_, provider_)
+        if options.regression_tests:
+            for t in types:
+                print t
+        else:
+            utils.multicolumn(types)
+
+    @command.skill_level('administrator')
+    @command.alias('meta')
+    @command.completers(complete_class_provider_type)
+    def do_info(self, context, *args):
+        "usage: info [<class>:[<provider>:]]<type>"
+        if len(args) == 0:
+            context.fatal_error("Expected [<class>:[<provider>:]]<type>")
+        elif len(args) > 1:  # obsolete syntax
+            ra_type = args[0]
+            ra_class = args[1]
+            if len(args) < 3:
+                ra_provider = "heartbeat"
+            else:
+                ra_provider = args[2]
+        else:
+            if args[0] in constants.meta_progs:
+                ra_class = args[0]
+                ra_provider = ra_type = None
+            else:
+                ra_class, ra_provider, ra_type = ra.disambiguate_ra_type(args[0])
+        agent = ra.RAInfo(ra_class, ra_type, ra_provider)
+        if agent.mk_ra_node() is None:
+            return False
+        try:
+            utils.page_string(agent.meta_pretty())
+        except Exception, msg:
+            context.fatal_error(msg)
diff --git a/modules/ui_report.py b/modules/ui_report.py
new file mode 100644
index 0000000..0d8b91d
--- /dev/null
+++ b/modules/ui_report.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import utils
+import config
+import options
+import subprocess
+from signal import signal, SIGPIPE, SIG_DFL
+
+
+def create_report(context, args):
+    toolopts = [os.path.join(config.path.sharedir, 'hb_report'),
+                'hb_report',
+                'crm_report']
+    extcmd = None
+    for tool in toolopts:
+        if utils.is_program(tool):
+            extcmd = tool
+            break
+    if not extcmd:
+        context.fatal_error("No reporting tool found")
+    cmd = [extcmd] + list(args)
+    if options.regression_tests:
+        print ".EXT", cmd
+    return subprocess.call(cmd, shell=False, preexec_fn=lambda: signal(SIGPIPE, SIG_DFL))
diff --git a/modules/ui_resource.py b/modules/ui_resource.py
new file mode 100644
index 0000000..b1f0403
--- /dev/null
+++ b/modules/ui_resource.py
@@ -0,0 +1,576 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import command
+import completers as compl
+import constants
+import config
+import utils
+import xmlutil
+import ui_utils
+import options
+
+from msg import common_error, common_err, common_info, common_debug
+from msg import no_prog_err
+from cibconfig import cib_factory
+
+
+def rm_meta_attribute(node, attr, l, force_children=False):
+    '''
+    Build a list of nvpair nodes which contain attribute
+    (recursively in all children resources)
+    '''
+    for c in node.iterchildren():
+        if c.tag == "meta_attributes":
+            nvpair = xmlutil.get_attr_in_set(c, attr)
+            if nvpair is not None:
+                l.append(nvpair)
+        elif force_children or \
+                (xmlutil.is_child_rsc(c) and not c.getparent().tag == "group"):
+            rm_meta_attribute(c, attr, l, force_children=force_children)
+
+
+def get_children_with_different_attr(node, attr, value):
+    l = []
+    for p in node.xpath(".//primitive"):
+        diff_attr = False
+        for meta_set in xmlutil.get_set_nodes(p, "meta_attributes", create=False):
+            p_value = xmlutil.get_attr_value(meta_set, attr)
+            if p_value is not None and p_value != value:
+                diff_attr = True
+                break
+        if diff_attr:
+            l.append(p)
+    return l
+
+
+def set_deep_meta_attr_node(target_node, attr, value):
+    nvpair_l = []
+    if xmlutil.is_clone(target_node):
+        for c in target_node.iterchildren():
+            if xmlutil.is_child_rsc(c):
+                rm_meta_attribute(c, attr, nvpair_l)
+    if config.core.manage_children != "never" and \
+            (xmlutil.is_group(target_node) or
+             (xmlutil.is_clone(target_node) and xmlutil.cloned_el(target_node) == "group")):
+        odd_children = get_children_with_different_attr(target_node, attr, value)
+        for c in odd_children:
+            if config.core.manage_children == "always" or \
+                    (config.core.manage_children == "ask" and
+                     utils.ask("Do you want to override %s for child resource %s?" %
+                               (attr, c.get("id")))):
+                common_debug("force remove meta attr %s from %s" %
+                             (attr, c.get("id")))
+                rm_meta_attribute(c, attr, nvpair_l, force_children=True)
+    xmlutil.rmnodes(list(set(nvpair_l)))
+    xmlutil.xml_processnodes(target_node,
+                             xmlutil.is_emptynvpairs, xmlutil.rmnodes)
+
+    # work around issue with pcs interoperability
+    # by finding exising nvpairs -- if there are any, just
+    # set the value in those. Otherwise fall back to adding
+    # to all meta_attributes tags
+    nvpairs = target_node.xpath("./meta_attributes/nvpair[@name='%s']" % (attr))
+    if len(nvpairs) > 0:
+        for nvpair in nvpairs:
+            nvpair.set("value", value)
+    else:
+        for n in xmlutil.get_set_nodes(target_node, "meta_attributes", create=True):
+            xmlutil.set_attr(n, attr, value)
+    return True
+
+
+def set_deep_meta_attr(rsc, attr, value, commit=True):
+    """
+    If the referenced rsc is a primitive that belongs to a group,
+    then set its attribute.
+    Otherwise, go up to the topmost resource which contains this
+    resource and set the attribute there (i.e. if the resource is
+    cloned).
+    If it's a group then check its children. If any of them has
+    the attribute set to a value different from the one given,
+    then ask the user whether to reset them or not (exact
+    behaviour depends on the value of config.core.manage_children).
+    """
+
+    def update_obj(obj):
+        """
+        set the meta attribute in the given object
+        """
+        node = obj.node
+        obj.set_updated()
+        if not (node.tag == "primitive" and
+                node.getparent().tag == "group"):
+            node = xmlutil.get_topmost_rsc(node)
+        return set_deep_meta_attr_node(node, attr, value)
+
+    def flatten(objs):
+        for obj in objs:
+            if isinstance(obj, list):
+                for subobj in obj:
+                    yield subobj
+            else:
+                yield obj
+
+    def resolve(obj):
+        if obj.obj_type == 'tag':
+            ret = [cib_factory.find_object(o) for o in obj.node.xpath('./obj_ref/@id')]
+            ret = [r for r in ret if r is not None]
+            return ret
+        return obj
+
+    def is_resource(obj):
+        return xmlutil.is_resource(obj.node)
+
+    objs = cib_factory.find_objects(rsc)
+    if objs is None:
+        common_error("CIB is not valid!")
+        return False
+    while any(obj for obj in objs if obj.obj_type == 'tag'):
+        objs = list(flatten(resolve(obj) for obj in objs))
+    objs = filter(is_resource, objs)
+    common_debug("set_deep_meta_attr: %s" % (', '.join([obj.obj_id for obj in objs])))
+    if not objs:
+        common_error("Resource not found: %s" % (rsc))
+        return False
+
+    ok = all(update_obj(obj) for obj in objs)
+    if not ok:
+        common_error("Failed to update meta attributes for %s" % (rsc))
+        return False
+
+    if not commit:
+        return True
+
+    ok = cib_factory.commit()
+    if not ok:
+        common_error("Failed to commit updates to %s" % (rsc))
+        return False
+    return True
+
+
+def cleanup_resource(rsc, node=''):
+    if not utils.is_name_sane(rsc) or not utils.is_name_sane(node):
+        return False
+    if not node:
+        rc = utils.ext_cmd(RscMgmt.rsc_cleanup_all % (rsc)) == 0
+    else:
+        rc = utils.ext_cmd(RscMgmt.rsc_cleanup % (rsc, node)) == 0
+    return rc
+
+
+_attrcmds = compl.choice(['delete', 'set', 'show'])
+_raoperations = compl.choice(constants.ra_operations)
+
+
+class RscMgmt(command.UI):
+    '''
+    Resources management class
+    '''
+    name = "resource"
+
+    rsc_status_all = "crm_resource -L"
+    rsc_status = "crm_resource -W -r '%s'"
+    rsc_showxml = "crm_resource -q -r '%s'"
+    rsc_setrole = "crm_resource --meta -r '%s' -p target-role -v '%s'"
+    rsc_migrate = "crm_resource -M -r '%s' %s"
+    rsc_unmigrate = "crm_resource -U -r '%s'"
+    rsc_cleanup = "crm_resource -C -r '%s' -H '%s'"
+    rsc_cleanup_all = "crm_resource -C -r '%s'"
+    rsc_maintenance = "crm_resource -r '%s' --meta -p maintenance -v '%s'"
+    rsc_param = {
+        'set': "crm_resource -r '%s' -p '%s' -v '%s'",
+        'delete': "crm_resource -r '%s' -d '%s'",
+        'show': "crm_resource -r '%s' -g '%s'",
+    }
+    rsc_meta = {
+        'set': "crm_resource --meta -r '%s' -p '%s' -v '%s'",
+        'delete': "crm_resource --meta -r '%s' -d '%s'",
+        'show': "crm_resource --meta -r '%s' -g '%s'",
+    }
+    rsc_failcount = {
+        'set': "crm_attribute -t status -n 'fail-count-%s' -N '%s' -v '%s' -d 0",
+        'delete': "crm_attribute -t status -n 'fail-count-%s' -N '%s' -D -d 0",
+        'show': "crm_attribute -t status -n 'fail-count-%s' -N '%s' -G -d 0",
+    }
+    rsc_utilization = {
+        'set': "crm_resource -z -r '%s' -p '%s' -v '%s'",
+        'delete': "crm_resource -z -r '%s' -d '%s'",
+        'show': "crm_resource -z -r '%s' -g '%s'",
+    }
+    rsc_secret = {
+        'set': "cibsecret set '%s' '%s' '%s'",
+        'stash': "cibsecret stash '%s' '%s'",
+        'unstash': "cibsecret unstash '%s' '%s'",
+        'delete': "cibsecret delete '%s' '%s'",
+        'show': "cibsecret get '%s' '%s'",
+        'check': "cibsecret check '%s' '%s'",
+    }
+    rsc_refresh = "crm_resource -C"
+    rsc_refresh_node = "crm_resource -C -H '%s'"
+    rsc_reprobe = "crm_resource -C"
+    rsc_reprobe_node = "crm_resource -C -H '%s'"
+
+    def requires(self):
+        for program in ('crm_resource', 'crm_attribute'):
+            if not utils.is_program(program):
+                no_prog_err(program)
+                return False
+        return True
+
+    @command.alias('show', 'list')
+    @command.completers(compl.resources)
+    def do_status(self, context, rsc=None):
+        "usage: status [<rsc>]"
+        if rsc:
+            if not utils.is_name_sane(rsc):
+                return False
+            return utils.ext_cmd(self.rsc_status % rsc) == 0
+        else:
+            return utils.ext_cmd(self.rsc_status_all) == 0
+
+    def _commit_meta_attr(self, context, rsc, name, value):
+        """
+        Perform change to resource
+        """
+        if not utils.is_name_sane(rsc):
+            return False
+        commit = not cib_factory.has_cib_changed()
+        if not commit:
+            context.info("Currently editing the CIB, changes will not be committed")
+        return set_deep_meta_attr(rsc, name, value, commit=commit)
+
+    @command.wait
+    @command.completers(compl.resources)
+    def do_start(self, context, rsc):
+        "usage: start <rsc>"
+        return self._commit_meta_attr(context, rsc, "target-role", "Started")
+
+    @command.wait
+    @command.completers(compl.resources)
+    def do_stop(self, context, rsc):
+        "usage: stop <rsc>"
+        return self._commit_meta_attr(context, rsc, "target-role", "Stopped")
+
+    @command.wait
+    @command.completers(compl.resources)
+    def do_restart(self, context, rsc):
+        "usage: restart <rsc>"
+        common_info("ordering %s to stop" % rsc)
+        if not self._commit_meta_attr(context, rsc, "target-role", "Stopped"):
+            return False
+        if not utils.wait4dc("stop", not options.batch):
+            return False
+        common_info("ordering %s to start" % rsc)
+        return self._commit_meta_attr(context, rsc, "target-role", "Started")
+
+    @command.wait
+    @command.completers(compl.resources)
+    def do_promote(self, context, rsc):
+        "usage: promote <rsc>"
+        if not utils.is_name_sane(rsc):
+            return False
+        if not xmlutil.RscState().is_ms(rsc):
+            common_err("%s is not a master-slave resource" % rsc)
+            return False
+        return utils.ext_cmd(self.rsc_setrole % (rsc, "Master")) == 0
+
+    def do_scores(self, context):
+        "usage: scores"
+        if utils.is_program('crm_simulate'):
+            utils.ext_cmd('crm_simulate -sL')
+        elif utils.is_program('ptest'):
+            utils.ext_cmd('ptest -sL')
+        else:
+            context.fatal_error("Need crm_simulate or ptest in path to display scores")
+
+    @command.wait
+    @command.completers(compl.resources)
+    def do_demote(self, context, rsc):
+        "usage: demote <rsc>"
+        if not utils.is_name_sane(rsc):
+            return False
+        if not xmlutil.RscState().is_ms(rsc):
+            common_err("%s is not a master-slave resource" % rsc)
+            return False
+        return utils.ext_cmd(self.rsc_setrole % (rsc, "Slave")) == 0
+
+    @command.completers(compl.resources)
+    def do_manage(self, context, rsc):
+        "usage: manage <rsc>"
+        return self._commit_meta_attr(context, rsc, "is-managed", "true")
+
+    @command.completers(compl.resources)
+    def do_unmanage(self, context, rsc):
+        "usage: unmanage <rsc>"
+        return self._commit_meta_attr(context, rsc, "is-managed", "false")
+
+    @command.alias('move')
+    @command.skill_level('administrator')
+    @command.wait
+    @command.completers_repeating(compl.resources, compl.nodes,
+                                  compl.choice(['reboot', 'forever', 'force']))
+    def do_migrate(self, context, rsc, *args):
+        """usage: migrate <rsc> [<node>] [<lifetime>] [force]"""
+        if not utils.is_name_sane(rsc):
+            return False
+        node = None
+        argl = list(args)
+        force = "force" in utils.fetch_opts(argl, ["force"])
+        lifetime = utils.fetch_lifetime_opt(argl)
+        if len(argl) > 0:
+            node = argl[0]
+            if not xmlutil.is_our_node(node):
+                context.fatal_error("Not our node: " + node)
+        opts = ''
+        if node:
+            opts = "--node='%s'" % node
+        if lifetime:
+            opts = "%s --lifetime='%s'" % (opts, lifetime)
+        if force or config.core.force:
+            opts = "%s --force" % opts
+        return utils.ext_cmd(self.rsc_migrate % (rsc, opts)) == 0
+
+    @command.alias('unmove')
+    @command.skill_level('administrator')
+    @command.wait
+    @command.completers(compl.resources)
+    def do_unmigrate(self, context, rsc):
+        "usage: unmigrate <rsc>"
+        if not utils.is_name_sane(rsc):
+            return False
+        return utils.ext_cmd(self.rsc_unmigrate % rsc) == 0
+
+    @command.skill_level('administrator')
+    @command.wait
+    @command.completers(compl.resources, compl.nodes)
+    def do_cleanup(self, context, resource, node=''):
+        "usage: cleanup <rsc> [<node>]"
+        # Cleanup a resource on a node. Omit node to cleanup on
+        # all live nodes.
+        return cleanup_resource(resource, node)
+
+    @command.wait
+    @command.completers(compl.resources, _attrcmds, compl.nodes)
+    def do_failcount(self, context, rsc, cmd, node, value=None):
+        """usage:
+        failcount <rsc> set <node> <value>
+        failcount <rsc> delete <node>
+        failcount <rsc> show <node>"""
+        return ui_utils.manage_attr(context.get_command_name(), self.rsc_failcount,
+                                    rsc, cmd, node, value)
+
+    @command.skill_level('administrator')
+    @command.wait
+    @command.completers(compl.resources, _attrcmds)
+    def do_param(self, context, rsc, cmd, param, value=None):
+        """usage:
+        param <rsc> set <param> <value>
+        param <rsc> delete <param>
+        param <rsc> show <param>"""
+        return ui_utils.manage_attr(context.get_command_name(), self.rsc_param,
+                                    rsc, cmd, param, value)
+
+    @command.skill_level('administrator')
+    @command.completers(compl.resources,
+                        compl.choice(['set', 'stash', 'unstash', 'delete', 'show', 'check']))
+    def do_secret(self, context, rsc, cmd, param, value=None):
+        """usage:
+        secret <rsc> set <param> <value>
+        secret <rsc> stash <param>
+        secret <rsc> unstash <param>
+        secret <rsc> delete <param>
+        secret <rsc> show <param>
+        secret <rsc> check <param>"""
+        return ui_utils.manage_attr(context.get_command_name(), self.rsc_secret,
+                                    rsc, cmd, param, value)
+
+    @command.skill_level('administrator')
+    @command.wait
+    @command.completers(compl.resources, _attrcmds)
+    def do_meta(self, context, rsc, cmd, attr, value=None):
+        """usage:
+        meta <rsc> set <attr> <value>
+        meta <rsc> delete <attr>
+        meta <rsc> show <attr>"""
+        return ui_utils.manage_attr(context.get_command_name(), self.rsc_meta,
+                                    rsc, cmd, attr, value)
+
+    @command.skill_level('administrator')
+    @command.wait
+    @command.completers(compl.resources, _attrcmds)
+    def do_utilization(self, context, rsc, cmd, attr, value=None):
+        """usage:
+        utilization <rsc> set <attr> <value>
+        utilization <rsc> delete <attr>
+        utilization <rsc> show <attr>"""
+        return ui_utils.manage_attr(context.get_command_name(), self.rsc_utilization,
+                                    rsc, cmd, attr, value)
+
+    @command.completers(compl.nodes)
+    def do_refresh(self, context, *args):
+        'usage: refresh [<node>]'
+        if len(args) == 1:
+            if not utils.is_name_sane(args[0]):
+                return False
+            return utils.ext_cmd(self.rsc_refresh_node % args[0]) == 0
+        else:
+            return utils.ext_cmd(self.rsc_refresh) == 0
+
+    @command.wait
+    @command.completers(compl.nodes)
+    def do_reprobe(self, context, *args):
+        'usage: reprobe [<node>]'
+        if len(args) == 1:
+            if not utils.is_name_sane(args[0]):
+                return False
+            return utils.ext_cmd(self.rsc_reprobe_node % args[0]) == 0
+        else:
+            return utils.ext_cmd(self.rsc_reprobe) == 0
+
+    @command.wait
+    @command.completers(compl.resources, compl.choice(['on', 'off', 'true', 'false']))
+    def do_maintenance(self, context, resource, on_off='true'):
+        'usage: maintenance <resource> [on|off|true|false]'
+        on_off = on_off.lower()
+        if on_off not in ('on', 'true', 'off', 'false'):
+            context.fatal_error("Expected <resource> [on|off|true|false]")
+        elif on_off in ('on', 'true'):
+            on_off = 'true'
+        else:
+            on_off = 'false'
+        return utils.ext_cmd(self.rsc_maintenance % (resource, on_off)) == 0
+
+    def _get_trace_rsc(self, rsc_id):
+        if not cib_factory.refresh():
+            return None
+        rsc = cib_factory.find_object(rsc_id)
+        if not rsc:
+            common_err("resource %s does not exist" % rsc_id)
+            return None
+        if rsc.obj_type != "primitive":
+            common_err("element %s is not a primitive resource" % rsc_id)
+            return None
+        return rsc
+
+    def _add_trace_op(self, rsc, op, interval):
+        from lxml import etree
+        n = etree.Element('op')
+        n.set('name', op)
+        n.set('interval', interval)
+        n.set(constants.trace_ra_attr, '1')
+        return rsc.add_operation(n)
+
+    def _trace_resource(self, context, rsc_id, rsc):
+        op_nodes = rsc.node.xpath('.//op')
+
+        def trace(name):
+            for o in op_nodes:
+                if o.get('name') == name:
+                    return
+            if not self._add_trace_op(rsc, name, '0'):
+                context.fatal_error("Failed to add trace for %s:%s" % (rsc_id, name))
+        trace('start')
+        trace('stop')
+        if xmlutil.is_ms(rsc.node):
+            trace('promote')
+            trace('demote')
+        for op_node in op_nodes:
+            rsc.set_op_attr(op_node, constants.trace_ra_attr, "1")
+
+    def _trace_op(self, context, rsc_id, rsc, op):
+        op_nodes = rsc.node.xpath('.//op[@name="%s"]' % (op))
+        if not op_nodes:
+            if op == 'monitor':
+                context.fatal_error("No monitor operation configured for %s" % (rsc_id))
+            if not self._add_trace_op(rsc, op, '0'):
+                context.fatal_error("Failed to add trace for %s:%s" % (rsc_id, op))
+        for op_node in op_nodes:
+            rsc.set_op_attr(op_node, constants.trace_ra_attr, "1")
+
+    def _trace_op_interval(self, context, rsc_id, rsc, op, interval):
+        op_node = xmlutil.find_operation(rsc.node, op, interval)
+        if op_node is None and utils.crm_msec(interval) != 0:
+            context.fatal_error("Operation %s with interval %s not found in %s" % (op, interval, rsc_id))
+        if op_node is None:
+            if not self._add_trace_op(rsc, op, interval):
+                context.fatal_error("Failed to add trace for %s:%s" % (rsc_id, op))
+        else:
+            rsc.set_op_attr(op_node, constants.trace_ra_attr, "1")
+
+    @command.completers(compl.primitives, _raoperations)
+    def do_trace(self, context, rsc_id, op=None, interval=None):
+        'usage: trace <rsc> [<op>] [<interval>]'
+        rsc = self._get_trace_rsc(rsc_id)
+        if not rsc:
+            return False
+        if op == "probe":
+            op = "monitor"
+            if interval is None:
+                interval = "0"
+        if op is None:
+            self._trace_resource(context, rsc_id, rsc)
+        elif interval is None:
+            self._trace_op(context, rsc_id, rsc, op)
+        else:
+            self._trace_op_interval(context, rsc_id, rsc, op, interval)
+        if not cib_factory.commit():
+            return False
+        if op is not None:
+            common_info("Trace for %s:%s is written to %s/trace_ra/" %
+                        (rsc_id, op, config.path.heartbeat_dir))
+        else:
+            common_info("Trace for %s is written to %s/trace_ra/" %
+                        (rsc_id, config.path.heartbeat_dir))
+        if op is not None and op != "monitor":
+            common_info("Trace set, restart %s to trace the %s operation" % (rsc_id, op))
+        else:
+            common_info("Trace set, restart %s to trace non-monitor operations" % (rsc_id))
+        return True
+
+    def _remove_trace(self, rsc, op_node):
+        from lxml import etree
+        common_debug("op_node: %s" % (etree.tostring(op_node)))
+        op_node = rsc.del_op_attr(op_node, constants.trace_ra_attr)
+        if rsc.is_dummy_operation(op_node):
+            rsc.del_operation(op_node)
+
+    @command.completers(compl.primitives, _raoperations)
+    def do_untrace(self, context, rsc_id, op=None, interval=None):
+        'usage: untrace <rsc> [<op>] [<interval>]'
+        rsc = self._get_trace_rsc(rsc_id)
+        if not rsc:
+            return False
+        if op == "probe":
+            op = "monitor"
+        if op is None:
+            n = 0
+            for tn in rsc.node.xpath('.//*[@%s]' % (constants.trace_ra_attr)):
+                self._remove_trace(rsc, tn)
+                n += 1
+            for tn in rsc.node.xpath('.//*[@name="%s"]' % (constants.trace_ra_attr)):
+                if tn.getparent().getparent().tag == 'op':
+                    self._remove_trace(rsc, tn.getparent().getparent())
+                    n += 1
+        else:
+            op_node = xmlutil.find_operation(rsc.node, op, interval=interval)
+            if op_node is None:
+                common_err("operation %s does not exist in %s" % (op, rsc.obj_id))
+                return False
+            self._remove_trace(rsc, op_node)
+        return cib_factory.commit()
diff --git a/modules/ui_root.py b/modules/ui_root.py
new file mode 100644
index 0000000..ca57480
--- /dev/null
+++ b/modules/ui_root.py
@@ -0,0 +1,184 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+# Revised UI structure for crmsh
+#
+# Goals:
+#
+# - Modularity
+# - Reduced global state
+# - Separate static hierarchy from current context
+# - Fix completion
+# - Implement bash completion
+# - Retain all previous functionality
+# - Have per-level pre-requirements:
+#   def requires(self): <- raise error if prereqs are not met
+#   This is so that crmsh can be installed with minimal prereqs,
+#   and use cluster sublevel to install all requirements
+
+import command
+import cmd_status
+import ui_cib
+import ui_cluster
+import ui_corosync
+import ui_resource
+import ui_configure
+import ui_history
+import ui_ra
+import ui_site
+import ui_node
+import ui_report
+import ui_options
+import ui_script
+
+
+class Root(command.UI):
+    """
+    Root of the UI hierarchy.
+    """
+
+    # name is the user-visible name of this CLI level.
+    name = 'root'
+
+    @command.level(ui_cluster.Cluster)
+    @command.help('''Cluster setup and management
+Commands at this level enable low-level cluster configuration
+management with HA awareness.
+''')
+    def do_cluster(self):
+        pass
+
+    @command.level(ui_corosync.Corosync)
+    @command.help('''Corosync configuration management
+Corosync is the underlying messaging layer for most HA clusters.
+This level provides commands for editing and managing the corosync
+configuration.
+''')
+    def do_corosync(self):
+        pass
+
+    @command.level(ui_script.Script)
+    @command.help('''Cluster scripts
+Cluster scripts can perform cluster-wide configuration,
+validation and management. See the `list` command for
+an overview of available scripts.
+''')
+    def do_script(self):
+        pass
+
+    @command.level(ui_cib.CibShadow)
+    @command.help('''manage shadow CIBs
+A shadow CIB is a regular cluster configuration which is kept in
+a file. The CRM and the CRM tools may manage a shadow CIB in the
+same way as the live CIB (i.e. the current cluster configuration).
+A shadow CIB may be applied to the cluster in one step.
+''')
+    def do_cib(self):
+        pass
+
+    @command.level(ui_resource.RscMgmt)
+    @command.help('''resources management
+Everything related to resources management is available at this
+level. Most commands are implemented using the crm_resource(8)
+program.
+''')
+    def do_resource(self):
+        pass
+
+    @command.level(ui_configure.CibConfig)
+    @command.help('''CRM cluster configuration
+The configuration level.
+
+Note that you can change the working CIB at the cib level. It is
+advisable to configure shadow CIBs and then commit them to the
+cluster.
+''')
+    def do_configure(self):
+        pass
+
+    @command.level(ui_node.NodeMgmt)
+    @command.help('''nodes management
+A few node related tasks such as node standby are implemented
+here.
+''')
+    def do_node(self):
+        pass
+
+    @command.level(ui_options.CliOptions)
+    @command.help('''user preferences
+Several user preferences are available. Note that it is possible
+to save the preferences to a startup file.
+''')
+    def do_options(self):
+        pass
+
+    @command.level(ui_history.History)
+    @command.help('''CRM cluster history
+The history level.
+
+Examine Pacemaker's history: node and resource events, logs.
+''')
+    def do_history(self):
+        pass
+
+    @command.level(ui_site.Site)
+    @command.help('''Geo-cluster support
+The site level.
+
+Geo-cluster related management.
+''')
+    def do_site(self):
+        pass
+
+    @command.level(ui_ra.RA)
+    @command.help('''resource agents information center
+This level contains commands which show various information about
+the installed resource agents. It is available both at the top
+level and at the `configure` level.
+''')
+    def do_ra(self):
+        pass
+
+    @command.help('''Utility to collect logs and other information
+`report` is a utility to collect all information (logs,
+configuration files, system information, etc) relevant to
+crmsh over the given period of time.
+''')
+    def do_report(self, context, *args):
+        return ui_report.create_report(context, args)
+
+    @command.help('''show cluster status
+Show cluster status. The status is displayed by `crm_mon`. Supply
+additional arguments for more information or different format.
+See `crm_mon(8)` for more details.
+
+Usage:
+...............
+status [<option> ...]
+
+option :: bynode | inactive | ops | timing | failcounts
+...............
+''')
+    def do_status(self, context, *args):
+        return cmd_status.cmd_status(args)
+
+# this will initialize _children for all levels under the root
+Root.init_ui()
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/ui_script.py b/modules/ui_script.py
new file mode 100644
index 0000000..334ab93
--- /dev/null
+++ b/modules/ui_script.py
@@ -0,0 +1,68 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import command
+import scripts
+
+from msg import err_buf
+
+
+class Script(command.UI):
+    '''
+    Cluster scripts can perform cluster-wide configuration,
+    validation and management. See the `list` command for
+    an overview of available scripts.
+
+    The script UI is a thin veneer over the scripts
+    backend module.
+    '''
+    name = "script"
+
+    def do_list(self, context):
+        '''
+        List available scripts.
+        '''
+        for name in scripts.list_scripts():
+            main = scripts.load_script(name)
+            print "%-16s %s" % (name, main.get('name', ''))
+
+    def do_verify(self, context, name):
+        '''
+        Verify the given script.
+        '''
+        if scripts.verify(name):
+            err_buf.ok(name)
+
+    def do_describe(self, context, name):
+        '''
+        Describe the given script.
+        '''
+        return scripts.describe(name)
+
+    def do_steps(self, context, name):
+        '''
+        Print names of steps in script
+        '''
+        main = scripts.load_script(name)
+        for step in main['steps']:
+            print step['name']
+
+    def do_run(self, context, name, *args):
+        '''
+        Run the given script.
+        '''
+        return scripts.run(name, args)
diff --git a/modules/ui_site.py b/modules/ui_site.py
new file mode 100644
index 0000000..c45bf5c
--- /dev/null
+++ b/modules/ui_site.py
@@ -0,0 +1,92 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import time
+import command
+import completers as compl
+import config
+import utils
+from msg import no_prog_err
+
+_ticket_commands = {
+    'grant': "%s -t '%s' -g",
+    'revoke': "%s -t '%s' -r",
+    'delete': "%s -t '%s' -D granted",
+    'standby': "%s -t '%s' -s",
+    'activate': "%s -t '%s' -a",
+    'show': "%s -t '%s' -G granted",
+    'time': "%s -t '%s' -G last-granted",
+}
+
+
+def _show(context, ticket, val):
+    "Display status of ticket"
+    if val == "false":
+        print "ticket %s is revoked" % ticket
+    elif val == "true":
+        print "ticket %s is granted" % ticket
+    else:
+        context.fatal_error("unexpected value for ticket %s: %s" % (ticket, val))
+
+
+def _time(context, ticket, val):
+    "Display grant time for ticket"
+    if not utils.is_int(val):
+        context.fatal_error("unexpected value for ticket %s: %s" % (ticket, val))
+    if val == "-1":
+        context.fatal_error("%s: no such ticket" % ticket)
+    print "ticket %s last time granted on %s" % (ticket, time.ctime(int(val)))
+
+
+class Site(command.UI):
+    '''
+    The site class
+    '''
+    name = "site"
+
+    def requires(self):
+        if not utils.is_program('crm_ticket'):
+            no_prog_err('crm_ticket')
+            return False
+        return True
+
+    @command.skill_level('administrator')
+    @command.completers(compl.choice(_ticket_commands.keys()))
+    def do_ticket(self, context, subcmd, ticket):
+        "usage: ticket {grant|revoke|standby|activate|show|time|delete} <ticket>"
+
+        base_cmd = "crm_ticket"
+        if config.core.force:
+            base_cmd += " --force"
+
+        attr_cmd = _ticket_commands.get(subcmd)
+        if not attr_cmd:
+            context.fatal_error('Expected one of %s' % '|'.join(_ticket_commands.keys()))
+        if not utils.is_name_sane(ticket):
+            return False
+        if subcmd not in ("show", "time"):
+            return utils.ext_cmd(attr_cmd % (base_cmd, ticket)) == 0
+        rc, l = utils.stdout2list(attr_cmd % (base_cmd, ticket))
+        try:
+            val = l[0]
+        except IndexError:
+            context.fatal_error("apparently nothing to show for ticket %s" % ticket)
+        if subcmd == "show":
+            _show(context, ticket, val)
+        else:  # time
+            _time(context, ticket, val)
diff --git a/modules/ui_template.py b/modules/ui_template.py
new file mode 100644
index 0000000..a219b16
--- /dev/null
+++ b/modules/ui_template.py
@@ -0,0 +1,371 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import re
+import shlex
+import command
+import completers as compl
+import utils
+import config
+import userdir
+import options
+from template import LoadTemplate
+from cliformat import cli_format
+from cibconfig import mkset_obj, cib_factory
+from msg import common_err, common_warn
+from msg import syntax_err, err_buf
+
+
+def check_transition(inp, state, possible_l):
+    if state not in possible_l:
+        common_err("input (%s) in wrong state %s" % (inp, state))
+        return False
+    return True
+
+
+def _unique_config_name(tmpl):
+    n = 0
+    while n < 99:
+        c = "%s-%d" % (tmpl, n)
+        if not os.path.isfile("%s/%s" % (userdir.CRMCONF_DIR, c)):
+            return c
+        n += 1
+    raise ValueError("Failed to generate unique configuration name")
+
+
+class Template(command.UI):
+    '''
+    Configuration templates.
+    '''
+    name = "template"
+
+    def __init__(self):
+        command.UI.__init__(self)
+        self.curr_conf = ''
+        self.init_dir()
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.null, compl.call(utils.listtemplates))
+    def do_new(self, context, name, *args):
+        "usage: new <config> <template> [<template> ...] [params name=value ...]"
+        if not utils.is_filename_sane(name):
+            return False
+        if os.path.isfile("%s/%s" % (userdir.CRMCONF_DIR, name)):
+            common_err("config %s exists; delete it first" % name)
+            return False
+        lt = LoadTemplate(name)
+        rc = True
+        mode = 0
+        params = {"id": name}
+        loaded_template = False
+        for s in args:
+            if mode == 0 and s == "params":
+                mode = 1
+            elif mode == 1:
+                a = s.split('=')
+                if len(a) != 2:
+                    syntax_err(args, context='new')
+                    rc = False
+                else:
+                    params[a[0]] = a[1]
+            elif lt.load_template(s):
+                loaded_template = True
+            else:
+                rc = False
+        if not loaded_template:
+            if name not in utils.listtemplates():
+                common_err("Expected template argument")
+                return False
+            tmpl = name
+            name = _unique_config_name(tmpl)
+            lt = LoadTemplate(name)
+            lt.load_template(tmpl)
+        if rc:
+            lt.post_process(params)
+        if not rc or not lt.write_config(name):
+            return False
+        self.curr_conf = name
+
+    @command.skill_level('administrator')
+    @command.completers(compl.call(utils.listconfigs))
+    def do_delete(self, context, name, force=''):
+        "usage: delete <config> [force]"
+        if force:
+            if force != "force" and force != "--force":
+                syntax_err((context.get_command_name(), force), context='delete')
+                return False
+        if not self.config_exists(name):
+            return False
+        if name == self.curr_conf:
+            if not force and not config.core.force and \
+                    not utils.ask("Do you really want to remove config %s which is in use?" %
+                                  self.curr_conf):
+                return False
+            else:
+                self.curr_conf = ''
+        os.remove("%s/%s" % (userdir.CRMCONF_DIR, name))
+
+    @command.skill_level('administrator')
+    @command.completers(compl.call(utils.listconfigs))
+    def do_load(self, context, name=''):
+        "usage: load [<config>]"
+        if not name:
+            self.curr_conf = ''
+            return True
+        if not self.config_exists(name):
+            return False
+        self.curr_conf = name
+
+    @command.skill_level('administrator')
+    @command.completers(compl.call(utils.listconfigs))
+    def do_edit(self, context, name=''):
+        "usage: edit [<config>]"
+        if not name and not self.curr_conf:
+            common_err("please load a config first")
+            return False
+        if name:
+            if not self.config_exists(name):
+                return False
+            utils.edit_file("%s/%s" % (userdir.CRMCONF_DIR, name))
+        else:
+            utils.edit_file("%s/%s" % (userdir.CRMCONF_DIR, self.curr_conf))
+
+    @command.completers(compl.call(utils.listconfigs))
+    def do_show(self, context, name=''):
+        "usage: show [<config>]"
+        if not name and not self.curr_conf:
+            common_err("please load a config first")
+            return False
+        if name:
+            if not self.config_exists(name):
+                return False
+            print self.process(name)
+        else:
+            print self.process()
+
+    @command.skill_level('administrator')
+    @command.completers(compl.join(compl.call(utils.listconfigs),
+                                   compl.choice(['replace', 'update'])),
+                        compl.call(utils.listconfigs))
+    def do_apply(self, context, *args):
+        "usage: apply [<method>] [<config>]"
+        method = "replace"
+        name = ''
+        if len(args) > 0:
+            i = 0
+            if args[0] in ("replace", "update"):
+                method = args[0]
+                i += 1
+            if len(args) > i:
+                name = args[i]
+        if not name and not self.curr_conf:
+            common_err("please load a config first")
+            return False
+        if name:
+            if not self.config_exists(name):
+                return False
+            s = self.process(name)
+        else:
+            s = self.process()
+        if not s:
+            return False
+        tmp = utils.str2tmp(s)
+        if not tmp:
+            return False
+        if method == "replace":
+            if options.interactive and cib_factory.has_cib_changed():
+                if not utils.ask("This operation will erase all changes. Do you want to proceed?"):
+                    return False
+            cib_factory.erase()
+        set_obj = mkset_obj()
+        rc = set_obj.import_file(method, tmp)
+        try:
+            os.unlink(tmp)
+        except:
+            pass
+        return rc
+
+    @command.completers(compl.choice(['templates']))
+    def do_list(self, context, templates=''):
+        "usage: list [templates]"
+        if templates == "templates":
+            utils.multicolumn(utils.listtemplates())
+        else:
+            utils.multicolumn(utils.listconfigs())
+
+    def init_dir(self):
+        '''Create the conf directory, link to templates'''
+        if not os.path.isdir(userdir.CRMCONF_DIR):
+            try:
+                os.makedirs(userdir.CRMCONF_DIR)
+            except os.error, msg:
+                common_err("makedirs: %s" % msg)
+
+    def get_depends(self, tmpl):
+        '''return a list of required templates'''
+        # Not used. May need it later.
+        try:
+            templatepath = os.path.join(config.path.sharedir, 'templates', tmpl)
+            tf = open(templatepath, "r")
+        except IOError, msg:
+            common_err("open: %s" % msg)
+            return
+        l = []
+        for s in tf:
+            a = s.split()
+            if len(a) >= 2 and a[0] == '%depends_on':
+                l += a[1:]
+        tf.close()
+        return l
+
+    def config_exists(self, name):
+        if not utils.is_filename_sane(name):
+            return False
+        if not os.path.isfile("%s/%s" % (userdir.CRMCONF_DIR, name)):
+            common_err("%s: no such config" % name)
+            return False
+        return True
+
+    def replace_params(self, s, user_data):
+        change = False
+        for i in range(len(s)):
+            word = s[i]
+            for p in user_data:
+                # is parameter in the word?
+                pos = word.find('%' + p)
+                if pos < 0:
+                    continue
+                endpos = pos + len('%' + p)
+                # and it isn't part of another word?
+                if re.match("[A-Za-z0-9]", word[endpos:endpos+1]):
+                    continue
+                # if the value contains a space or
+                # it is a value of an attribute
+                # put quotes around it
+                if user_data[p].find(' ') >= 0 or word[pos-1:pos] == '=':
+                    v = '"' + user_data[p] + '"'
+                else:
+                    v = user_data[p]
+                word = word.replace('%' + p, v)
+                change = True  # we did replace something
+            if change:
+                s[i] = word
+        if 'opt' in s:
+            if not change:
+                s = []
+            else:
+                s.remove('opt')
+        return s
+
+    def generate(self, l, user_data):
+        '''replace parameters (user_data) and generate output
+        '''
+        l2 = []
+        for piece in l:
+            piece2 = []
+            for s in piece:
+                s = self.replace_params(s, user_data)
+                if s:
+                    piece2.append(' '.join(s))
+            if piece2:
+                l2.append(cli_format(piece2, break_lines=True))
+        return '\n'.join(l2)
+
+    def process(self, config=''):
+        '''Create a cli configuration from the current config'''
+        try:
+            f = open("%s/%s" % (userdir.CRMCONF_DIR, config or self.curr_conf), 'r')
+        except IOError, msg:
+            common_err("open: %s" % msg)
+            return ''
+        l = []
+        piece = []
+        user_data = {}
+        # states
+        START = 0
+        PFX = 1
+        DATA = 2
+        GENERATE = 3
+        state = START
+        err_buf.start_tmp_lineno()
+        rc = True
+        for inp in f:
+            err_buf.incr_lineno()
+            if inp.startswith('#'):
+                continue
+            if type(inp) == type(u''):
+                inp = inp.encode('ascii')
+            inp = inp.strip()
+            try:
+                s = shlex.split(inp)
+            except ValueError, msg:
+                common_err(msg)
+                continue
+            while '\n' in s:
+                s.remove('\n')
+            if not s:
+                if state == GENERATE and piece:
+                    l.append(piece)
+                    piece = []
+            elif s[0] in ("%name", "%depends_on", "%suggests"):
+                continue
+            elif s[0] == "%pfx":
+                if check_transition(inp, state, (START, DATA)) and len(s) == 2:
+                    pfx = s[1]
+                    state = PFX
+            elif s[0] == "%required":
+                if check_transition(inp, state, (PFX,)):
+                    state = DATA
+                    data_reqd = True
+            elif s[0] == "%optional":
+                if check_transition(inp, state, (PFX, DATA)):
+                    state = DATA
+                    data_reqd = False
+            elif s[0] == "%%":
+                if state != DATA:
+                    common_warn("user data in wrong state %s" % state)
+                if len(s) < 2:
+                    common_warn("parameter name missing")
+                elif len(s) == 2:
+                    if data_reqd:
+                        common_err("required parameter %s not set" % s[1])
+                        rc = False
+                elif len(s) == 3:
+                    user_data["%s:%s" % (pfx, s[1])] = s[2]
+                else:
+                    common_err("%s: syntax error" % inp)
+            elif s[0] == "%generate":
+                if check_transition(inp, state, (DATA,)):
+                    state = GENERATE
+                    piece = []
+            elif state == GENERATE:
+                if s:
+                    piece.append(s)
+            else:
+                common_err("<%s> unexpected" % inp)
+        if piece:
+            l.append(piece)
+        err_buf.stop_tmp_lineno()
+        f.close()
+        if not rc:
+            return ''
+        return self.generate(l, user_data)
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/ui_utils.py b/modules/ui_utils.py
new file mode 100644
index 0000000..16238cc
--- /dev/null
+++ b/modules/ui_utils.py
@@ -0,0 +1,174 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import re
+import inspect
+from msg import bad_usage, common_err
+import utils
+
+
+def _get_attr_cmd(attr_ext_commands, subcmd):
+    attr_ext_commands
+    try:
+        attr_cmd = attr_ext_commands[subcmd]
+        if attr_cmd:
+            return attr_cmd
+    except KeyError, msg:
+        raise ValueError(msg)
+    raise ValueError("Bad attr_cmd " + repr(attr_ext_commands))
+
+
+def _dispatch_attr_cmd(cmd, attr_cmd, rsc, subcmd, attr, value):
+    def sanity_check(arg):
+        if not utils.is_name_sane(arg):
+            raise ValueError("Expected valid name, got '%s'" % (arg))
+    if subcmd == 'set':
+        if value is None:
+            raise ValueError("Missing value argument to set")
+        sanity_check(rsc)
+        sanity_check(attr)
+        sanity_check(value)
+        return utils.ext_cmd(attr_cmd % (rsc, attr, value)) == 0
+    elif subcmd in ('delete', 'show') or \
+            (cmd == "secret" and subcmd in ('stash', 'unstash', 'check')):
+        if value is not None:
+            raise ValueError("Too many arguments to %s" % (subcmd))
+        sanity_check(rsc)
+        sanity_check(attr)
+        return utils.ext_cmd(attr_cmd % (rsc, attr)) == 0
+    raise ValueError("Unknown command " + repr(subcmd))
+
+
+def manage_attr(cmd, attr_ext_commands, rsc, subcmd, attr, value):
+    '''
+    TODO: describe.
+    '''
+    try:
+        attr_cmd = _get_attr_cmd(attr_ext_commands, subcmd)
+        return _dispatch_attr_cmd(cmd, attr_cmd, rsc, subcmd, attr, value)
+    except ValueError, msg:
+        cmdline = [rsc, subcmd, attr]
+        if value is not None:
+            cmdline.append(value)
+        bad_usage(cmd, ' '.join(cmdline), msg)
+        return False
+
+
+def ptestlike(simfun, def_verb, cmd, args):
+    verbosity = def_verb  # default verbosity
+    nograph = False
+    scores = False
+    utilization = False
+    actions = False
+    for p in args:
+        if p == "nograph":
+            nograph = True
+        elif p == "scores":
+            scores = True
+        elif p == "utilization":
+            utilization = True
+        elif p == "actions":
+            actions = True
+        elif re.match("^vv*$", p):
+            verbosity = p
+        else:
+            bad_usage(cmd, ' '.join(args))
+            return False
+    return simfun(nograph, scores, utilization, actions, verbosity)
+
+
+def graph_args(args):
+    '''
+    Common parameters for two graph commands:
+        configure graph [<gtype> [<file> [<img_format>]]]
+        history graph <pe> [<gtype> [<file> [<img_format>]]]
+    '''
+    from crm_gv import gv_types
+    gtype, outf, ftype = None, None, None
+    try:
+        gtype = args[0]
+        if gtype not in gv_types:
+            common_err("graph type %s is not supported" % gtype)
+            return False, gtype, outf, ftype
+    except:
+        gtype = "dot"
+    try:
+        outf = args[1]
+        if not utils.is_path_sane(outf):
+            return False, gtype, outf, ftype
+    except:
+        outf = None
+    try:
+        ftype = args[2]
+    except:
+        ftype = gtype
+    return True, gtype, outf, ftype
+
+
+def pretty_arguments(f, nskip=0):
+    '''
+    Returns a prettified representation
+    of the command arguments
+    '''
+    specs = inspect.getargspec(f)
+    named_args = []
+    if specs.defaults is None:
+        named_args += specs.args
+    else:
+        named_args += specs.args[:-len(specs.defaults)]
+        named_args += [("[%s]" % a) for a in specs.args[-len(specs.defaults):]]
+    if specs.varargs:
+        named_args += ['[%s ...]' % (specs.varargs)]
+    if nskip:
+        named_args = named_args[nskip:]
+    return ' '.join(named_args)
+
+
+def validate_arguments(f, args, nskip=0):
+    '''
+    Compares the declared arguments of f to
+    the given arguments in args, and raises
+    ValueError if the arguments don't match.
+
+    nskip: When reporting an error, skip these
+    many initial arguments when counting.
+    For example, pass 1 to not count self on a
+    method.
+
+    Note: Does not support keyword arguments.
+    '''
+    specs = inspect.getargspec(f)
+    min_args = len(specs.args)
+    if specs.defaults is not None:
+        min_args -= len(specs.defaults)
+    max_args = len(specs.args)
+    if specs.varargs:
+        max_args = -1
+
+    def mknamed():
+        return pretty_arguments(f, nskip=nskip)
+
+    if min_args == max_args and len(args) != min_args:
+        raise ValueError("Expected (%s), takes exactly %d arguments (%d given)" %
+                         (mknamed(), min_args-nskip, len(args)-nskip))
+    elif len(args) < min_args:
+        raise ValueError("Expected (%s), takes at least %d arguments (%d given)" %
+                         (mknamed(), min_args-nskip, len(args)-nskip))
+    if max_args >= 0 and len(args) > max_args:
+        raise ValueError("Expected (%s), takes at most %d arguments (%d given)" %
+                         (mknamed(), max_args-nskip, len(args)-nskip))
diff --git a/modules/userdir.py b/modules/userdir.py
new file mode 100644
index 0000000..4bdd857
--- /dev/null
+++ b/modules/userdir.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import os
+
+
+def getuser():
+    "Returns the name of the current user"
+    import getpass
+    return getpass.getuser()
+
+
+def gethomedir(user=''):
+    return os.path.expanduser("~" + user)
+
+# see http://standards.freedesktop.org/basedir-spec
+CONFIG_HOME = os.path.join(os.path.expanduser("~/.config"), 'crm')
+CACHE_HOME = os.path.join(os.path.expanduser("~/.cache"), 'crm')
+try:
+    from xdg import BaseDirectory
+    CONFIG_HOME = os.path.join(BaseDirectory.xdg_config_home, 'crm')
+    CACHE_HOME = os.path.join(BaseDirectory.xdg_cache_home, 'crm')
+except:
+    pass
+
+# TODO: move to CONFIG_HOME
+HISTORY_FILE = os.path.expanduser("~/.crm_history")
+RC_FILE = os.path.expanduser("~/.crm.rc")
+CRMCONF_DIR = os.path.expanduser("~/.crmconf")
+
+GRAPHVIZ_USER_FILE = os.path.join(CONFIG_HOME, "graphviz")
+
+
+def mv_user_files():
+    '''
+    Called from main
+    '''
+    global HISTORY_FILE
+    global RC_FILE
+    global CRMCONF_DIR
+
+    def _xdg_file(name, xdg_name, chk_fun, directory):
+        from msg import common_warn, common_info, common_debug
+        if not name:
+            return name
+        if not os.path.isdir(directory):
+            os.makedirs(directory, 0700)
+        new = os.path.join(directory, xdg_name)
+        if directory == CONFIG_HOME and chk_fun(new) and chk_fun(name):
+            common_warn("both %s and %s exist, please cleanup" % (name, new))
+            return name
+        if chk_fun(name):
+            if directory == CONFIG_HOME:
+                common_info("moving %s to %s" % (name, new))
+            else:
+                common_debug("moving %s to %s" % (name, new))
+            os.rename(name, new)
+        return new
+
+    HISTORY_FILE = _xdg_file(HISTORY_FILE, "history", os.path.isfile, CACHE_HOME)
+    RC_FILE = _xdg_file(RC_FILE, "rc", os.path.isfile, CONFIG_HOME)
+    CRMCONF_DIR = _xdg_file(CRMCONF_DIR, "crmconf", os.path.isdir, CONFIG_HOME)
diff --git a/modules/utils.py b/modules/utils.py
new file mode 100644
index 0000000..de9721d
--- /dev/null
+++ b/modules/utils.py
@@ -0,0 +1,1357 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import os
+import sys
+from tempfile import mkstemp
+import subprocess
+import re
+import glob
+import time
+import datetime
+import shutil
+import bz2
+import config
+import userdir
+import constants
+import options
+import term
+from msg import common_warn, common_info, common_debug, common_err, err_buf
+
+
+getuser = userdir.getuser
+gethomedir = userdir.gethomedir
+
+
+def this_node():
+    'returns name of this node (hostname)'
+    return os.uname()[1]
+
+
+_cib_shadow = 'CIB_shadow'
+_cib_in_use = ''
+
+
+def set_cib_in_use(name):
+    os.putenv(_cib_shadow, name)
+    global _cib_in_use
+    _cib_in_use = name
+
+
+def clear_cib_in_use():
+    os.unsetenv(_cib_shadow)
+    global _cib_in_use
+    _cib_in_use = ''
+
+
+def get_cib_in_use():
+    return _cib_in_use
+
+
+def get_tempdir():
+    return os.getenv("TMPDIR") or "/tmp"
+
+
+def is_program(prog):
+    """Is this program available?"""
+    def isexec(filename):
+        return os.path.isfile(filename) and os.access(filename, os.X_OK)
+    for p in os.getenv("PATH").split(os.pathsep):
+        f = os.path.join(p, prog)
+        if isexec(f):
+            return f
+    return None
+
+
+def can_ask():
+    """
+    Is user-interactivity possible?
+    Checks if connected to a TTY.
+    """
+    return sys.stdin.isatty()
+
+
+def ask(msg):
+    """
+    Ask for user confirmation.
+    If core.force is true, always return true.
+    If not interactive and core.force is false, always return false.
+    """
+    if config.core.force:
+        common_info("%s [YES]" % (msg))
+        return True
+    if not can_ask():
+        return False
+
+    msg += ' '
+    if msg.endswith('? '):
+        msg = msg[:-2] + ' (y/n)? '
+
+    while True:
+        try:
+            ans = raw_input(msg)
+        except EOFError:
+            ans = 'n'
+        if ans:
+            ans = ans[0].lower()
+            if ans in 'yn':
+                return ans == 'y'
+
+# holds part of line before \ split
+# for a multi-line input
+_LINE_BUFFER = ''
+
+
+def get_line_buffer():
+    return _LINE_BUFFER
+
+
+def multi_input(prompt=''):
+    """
+    Get input from user
+    Allow multiple lines using a continuation character
+    """
+    global _LINE_BUFFER
+    line = []
+    _LINE_BUFFER = ''
+    while True:
+        try:
+            text = raw_input(prompt)
+        except EOFError:
+            return None
+        err_buf.incr_lineno()
+        if options.regression_tests:
+            print ".INP:", text
+            sys.stdout.flush()
+            sys.stderr.flush()
+        stripped = text.strip()
+        if stripped.endswith('\\'):
+            stripped = stripped.rstrip('\\')
+            line.append(stripped)
+            _LINE_BUFFER += stripped
+            if prompt:
+                prompt = '   > '
+        else:
+            line.append(stripped)
+            break
+    return ''.join(line)
+
+
+def verify_boolean(opt):
+    return opt.lower() in ("yes", "true", "on") or \
+        opt.lower() in ("no", "false", "off")
+
+
+def is_boolean_true(opt):
+    return opt.lower() in ("yes", "true", "on")
+
+
+def is_boolean_false(opt):
+    return opt.lower() in ("no", "false", "off")
+
+
+def get_boolean(opt, dflt=False):
+    if not opt:
+        return dflt
+    return is_boolean_true(opt)
+
+
+def canonical_boolean(opt):
+    return 'true' if is_boolean_true(opt) else 'false'
+
+
+def keyword_cmp(string1, string2):
+    return string1.lower() == string2.lower()
+
+
+class olist(list):
+    """
+    Implements the 'in' operator
+    in a case-insensitive manner,
+    allowing "if x in olist(...)"
+    """
+    def __init__(self, keys):
+        super(olist, self).__init__([k.lower() for k in keys])
+
+    def __contains__(self, key):
+        return super(olist, self).__contains__(key.lower())
+
+    def append(self, key):
+        super(olist, self).append(key.lower())
+
+
+def os_types_list(path):
+    l = []
+    for f in glob.glob(path):
+        if os.access(f, os.X_OK) and os.path.isfile(f):
+            a = f.split("/")
+            l.append(a[-1])
+    return l
+
+
+def listtemplates():
+    l = []
+    templates_dir = os.path.join(config.path.sharedir, 'templates')
+    for f in os.listdir(templates_dir):
+        if os.path.isfile("%s/%s" % (templates_dir, f)):
+            l.append(f)
+    return l
+
+
+def listconfigs():
+    l = []
+    for f in os.listdir(userdir.CRMCONF_DIR):
+        if os.path.isfile("%s/%s" % (userdir.CRMCONF_DIR, f)):
+            l.append(f)
+    return l
+
+
+def add_sudo(cmd):
+    if config.core.user:
+        return "sudo -E -u %s %s" % (config.core.user, cmd)
+    return cmd
+
+
+def pipe_string(cmd, s):
+    rc = -1  # command failed
+    cmd = add_sudo(cmd)
+    common_debug("piping string to %s" % cmd)
+    if options.regression_tests:
+        print ".EXT", cmd
+    p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE)
+    try:
+        p.communicate(s)
+        p.wait()
+        rc = p.returncode
+    except IOError, msg:
+        if "Broken pipe" not in msg:
+            common_err(msg)
+    return rc
+
+
+def filter_string(cmd, s, stderr_on=True):
+    rc = -1  # command failed
+    outp = ''
+    if stderr_on:
+        stderr = None
+    else:
+        stderr = subprocess.PIPE
+    cmd = add_sudo(cmd)
+    common_debug("pipe through %s" % cmd)
+    if options.regression_tests:
+        print ".EXT", cmd
+    p = subprocess.Popen(cmd,
+                         shell=True,
+                         stdin=subprocess.PIPE,
+                         stdout=subprocess.PIPE,
+                         stderr=stderr)
+    try:
+        outp = p.communicate(s)[0]
+        p.wait()
+        rc = p.returncode
+    except OSError, (errno, strerror):
+        if errno != os.errno.EPIPE:
+            common_err(strerror)
+        common_info("from: %s" % cmd)
+    except Exception, msg:
+        common_err(msg)
+        common_info("from: %s" % cmd)
+    return rc, outp
+
+
+def str2tmp(s, suffix=".pcmk"):
+    '''
+    Write the given string to a temporary file. Return the name
+    of the file.
+    '''
+    fd, tmp = mkstemp(suffix=suffix)
+    try:
+        f = os.fdopen(fd, "w")
+    except IOError, msg:
+        common_err(msg)
+        return
+    f.write(s)
+    if not s.endswith('\n'):
+        f.write("\n")
+    f.close()
+    return tmp
+
+
+def str2file(s, fname):
+    '''
+    Write a string to a file.
+    '''
+    try:
+        f = open(fname, "w")
+    except IOError, msg:
+        common_err(msg)
+        return False
+    f.write(s)
+    f.close()
+    return True
+
+
+def file2str(fname, noerr=True):
+    '''
+    Read a one line file into a string, strip whitespace around.
+    '''
+    try:
+        f = open(fname, "r")
+    except IOError, msg:
+        if not noerr:
+            common_err(msg)
+        return None
+    s = f.readline()
+    f.close()
+    return s.strip()
+
+
+def file2list(fname):
+    '''
+    Read a file into a list (newlines dropped).
+    '''
+    try:
+        f = open(fname, "r")
+    except IOError, msg:
+        common_err(msg)
+        return None
+    l = ''.join(f).split('\n')
+    f.close()
+    return l
+
+
+def safe_open_w(fname):
+    if fname == "-":
+        f = sys.stdout
+    else:
+        if not options.batch and os.access(fname, os.F_OK):
+            if not ask("File %s exists. Do you want to overwrite it?" % fname):
+                return None
+        try:
+            f = open(fname, "w")
+        except IOError, msg:
+            common_err(msg)
+            return None
+    return f
+
+
+def safe_close_w(f):
+    if f and f != sys.stdout:
+        f.close()
+
+
+def is_path_sane(name):
+    if re.search(r"['`#*?$\[\]]", name):
+        common_err("%s: bad path" % name)
+        return False
+    return True
+
+
+def is_filename_sane(name):
+    if re.search(r"['`/#*?$\[\]]", name):
+        common_err("%s: bad filename" % name)
+        return False
+    return True
+
+
+def is_name_sane(name):
+    if re.search("[']", name):
+        common_err("%s: bad name" % name)
+        return False
+    return True
+
+
+def is_value_sane(name):
+    if re.search("[']", name):
+        common_err("%s: bad value" % name)
+        return False
+    return True
+
+
+def show_dot_graph(dotfile, keep_file=False, desc="transition graph"):
+    cmd = "%s %s" % (config.core.dotty, dotfile)
+    if not keep_file:
+        cmd = "(%s; rm -f %s)" % (cmd, dotfile)
+    if options.regression_tests:
+        print ".EXT", cmd
+    subprocess.Popen(cmd, shell=True, bufsize=0,
+                     stdin=None, stdout=None, stderr=None, close_fds=True)
+    common_info("starting %s to show %s" % (config.core.dotty, desc))
+
+
+def ext_cmd(cmd, shell=True):
+    cmd = add_sudo(cmd)
+    if options.regression_tests:
+        print ".EXT", cmd
+    common_debug("invoke: %s" % cmd)
+    return subprocess.call(cmd, shell=shell)
+
+
+def ext_cmd_nosudo(cmd, shell=True):
+    if options.regression_tests:
+        print ".EXT", cmd
+    return subprocess.call(cmd, shell=shell)
+
+
+def rmdir_r(d):
+    if d and os.path.isdir(d):
+        shutil.rmtree(d)
+
+
+def nvpairs2dict(pairs):
+    '''
+    takes a list of string of form ['a=b', 'c=d']
+    and returns {'a':'b', 'c':'d'}
+    '''
+    data = []
+    for var in pairs:
+        if '=' in var:
+            data.append(var.split('=', 1))
+        else:
+            data.append([var, None])
+    return dict(data)
+
+
+def is_check_always():
+    '''
+    Even though the frequency may be set to always, it doesn't
+    make sense to do that with non-interactive sessions.
+    '''
+    return options.interactive and config.core.check_frequency == "always"
+
+
+def get_check_rc():
+    '''
+    If the check mode is set to strict, then on errors we
+    return 2 which is the code for error. Otherwise, we
+    pretend that errors are warnings.
+    '''
+    return config.core.check_mode == "strict" and 2 or 1
+
+
+_LOCKDIR = ".lockdir"
+_PIDF = "pid"
+
+
+def check_locker(dir):
+    if not os.path.isdir(os.path.join(dir, _LOCKDIR)):
+        return
+    s = file2str(os.path.join(dir, _LOCKDIR, _PIDF))
+    pid = convert2ints(s)
+    if not isinstance(pid, int):
+        common_warn("history: removing malformed lock")
+        rmdir_r(os.path.join(dir, _LOCKDIR))
+        return
+    try:
+        os.kill(pid, 0)
+    except OSError, (errno, strerror):
+        if errno == os.errno.ESRCH:
+            common_info("history: removing stale lock")
+            rmdir_r(os.path.join(dir, _LOCKDIR))
+        else:
+            common_err("%s: %s" % (_LOCKDIR, strerror))
+
+
+def acquire_lock(dir):
+    check_locker(dir)
+    while True:
+        try:
+            os.makedirs(os.path.join(dir, _LOCKDIR))
+            str2file("%d" % os.getpid(), os.path.join(dir, _LOCKDIR, _PIDF))
+            return True
+        except OSError, (errno, strerror):
+            if errno != os.errno.EEXIST:
+                common_err(strerror)
+                return False
+            time.sleep(0.1)
+            continue
+        else:
+            return False
+
+
+def release_lock(dir):
+    rmdir_r(os.path.join(dir, _LOCKDIR))
+
+
+def pipe_cmd_nosudo(cmd):
+    if options.regression_tests:
+        print ".EXT", cmd
+    proc = subprocess.Popen(cmd,
+                            shell=True,
+                            stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE)
+    (outp, err_outp) = proc.communicate()
+    proc.wait()
+    rc = proc.returncode
+    if rc != 0:
+        print outp
+        print err_outp
+    return rc
+
+
+def get_stdout(cmd, input_s=None, stderr_on=True, shell=True):
+    '''
+    Run a cmd, return stdout output.
+    Optional input string "input_s".
+    stderr_on controls whether to show output which comes on stderr.
+    '''
+    if stderr_on:
+        stderr = None
+    else:
+        stderr = subprocess.PIPE
+    if options.regression_tests:
+        print ".EXT", cmd
+    proc = subprocess.Popen(cmd,
+                            shell=shell,
+                            stdin=subprocess.PIPE,
+                            stdout=subprocess.PIPE,
+                            stderr=stderr)
+    stdout_data, stderr_data = proc.communicate(input_s)
+    return proc.returncode, stdout_data.strip()
+
+
+def get_stdout_stderr(cmd, input_s=None, shell=True):
+    '''
+    Run a cmd, return (rc, stdout, stderr)
+    '''
+    if options.regression_tests:
+        print ".EXT", cmd
+    proc = subprocess.Popen(cmd,
+                            shell=shell,
+                            stdin=input_s and subprocess.PIPE or None,
+                            stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE)
+    stdout_data, stderr_data = proc.communicate(input_s)
+    return proc.returncode, stdout_data.strip(), stderr_data.strip()
+
+
+def stdout2list(cmd, stderr_on=True, shell=True):
+    '''
+    Run a cmd, fetch output, return it as a list of lines.
+    stderr_on controls whether to show output which comes on stderr.
+    '''
+    rc, s = get_stdout(add_sudo(cmd), stderr_on=stderr_on, shell=shell)
+    if not s:
+        return rc, []
+    else:
+        return rc, s.split('\n')
+
+
+def append_file(dest, src):
+    'Append src to dest'
+    try:
+        dest_f = open(dest, "a")
+    except IOError, msg:
+        common_err("open %s: %s" % (dest, msg))
+        return False
+    try:
+        f = open(src)
+    except IOError, msg:
+        common_err("open %s: %s" % (src, msg))
+        dest_f.close()
+        return False
+    dest_f.write(''.join(f))
+    f.close()
+    dest_f.close()
+    return True
+
+
+def get_dc():
+    cmd = "crmadmin -D"
+    rc, s = get_stdout(add_sudo(cmd))
+    if rc != 0:
+        return None
+    if not s.startswith("Designated"):
+        return None
+    return s.split()[-1]
+
+
+def wait4dc(what="", show_progress=True):
+    '''
+    Wait for the DC to get into the S_IDLE state. This should be
+    invoked only after a CIB modification which would exercise
+    the PE. Parameter "what" is whatever the caller wants to be
+    printed if showing progress.
+
+    It is assumed that the DC is already in a different state,
+    usually it should be either PENGINE or TRANSITION. This
+    assumption may not be true, but there's a high chance that it
+    is since crmd should be faster to move through states than
+    this shell.
+
+    Further, it may also be that crmd already calculated the new
+    graph, did transition, and went back to the idle state. This
+    may in particular be the case if the transition turned out to
+    be empty.
+
+    Tricky. Though in practice it shouldn't be an issue.
+
+    There's no timeout, as we expect the DC to eventually becomes
+    idle.
+    '''
+    dc = get_dc()
+    if not dc:
+        common_warn("can't find DC")
+        return False
+    cmd = "crm_attribute -Gq -t crm_config -n crmd-transition-delay 2> /dev/null"
+    delay = get_stdout(add_sudo(cmd))[1]
+    if delay:
+        delaymsec = crm_msec(delay)
+        if 0 < delaymsec:
+            common_info("The crmd-transition-delay is configured. Waiting %d msec before check DC status." % delaymsec)
+            time.sleep(delaymsec / 1000)
+    cmd = "crmadmin -S %s" % dc
+    cnt = 0
+    output_started = 0
+    init_sleep = 0.25
+    max_sleep = 1.00
+    sleep_time = init_sleep
+    while True:
+        rc, s = get_stdout(add_sudo(cmd))
+        if not s.startswith("Status"):
+            common_warn("%s unexpected output: %s (exit code: %d)" %
+                        (cmd, s, rc))
+            return False
+        try:
+            dc_status = s.split()[-2]
+        except:
+            common_warn("%s unexpected output: %s" % (cmd, s))
+            return False
+        if dc_status == "S_IDLE":
+            if output_started:
+                sys.stderr.write(" done\n")
+            return True
+        time.sleep(sleep_time)
+        if sleep_time < max_sleep:
+            sleep_time *= 2
+        if show_progress:
+            if not output_started:
+                output_started = 1
+                sys.stderr.write("waiting for %s to finish ." % what)
+            cnt += 1
+            if cnt % 5 == 0:
+                sys.stderr.write(".")
+
+
+def run_ptest(graph_s, nograph, scores, utilization, actions, verbosity):
+    '''
+    Pipe graph_s thru ptest(8). Show graph using dotty if requested.
+    '''
+    actions_filter = "grep LogActions: | grep -vw Leave"
+    ptest = "2>&1 %s -x -" % config.core.ptest
+    if re.search("simulate", ptest) and \
+            not re.search("-[RS]", ptest):
+        ptest = "%s -S" % ptest
+    if verbosity:
+        if actions:
+            verbosity = 'v' * max(3, len(verbosity))
+        ptest = "%s -%s" % (ptest, verbosity.upper())
+    if scores:
+        ptest = "%s -s" % ptest
+    if utilization:
+        ptest = "%s -U" % ptest
+    if config.core.dotty and not nograph:
+        fd, dotfile = mkstemp()
+        ptest = "%s -D %s" % (ptest, dotfile)
+    else:
+        dotfile = None
+    # ptest prints to stderr
+    if actions:
+        ptest = "%s | %s" % (ptest, actions_filter)
+    if options.regression_tests:
+        ptest = ">/dev/null %s" % ptest
+    common_debug("invoke: %s" % ptest)
+    rc, s = get_stdout(ptest, input_s=graph_s)
+    if rc != 0:
+        common_debug("%s exited with %d" % (ptest, rc))
+        if actions and rc == 1:
+            common_warn("No actions found.")
+        else:
+            common_warn("Simulation was unsuccessful (RC=%d)." % (rc))
+    if dotfile:
+        if os.path.getsize(dotfile) > 0:
+            show_dot_graph(dotfile)
+        else:
+            common_warn("ptest produced empty dot file")
+    else:
+        if not nograph:
+            common_info("install graphviz to see a transition graph")
+    if s:
+        page_string(s)
+    return True
+
+
+def is_id_valid(id):
+    """
+    Verify that the id follows the definition:
+    http://www.w3.org/TR/1999/REC-xml-names-19990114/#ns-qualnames
+    """
+    if not id:
+        return False
+    id_re = r"^[A-Za-z_][\w._-]*$"
+    return re.match(id_re, id)
+
+
+def check_range(a):
+    """
+    Verify that the integer range in list a is valid.
+    """
+    if len(a) != 2:
+        return False
+    if not isinstance(a[0], int) or not isinstance(a[1], int):
+        return False
+    return int(a[0]) <= int(a[1])
+
+
+def crm_msec(t):
+    '''
+    See lib/common/utils.c:crm_get_msec().
+    '''
+    convtab = {
+        'ms': (1, 1),
+        'msec': (1, 1),
+        'us': (1, 1000),
+        'usec': (1, 1000),
+        '': (1000, 1),
+        's': (1000, 1),
+        'sec': (1000, 1),
+        'm': (60*1000, 1),
+        'min': (60*1000, 1),
+        'h': (60*60*1000, 1),
+        'hr': (60*60*1000, 1),
+    }
+    if not t:
+        return -1
+    r = re.match(r"\s*(\d+)\s*([a-zA-Z]+)?", t)
+    if not r:
+        return -1
+    if not r.group(2):
+        q = ''
+    else:
+        q = r.group(2).lower()
+    try:
+        mult, div = convtab[q]
+    except KeyError:
+        return -1
+    return (int(r.group(1))*mult)/div
+
+
+def crm_time_cmp(a, b):
+    return crm_msec(a) - crm_msec(b)
+
+
+def shorttime(ts):
+    if isinstance(ts, datetime.datetime):
+        return ts.strftime("%X")
+    if ts is not None:
+        return time.strftime("%X", time.localtime(ts))
+    return time.strftime("%X", time.localtime(0))
+
+
+def shortdate(ts):
+    if isinstance(ts, datetime.datetime):
+        return ts.strftime("%F")
+    if ts is not None:
+        return time.strftime("%F", time.localtime(ts))
+    return time.strftime("%F", time.localtime(0))
+
+
+def sort_by_mtime(l):
+    'Sort a (small) list of files by time mod.'
+    l2 = [(os.stat(x).st_mtime, x) for x in l]
+    l2.sort()
+    return [x[1] for x in l2]
+
+
+def dirwalk(dir):
+    "walk a directory tree, using a generator"
+    # http://code.activestate.com/recipes/105873/
+    for f in os.listdir(dir):
+        fullpath = os.path.join(dir, f)
+        if os.path.isdir(fullpath) and not os.path.islink(fullpath):
+            for x in dirwalk(fullpath):  # recurse into subdir
+                yield x
+        else:
+            yield fullpath
+
+
+def file_find_by_name(dir, fname):
+    'Find a file within a tree matching fname.'
+    if not dir:
+        common_err("cannot dirwalk nothing!")
+        return None
+    if not fname:
+        common_err("file to find not provided")
+        return None
+    for f in dirwalk(dir):
+        if os.path.basename(f) == fname:
+            return f
+    return None
+
+
+def convert2ints(l):
+    """
+    Convert a list of strings (or a string) to a list of ints.
+    All strings must be ints, otherwise conversion fails and None
+    is returned!
+    """
+    try:
+        if isinstance(l, (tuple, list)):
+            return [int(x) for x in l]
+        else:  # it's a string then
+            return int(l)
+    except ValueError:
+        return None
+
+
+def is_int(s):
+    'Check if the string can be converted to an integer.'
+    try:
+        int(s)
+        return True
+    except ValueError:
+        return False
+
+
+def is_process(s):
+    cmd = "ps -e -o pid,command | grep -qs '%s'" % s
+    if options.regression_tests:
+        print ".EXT", cmd
+    proc = subprocess.Popen(cmd,
+                            shell=True,
+                            stdout=subprocess.PIPE)
+    proc.wait()
+    return proc.returncode == 0
+
+
+def cluster_stack():
+    if is_process("heartbeat:.[m]aster"):
+        return "heartbeat"
+    elif is_process("[a]isexec"):
+        return "openais"
+    elif os.path.exists("/etc/corosync/corosync.conf") or is_program('corosync-cfgtool'):
+        return "corosync"
+    return ""
+
+
+def edit_file(fname):
+    'Edit a file.'
+    if not fname:
+        return
+    if not config.core.editor:
+        return
+    return ext_cmd_nosudo("%s %s" % (config.core.editor, fname))
+
+
+def edit_file_ext(fname, template=''):
+    '''
+    Edit a file via a temporary file.
+    Raises IOError on any error.
+    '''
+    if not os.path.isfile(fname):
+        s = template
+    else:
+        s = open(fname).read()
+    filehash = hash(s)
+    tmpfile = str2tmp(s)
+    try:
+        try:
+            if edit_file(tmpfile) != 0:
+                return
+            s = open(tmpfile, 'r').read()
+            if hash(s) == filehash:  # file unchanged
+                return
+            f2 = open(fname, 'w')
+            f2.write(s)
+            f2.close()
+        finally:
+            os.unlink(tmpfile)
+    except OSError, e:
+        raise IOError(e)
+
+
+def need_pager(s, w, h):
+    from math import ceil
+    cnt = 0
+    for l in s.split('\n'):
+        # need to remove color codes
+        l = re.sub(r'\${\w+}', '', l)
+        cnt += int(ceil((len(l) + 0.5)/w))
+        if cnt >= h:
+            return True
+    return False
+
+
+def term_render(s):
+    'Render for TERM.'
+    try:
+        return term.render(s)
+    except:
+        return s
+
+
+def get_pager_cmd(*extra_opts):
+    'returns a commandline which calls the configured pager'
+    cmdline = [config.core.pager]
+    if os.path.basename(config.core.pager) == "less":
+        cmdline.append('-R')
+    cmdline.extend(extra_opts)
+    return ' '.join(cmdline)
+
+
+def page_string(s):
+    'Page string rendered for TERM.'
+    if not s:
+        return
+    w, h = get_winsize()
+    if not need_pager(s, w, h):
+        print term_render(s)
+    elif not config.core.pager or not can_ask() or options.batch:
+        print term_render(s)
+    else:
+        pipe_string(get_pager_cmd(), term_render(s))
+
+
+def page_file(filename):
+    'Open file in pager'
+    if not os.path.isfile(filename):
+        return
+    return ext_cmd_nosudo(get_pager_cmd(filename), shell=True)
+
+
+def get_winsize():
+    try:
+        import curses
+        curses.setupterm()
+        w = curses.tigetnum('cols')
+        h = curses.tigetnum('lines')
+    except:
+        try:
+            w = os.environ['COLS']
+            h = os.environ['LINES']
+        except KeyError:
+            w = 80
+            h = 25
+    return w, h
+
+
+def multicolumn(l):
+    '''
+    A ls-like representation of a list of strings.
+    A naive approach.
+    '''
+    min_gap = 2
+    w, _ = get_winsize()
+    max_len = 8
+    for s in l:
+        if len(s) > max_len:
+            max_len = len(s)
+    cols = w/(max_len + min_gap)  # approx.
+    if not cols:
+        cols = 1
+    col_len = w/cols
+    for i in range(len(l)/cols + 1):
+        s = ''
+        for j in range(i * cols, (i + 1) * cols):
+            if not j < len(l):
+                break
+            if not s:
+                s = "%-*s" % (col_len, l[j])
+            elif (j + 1) % cols == 0:
+                s = "%s%s" % (s, l[j])
+            else:
+                s = "%s%-*s" % (s, col_len, l[j])
+        if s:
+            print s
+
+
+def find_value(pl, name):
+    for n, v in pl:
+        if n == name:
+            return v
+    return None
+
+
+def cli_replace_attr(pl, name, new_val):
+    for i in range(len(pl)):
+        if pl[i][0] == name:
+            pl[i][1] = new_val
+            return
+
+
+def cli_append_attr(pl, name, val):
+    pl.append([name, val])
+
+
+def lines2cli(s):
+    '''
+    Convert a string into a list of lines. Replace continuation
+    characters. Strip white space, left and right. Drop empty lines.
+    '''
+    cl = []
+    l = s.split('\n')
+    cum = []
+    for p in l:
+        p = p.strip()
+        if p.endswith('\\'):
+            p = p.rstrip('\\')
+            cum.append(p)
+        else:
+            cum.append(p)
+            cl.append(''.join(cum).strip())
+            cum = []
+    if cum:  # in case s ends with backslash
+        cl.append(''.join(cum))
+    return [x for x in cl if x]
+
+
+def parse_time(t):
+    '''
+    Try to make sense of the user provided time spec.
+    Use dateutil if available, otherwise strptime.
+    Return the datetime value.
+    '''
+    try:
+        import dateutil.parser
+        dt = dateutil.parser.parse(t)
+    except ValueError, msg:
+        common_err("%s: %s" % (t, msg))
+        return None
+    except ImportError, msg:
+        try:
+            tm = time.strptime(t)
+            dt = datetime.datetime(*tm[0:7])
+        except ValueError, msg:
+            common_err("no dateutil, please provide times as printed by date(1)")
+            return None
+    return dt
+
+
+def save_graphviz_file(ini_f, attr_d):
+    '''
+    Save graphviz settings to an ini file, if it does not exist.
+    '''
+    if os.path.isfile(ini_f):
+        common_err("%s exists, please remove it first" % ini_f)
+        return False
+    try:
+        f = open(ini_f, "wb")
+    except IOError, msg:
+        common_err(msg)
+        return False
+    import ConfigParser
+    p = ConfigParser.SafeConfigParser()
+    for section, sect_d in attr_d.iteritems():
+        p.add_section(section)
+        for n, v in sect_d.iteritems():
+            p.set(section, n, v)
+    try:
+        p.write(f)
+    except IOError, msg:
+        common_err(msg)
+        return False
+    f.close()
+    common_info("graphviz attributes saved to %s" % ini_f)
+    return True
+
+
+def load_graphviz_file(ini_f):
+    '''
+    Load graphviz ini file, if it exists.
+    '''
+    if not os.path.isfile(ini_f):
+        return True, None
+    import ConfigParser
+    p = ConfigParser.SafeConfigParser()
+    try:
+        p.read(ini_f)
+    except Exception, msg:
+        common_err(msg)
+        return False, None
+    _graph_d = {}
+    for section in p.sections():
+        d = {}
+        for n, v in p.items(section):
+            d[n] = v
+        _graph_d[section] = d
+    return True, _graph_d
+
+
+def get_pcmk_version(dflt):
+    version = dflt
+
+    crmd = is_program('crmd')
+    if crmd:
+        cmd = crmd
+    else:
+        return version
+
+    try:
+        rc, s, err = get_stdout_stderr("%s version" % (cmd))
+        if rc != 0:
+            common_err("%s exited with %d [err: %s][out: %s]" % (cmd, rc, err, s))
+        else:
+            common_debug("pacemaker version: [err: %s][out: %s]" % (err, s))
+            if err.startswith("CRM Version:"):
+                version = s.split()[0]
+            else:
+                version = s.split()[2]
+            common_debug("found pacemaker version: %s" % version)
+    except Exception, msg:
+        common_warn("could not get the pacemaker version, bad installation?")
+        common_warn(msg)
+    return version
+
+
+def get_cib_property(cib_f, attr, dflt):
+    """A poor man's get attribute procedure.
+    We don't want heavy parsing, this needs to be relatively
+    fast.
+    """
+    open_t = "<cluster_property_set"
+    close_t = "</cluster_property_set"
+    attr_s = 'name="%s"' % attr
+    ver_patt = re.compile('value="([^"]+)"')
+    ver = dflt  # return some version in any case
+    try:
+        f = open(cib_f, "r")
+    except IOError, msg:
+        common_err(msg)
+        return ver
+    state = 0
+    for s in f:
+        if state == 0:
+            if open_t in s:
+                state += 1
+        elif state == 1:
+            if close_t in s:
+                break
+            if attr_s in s:
+                r = ver_patt.search(s)
+                if r:
+                    ver = r.group(1)
+                break
+    f.close()
+    return ver
+
+
+def get_cib_attributes(cib_f, tag, attr_l, dflt_l):
+    """A poor man's get attribute procedure.
+    We don't want heavy parsing, this needs to be relatively
+    fast.
+    """
+    open_t = "<%s " % tag
+    val_patt_l = [re.compile('%s="([^"]+)"' % x) for x in attr_l]
+    val_l = []
+    try:
+        f = open(cib_f, "r")
+    except IOError, msg:
+        common_err(msg)
+        return dflt_l
+    if os.path.splitext(cib_f)[-1] == '.bz2':
+        cib_s = bz2.decompress(''.join(f))
+    else:
+        cib_s = ''.join(f)
+    for s in cib_s.split('\n'):
+        if s.startswith(open_t):
+            i = 0
+            for patt in val_patt_l:
+                r = patt.search(s)
+                val_l.append(r and r.group(1) or dflt_l[i])
+                i += 1
+            break
+    f.close()
+    return val_l
+
+
+def is_min_pcmk_ver(min_ver, cib_f=None):
+    if not constants.pcmk_version:
+        if cib_f:
+            constants.pcmk_version = get_cib_property(cib_f, "dc-version", "1.1.11")
+            common_debug("found pacemaker version: %s in cib: %s" %
+                         (constants.pcmk_version, cib_f))
+        else:
+            constants.pcmk_version = get_pcmk_version("1.1.11")
+    from distutils.version import LooseVersion
+    return LooseVersion(constants.pcmk_version) >= LooseVersion(min_ver)
+
+
+def is_pcmk_118(cib_f=None):
+    return is_min_pcmk_ver("1.1.8", cib_f=cib_f)
+
+
+_cibadmin_features_cached = None
+
+def cibadmin_features():
+    '''
+    # usage example:
+    if 'corosync-plugin' in cibadmin_features()
+    '''
+    global _cibadmin_features_cached
+    if _cibadmin_features_cached is None:
+        _cibadmin_features_cached = []
+        rc, outp = get_stdout(['cibadmin', '-!'], shell=False)
+        if rc == 0:
+            outp = outp.strip()
+            m = re.match(r'Pacemaker\s(\S+)\s\(Build: ([^\)]+)\):\s(.*)', outp)
+            if m and len(m.groups()) > 2:
+                _cibadmin_features_cached = m.group(3).split()
+    return _cibadmin_features_cached
+
+
+def cibadmin_can_patch():
+    # cibadmin -P doesn't handle comments in <1.1.11 (unless patched)
+    return is_min_pcmk_ver("1.1.11")
+
+
+# quote function from python module shlex.py in python 3.3
+
+_find_unsafe = re.compile(r'[^\w@%+=:,./-]').search
+
+
+def quote(s):
+    """Return a shell-escaped version of the string *s*."""
+    if not s:
+        return "''"
+    if _find_unsafe(s) is None:
+        return s
+
+    # use single quotes, and put single quotes into double quotes
+    # the string $'b is then quoted as '$'"'"'b'
+    return "'" + s.replace("'", "'\"'\"'") + "'"
+
+
+def fetch_opts(args, opt_l):
+    '''
+    Get and remove option keywords from args.
+    They are always listed last, at the end of the line.
+    Return a list of options found. The caller can do
+    if keyw in optlist: ...
+    '''
+    re_opt = None
+    if opt_l[0].startswith("@"):
+        re_opt = re.compile("^%s$" % opt_l[0][1:])
+        del opt_l[0]
+    l = []
+    for i in reversed(range(len(args))):
+        if (args[i] in opt_l) or (re_opt and re_opt.search(args[i])):
+            l.append(args.pop())
+        else:
+            break
+    return l
+
+
+_LIFETIME = ["reboot", "forever"]
+_ISO8601_RE = re.compile("(PT?[0-9]|[0-9]+.*[:-])")
+
+
+def fetch_lifetime_opt(args, iso8601=True):
+    '''
+    Get and remove a lifetime option from args. It can be one of
+    lifetime_options or an ISO 8601 formatted period/time. There
+    is apparently no good support in python for this format, so
+    we cheat a bit.
+    '''
+    if args:
+        opt = args[-1]
+        if opt in _LIFETIME or (iso8601 and _ISO8601_RE.match(opt)):
+            return args.pop()
+    return None
+
+
+def resolve_hostnames(hostnames):
+    '''
+    Tries to resolve the given list of hostnames.
+    returns (ok, failed-hostname)
+    ok: True if all hostnames resolved
+    failed-hostname: First failed hostname resolution
+    '''
+    import socket
+    for node in hostnames:
+        try:
+            socket.gethostbyname(node)
+        except socket.error:
+            return False, node
+    return True, None
+
+
+def list_corosync_nodes():
+    '''
+    Returns list of nodes configured
+    in corosync.conf
+    '''
+    try:
+        cfg = os.getenv('COROSYNC_MAIN_CONFIG_FILE', '/etc/corosync/corosync.conf')
+        lines = open(cfg).read().split('\n')
+        addr_re = re.compile(r'\s*ring0_addr:\s+(.*)')
+        nodes = []
+        for line in lines:
+            addr = addr_re.match(line)
+            if addr:
+                nodes.append(addr.group(1))
+        return nodes
+    except Exception:
+        return []
+
+
+def list_cluster_nodes():
+    '''
+    Returns a list of nodes in the cluster.
+    '''
+
+    def getname(toks):
+        if toks and len(toks) >= 2:
+            return toks[1]
+        return None
+
+    try:
+        rc, outp = stdout2list(['crm_node', '-l'], stderr_on=False, shell=False)
+        if rc != 0:
+            raise ValueError("Error listing cluster nodes: crm_node (rc=%d)" % (rc))
+        return [x for x in [getname(line.split()) for line in outp] if x and x != '(null)']
+    except OSError, msg:
+        raise ValueError("Error listing cluster nodes: %s" % (msg))
+
+
+def service_info(name):
+    p = is_program('systemctl')
+    if p:
+        rc, outp = get_stdout([p, 'show',
+                               '-p', 'UnitFileState',
+                               '-p', 'ActiveState',
+                               '-p', 'SubState',
+                               name + '.service'], shell=False)
+        if rc == 0:
+            info = []
+            for line in outp.split('\n'):
+                data = line.split('=', 1)
+                if len(data) == 2:
+                    info.append(data[1].strip())
+            return '/'.join(info)
+    return None
+
+
+# This RE matches nvpair values that can
+# be left unquoted
+_NOQUOTES_RE = re.compile(r'^[\w\.-]+$')
+
+
+def noquotes(v):
+    return _NOQUOTES_RE.match(v) is not None
+
+
+# vim:ts=4:sw=4:et:
diff --git a/modules/xmlbuilder.py b/modules/xmlbuilder.py
new file mode 100644
index 0000000..c4c8f57
--- /dev/null
+++ b/modules/xmlbuilder.py
@@ -0,0 +1,117 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+from lxml import etree
+import constants
+
+
+def new(tag, **attributes):
+    """
+    <tag/>
+    """
+    return etree.Element(tag, **attributes)
+
+
+def child(parent, tag, **attributes):
+    """append new tag to parent.
+    Use append() in case parent is a list and not an element.
+    """
+    e = etree.Element(tag, **attributes)
+    parent.append(e)
+    return e
+
+
+def tostring(n):
+    return etree.tostring(n, pretty_print=True)
+
+
+def maybe_set(node, key, value):
+    if value:
+        node.set(key, value)
+    return node
+
+
+def nvpair(name, value):
+    """
+    <nvpair name="" value="" />
+    """
+    return new("nvpair", name=name, value=value)
+
+
+def nvpair_id(nvpairid, name, value):
+    """
+    <nvpair id="" name="" value="" />
+    """
+    if name is None:
+        name = nvpairid
+    return new("nvpair", id=nvpairid, name=name, value=value)
+
+
+def nvpair_ref(idref, name=None):
+    """
+    <nvpair id-ref=<idref> [name=<name>]/>
+    """
+    print "nvpair_ref:", repr(idref), repr(name)
+    nvp = new("nvpair")
+    nvp.set('id-ref', idref)
+    if name is not None:
+        nvp.set('name', name)
+    return nvp
+
+
+def set_date_expression(expr, tag, values):
+    """
+    Fill in date_expression tag for date_spec/in_range operations
+    expr: <date_expression/>
+    values: [nvpair...]
+    """
+    if set(nvp.get('name') for nvp in values) == set(constants.in_range_attrs):
+        for nvp in values:
+            expr.set(nvp.get('name'), nvp.get('value'))
+        return expr
+    subtag = child(expr, tag)
+    for nvp in values:
+        if nvp.get('name') in constants.in_range_attrs:
+            expr.set(nvp.get('name'), nvp.get('value'))
+        else:
+            subtag.set(nvp.get('name'), nvp.get('value'))
+    return expr
+
+
+def attributes(typename, rules, values, xmlid=None, score=None):
+    """
+    Represents a set of name-value pairs, tagged with
+    a container typename and an optional xml id.
+    The container can also hold rule expressions, passed
+    in the rules parameter.
+
+    returns an xml object containing the data
+    example:
+    <instance_attributes id="foo">
+    <nvpair name="thing" value="yes"/>
+    </instance_attributes>
+    """
+    e = new(typename)
+    if xmlid:
+        e.set("id", xmlid)
+    if score:
+        e.set("score", score)
+    for rule in rules:
+        e.append(rule)
+    for nvp in values:
+        e.append(nvp)
+    return e
diff --git a/modules/xmlutil.py b/modules/xmlutil.py
new file mode 100644
index 0000000..8052fec
--- /dev/null
+++ b/modules/xmlutil.py
@@ -0,0 +1,1323 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+import os
+import subprocess
+from lxml import etree, doctestcompare
+import copy
+import bz2
+from collections import defaultdict
+
+import config
+import options
+import schema
+import constants
+from msg import common_err, common_error, common_debug, cib_parse_err, err_buf
+import userdir
+import utils
+from utils import add_sudo, str2file, str2tmp, get_boolean
+from utils import get_stdout, stdout2list, crm_msec, crm_time_cmp
+from utils import olist, get_cib_in_use, get_tempdir
+
+
+def xmlparse(f):
+    try:
+        cib_elem = etree.parse(f).getroot()
+    except Exception, msg:
+        common_err("cannot parse xml: %s" % msg)
+        return None
+    return cib_elem
+
+
+def file2cib_elem(s):
+    try:
+        f = open(s, 'r')
+    except IOError, msg:
+        common_err(msg)
+        return None
+    cib_elem = xmlparse(f)
+    f.close()
+    if options.regression_tests and cib_elem is None:
+        print "Failed to read CIB from file: %s" % (s)
+    return cib_elem
+
+
+def compressed_file_to_cib(s):
+    try:
+        if s.endswith('.bz2'):
+            import bz2
+            f = bz2.BZ2File(s)
+        elif s.endswith('.gz'):
+            import gzip
+            f = gzip.open(s)
+        else:
+            f = open(s)
+    except IOError, msg:
+        common_err(msg)
+        return None
+    cib_elem = xmlparse(f)
+    if options.regression_tests and cib_elem is None:
+        print "Failed to read CIB from file %s" % (s)
+        f.seek(0)
+        print f.read()
+    f.close()
+    return cib_elem
+
+
+cib_dump = "cibadmin -Ql"
+
+
+def cibdump2file(fname):
+    cmd = add_sudo(cib_dump)
+    if options.regression_tests:
+        print ".EXT", cmd
+    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+    try:
+        s = ''.join(p.stdout)
+        p.wait()
+    except IOError, msg:
+        common_err(msg)
+        return None
+    return str2file(s, fname)
+
+
+def cibdump2tmp():
+    cmd = add_sudo(cib_dump)
+    if options.regression_tests:
+        print ".EXT", cmd
+    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+    try:
+        tmpf = str2tmp(''.join(p.stdout))
+        p.wait()
+    except IOError, msg:
+        common_err(msg)
+        return None
+    return tmpf
+
+
+def cibtext2elem(cibtext):
+    """
+    Convert a text format CIB to
+    an XML tree.
+    """
+    try:
+        return etree.fromstring(cibtext)
+    except Exception, err:
+        cib_parse_err(err, cibtext)
+        return None
+
+
+def cibdump2elem(section=None):
+    if section:
+        cmd = "%s -o %s" % (cib_dump, section)
+    else:
+        cmd = cib_dump
+    cmd = add_sudo(cmd)
+    if options.regression_tests:
+        print ".EXT", cmd
+    p = subprocess.Popen(cmd,
+                         shell=True,
+                         stdout=subprocess.PIPE,
+                         stderr=subprocess.PIPE)
+    try:
+        (outp, err_outp) = p.communicate()
+        p.wait()
+        rc = p.returncode
+    except IOError, msg:
+        common_err("running %s: %s" % (cmd, msg))
+        return None
+    if rc == 0:
+        return cibtext2elem(outp)
+    elif rc == constants.cib_no_section_rc:
+        return None
+    else:
+        common_error("running %s: %s" % (cmd, err_outp))
+        return None
+
+
+def read_cib(fun, params=None):
+    cib_elem = fun(params)
+    if cib_elem is None or cib_elem.tag != "cib":
+        return None
+    return cib_elem
+
+
+def sanity_check_nvpairs(id, node, attr_list):
+    rc = 0
+    for nvpair in node.iterchildren("nvpair"):
+        n = nvpair.get("name")
+        if n and n not in attr_list:
+            common_err("%s: attribute %s does not exist" % (id, n))
+            rc |= utils.get_check_rc()
+    return rc
+
+
+def sanity_check_meta(id, node, attr_list):
+    rc = 0
+    if node is None or not attr_list:
+        return rc
+    for c in node.iterchildren():
+        if c.tag == "meta_attributes":
+            rc |= sanity_check_nvpairs(id, c, attr_list)
+    return rc
+
+
+def get_interesting_nodes(node, nodes_l):
+    '''
+    All nodes which can be represented as CIB objects.
+    '''
+    for c in node.iterchildren():
+        if is_cib_element(c):
+            nodes_l.append(c)
+        get_interesting_nodes(c, nodes_l)
+    return nodes_l
+
+
+def get_top_cib_nodes(node, nodes_l):
+    '''
+    All nodes which can be represented as CIB objects, but not
+    nodes which are children of other CIB objects.
+    '''
+    for c in node.iterchildren():
+        if is_cib_element(c):
+            nodes_l.append(c)
+        else:
+            get_top_cib_nodes(c, nodes_l)
+    return nodes_l
+
+
+class RscState(object):
+    '''
+    Get the resource status and some other relevant bits.
+    In particular, this class should allow for a bit of caching
+    of cibadmin -Q -o resources output in case we need to check
+    more than one resource in a row.
+    '''
+
+    rsc_status = "crm_resource -W -r '%s'"
+
+    def __init__(self):
+        self.current_cib = None
+        self.rsc_elem = None
+        self.prop_elem = None
+        self.rsc_dflt_elem = None
+
+    def _init_cib(self):
+        self.current_cib = cibdump2elem("configuration")
+        self.rsc_elem = \
+            get_first_conf_elem(self.current_cib, "resources")
+        self.prop_elem = \
+            get_first_conf_elem(self.current_cib, "crm_config/cluster_property_set")
+        self.rsc_dflt_elem = \
+            get_first_conf_elem(self.current_cib, "rsc_defaults/meta_attributes")
+
+    def rsc2node(self, id):
+        '''
+        Get a resource XML element given the id.
+        NB: this is called from almost all other methods.
+        Hence we initialize the cib here. CIB reading is
+        expensive.
+        '''
+        if self.rsc_elem is None:
+            self._init_cib()
+        if self.rsc_elem is None:
+            return None
+        # does this need to be optimized?
+        expr = './/*[@id="%s"]' % id
+        try:
+            return self.rsc_elem.xpath(expr)[0]
+        except (IndexError, AttributeError):
+            return None
+
+    def is_ms(self, id):
+        '''
+        Test if the resource is master-slave.
+        '''
+        rsc_node = self.rsc2node(id)
+        if rsc_node is None:
+            return False
+        return is_ms(rsc_node)
+
+    def rsc_clone(self, id):
+        '''
+        Return id of the clone/ms containing this resource
+        or None if it's not cloned.
+        '''
+        rsc_node = self.rsc2node(id)
+        if rsc_node is None:
+            return None
+        pnode = rsc_node.getparent()
+        if pnode is None:
+            return None
+        if is_group(pnode):
+            pnode = pnode.getparent()
+        if is_clonems(pnode):
+            return pnode.get("id")
+        return None
+
+    def is_managed(self, id):
+        '''
+        Is this resource managed?
+        '''
+        rsc_node = self.rsc2node(id)
+        if rsc_node is None:
+            return False
+        # maintenance-mode, if true, overrides all
+        attr = get_attr_value(self.prop_elem, "maintenance-mode")
+        if attr and is_xs_boolean_true(attr):
+            return False
+        # then check the rsc is-managed meta attribute
+        rsc_meta_node = get_rsc_meta_node(rsc_node)
+        attr = get_attr_value(rsc_meta_node, "is-managed")
+        if attr:
+            return is_xs_boolean_true(attr)
+        # then rsc_defaults is-managed attribute
+        attr = get_attr_value(self.rsc_dflt_elem, "is-managed")
+        if attr:
+            return is_xs_boolean_true(attr)
+        # finally the is-managed-default property
+        attr = get_attr_value(self.prop_elem, "is-managed-default")
+        if attr:
+            return is_xs_boolean_true(attr)
+        return True
+
+    def is_running(self, id):
+        '''
+        Is this resource running?
+        '''
+        if not is_live_cib():
+            return False
+        test_id = self.rsc_clone(id) or id
+        rc, outp = get_stdout(self.rsc_status % test_id, stderr_on=False)
+        return outp.find("running") > 0 and outp.find("NOT") == -1
+
+    def is_group(self, id):
+        '''
+        Test if the resource is a group
+        '''
+        rsc_node = self.rsc2node(id)
+        if rsc_node is None:
+            return False
+        return is_group(rsc_node)
+
+    def can_delete(self, id):
+        '''
+        Can a resource be deleted?
+        The order below is important!
+        '''
+        return not (self.is_running(id) and not self.is_group(id) and self.is_managed(id))
+
+
+def resources_xml():
+    return cibdump2elem("resources")
+
+
+def is_normal_node(n):
+    return n.tag == "node" and (n.get("type") in (None, "normal", "member", ""))
+
+
+def unique_ra(typ, klass, provider):
+    """
+    Unique:
+    * it's explicitly ocf:heartbeat: or ocf:pacemaker:
+    * no explicit class or provider
+    * only one provider (heartbeat and pacemaker counts as one provider)
+    Not unique:
+    * class is not ocf
+    * multiple providers
+    """
+    if klass is None and provider is None:
+        return True
+    return klass == 'ocf' and provider is None or provider == 'heartbeat'
+
+
+def mk_rsc_type(n):
+    """
+    Returns prefixless for unique RAs
+    """
+    ra_type = n.get("type")
+    ra_class = n.get("class")
+    ra_provider = n.get("provider")
+    if unique_ra(ra_type, ra_class, ra_provider):
+        ra_class = None
+        ra_provider = None
+    s1 = s2 = ''
+    if ra_class:
+        s1 = "%s:" % ra_class
+    if ra_provider:
+        s2 = "%s:" % ra_provider
+    return ''.join((s1, s2, ra_type))
+
+
+def listnodes():
+    cib = cibdump2elem()
+    if cib is None:
+        return []
+    local_nodes = cib.xpath('/cib/configuration/nodes/node/@uname')
+    remote_nodes = cib.xpath('/cib/status/node_state[@remote_node="true"]/@uname')
+    return list(set([n for n in local_nodes + remote_nodes if n]))
+
+
+def is_our_node(s):
+    '''
+    Check if s is in a list of our nodes (ignore case).
+    This is not fast, perhaps should be cached.
+
+    Includes remote nodes as well
+    '''
+    for n in listnodes():
+        if n.lower() == s.lower():
+            return True
+    return False
+
+
+def is_live_cib():
+    '''We working with the live cluster?'''
+    return not get_cib_in_use() and not os.getenv("CIB_file")
+
+
+def is_crmuser():
+    return (config.core.user in ("root", config.path.crm_daemon_user)
+            or userdir.getuser() in ("root", config.path.crm_daemon_user))
+
+
+def cib_shadow_dir():
+    if os.getenv("CIB_shadow_dir"):
+        return os.getenv("CIB_shadow_dir")
+    if is_crmuser():
+        return config.path.crm_config
+    home = userdir.gethomedir(config.core.user)
+    if home and home.startswith(os.path.sep):
+        return os.path.join(home, ".cib")
+    return get_tempdir()
+
+
+def listshadows():
+    dir = cib_shadow_dir()
+    if not os.path.isdir(dir):
+        return []
+    rc, l = stdout2list("ls %s | fgrep shadow. | sed 's/^shadow\\.//'" % dir)
+    return l
+
+
+def shadowfile(name):
+    return "%s/shadow.%s" % (cib_shadow_dir(), name)
+
+
+def pe2shadow(pe_file, name):
+    '''Copy a PE file (or any CIB file) to a shadow.'''
+    try:
+        f = open(pe_file)
+    except IOError, msg:
+        common_err("open: %s" % msg)
+        return False
+    s = ''.join(f)
+    f.close()
+    # decompresed if it ends with .bz2
+    if pe_file.endswith(".bz2"):
+        s = bz2.decompress(s)
+    # copy input to the shadow
+    try:
+        f = open(shadowfile(name), "w")
+    except IOError, msg:
+        common_err("open: %s" % msg)
+        return False
+    f.write(s)
+    f.close()
+    return True
+
+
+def is_xs_boolean_true(bool):
+    return bool.lower() in ("true", "1")
+
+
+def cloned_el(node):
+    for c in node.iterchildren():
+        if is_resource(c):
+            return c.tag
+
+
+def get_topmost_rsc(node):
+    '''
+    Return a topmost node which is a resource and contains this resource
+    '''
+    if is_container(node.getparent()):
+        return get_topmost_rsc(node.getparent())
+    return node
+
+
+attr_defaults = {
+    "rule": (("boolean-op", "and"),),
+    "expression": (("type", "string"),),
+}
+
+
+def drop_attr_defaults(node, ts=0):
+    try:
+        for defaults in attr_defaults[node.tag]:
+            if node.get(defaults[0]) == defaults[1]:
+                del node.attrib[defaults[0]]
+    except:
+        pass
+
+
+def nameandid(e, level):
+    if e.tag:
+        print level*' ', e.tag, e.get("id"), e.get("name")
+
+
+def xmltraverse(e, fun, ts=0):
+    for c in e.iterchildren():
+        fun(c, ts)
+        xmltraverse(c, fun, ts+1)
+
+
+def xmltraverse_thin(e, fun, ts=0):
+    '''
+    Skip elements which may be resources themselves.
+    NB: Call this only on resource (or constraint) nodes, but
+    never on cib or configuration!
+    '''
+    for c in e.iterchildren():
+        if c.tag not in ('primitive', 'group'):
+            xmltraverse_thin(c, fun, ts+1)
+    fun(e, ts)
+
+
+def xml_processnodes(e, node_filter, proc):
+    '''
+    Process with proc all nodes that match filter.
+    '''
+    node_list = []
+    for child in e.iterchildren():
+        if node_filter(child):
+            node_list.append(child)
+        if len(child) > 0:
+            xml_processnodes(child, node_filter, proc)
+    if node_list:
+        proc(node_list)
+
+
+# filter the cib
+def true(e):
+    'Just return True.'
+    return True
+
+
+def is_entity(e):
+    return e.tag == etree.Entity
+
+
+def is_comment(e):
+    return e.tag == etree.Comment
+
+
+def is_status_node(e):
+    return e.tag == "status"
+
+
+def is_emptyelem(node, tag_l):
+    if node.tag in tag_l:
+        for a in constants.precious_attrs:
+            if node.get(a):
+                return False
+        for n in node.iterchildren():
+            return False
+        return True
+    else:
+        return False
+
+
+def is_emptynvpairs(node):
+    return is_emptyelem(node, constants.nvpairs_tags)
+
+
+def is_emptyops(node):
+    return is_emptyelem(node, ("operations",))
+
+
+def is_cib_element(node):
+    return node.tag in constants.cib_cli_map
+
+
+def is_group(node):
+    return node.tag == "group"
+
+
+def is_ms(node):
+    return node.tag in ("master", "ms")
+
+
+def is_clone(node):
+    return node.tag == "clone"
+
+
+def is_clonems(node):
+    return node.tag in constants.clonems_tags
+
+
+def is_cloned(node):
+    return (node.getparent().tag in constants.clonems_tags or
+            (node.getparent().tag == "group" and
+             node.getparent().getparent().tag in constants.clonems_tags))
+
+
+def is_container(node):
+    return node.tag in constants.container_tags
+
+
+def is_primitive(node):
+    return node.tag == "primitive"
+
+
+def is_resource(node):
+    return node.tag in constants.resource_tags
+
+
+def is_template(node):
+    return node.tag == "template"
+
+
+def is_child_rsc(node):
+    return node.tag in constants.children_tags
+
+
+def is_constraint(node):
+    return node.tag in constants.constraint_tags
+
+
+def is_defaults(node):
+    return node.tag in constants.defaults_tags
+
+
+def rsc_constraint(rsc_id, con_elem):
+    for attr in con_elem.keys():
+        if attr in constants.constraint_rsc_refs \
+                and rsc_id == con_elem.get(attr):
+            return True
+    for rref in con_elem.xpath("resource_set/resource_ref"):
+        if rsc_id == rref.get("id"):
+            return True
+    return False
+
+
+def sort_container_children(e_list):
+    '''
+    Make sure that attributes's nodes are first, followed by the
+    elements (primitive/group). The order of elements is not
+    disturbed, they are just shifted to end!
+    '''
+    for node in e_list:
+        children = [x for x in node.iterchildren()
+                    if x.tag in constants.children_tags]
+        for c in children:
+            node.remove(c)
+        for c in children:
+            node.append(c)
+
+
+def rmnode(e):
+    if e is not None and e.getparent() is not None:
+        e.getparent().remove(e)
+
+
+def rmnodes(e_list):
+    for e in e_list:
+        rmnode(e)
+
+
+def printid(e_list):
+    for e in e_list:
+        id = e.get("id")
+        if id:
+            print "element id:", id
+
+
+def remove_dflt_attrs(e_list):
+    '''
+    Drop optional attributes which are already set to default
+    '''
+    for e in e_list:
+        try:
+            d = constants.attr_defaults[e.tag]
+            for a in d.keys():
+                if e.get(a) == d[a]:
+                    del e.attrib[a]
+        except:
+            pass
+
+
+def remove_text(e_list):
+    for e in e_list:
+        if not is_comment(e):
+            e.text = None
+            e.tail = None
+
+
+def sanitize_cib(doc):
+    xml_processnodes(doc, is_status_node, rmnodes)
+    #xml_processnodes(doc, true, printid)
+    #xml_processnodes(doc, is_emptynvpairs, rmnodes)
+    #xml_processnodes(doc, is_emptyops, rmnodes)
+    xml_processnodes(doc, is_entity, rmnodes)
+    #xml_processnodes(doc, is_comment, rmnodes)
+    xml_processnodes(doc, is_container, sort_container_children)
+    xml_processnodes(doc, true, remove_dflt_attrs)
+    xml_processnodes(doc, true, remove_text)
+    xmltraverse(doc, drop_attr_defaults)
+
+
+def is_simpleconstraint(node):
+    return len(node.xpath("resource_set/resource_ref")) == 0
+
+
+match_list = defaultdict(tuple,
+                         {"node": ("uname",),
+                          "nvpair": ("name",),
+                          "op": ("name", "interval"),
+                          "rule": ("score", "score-attribute", "role"),
+                          "expression": ("attribute", "operation", "value"),
+                          "fencing-level": ("target", "devices")})
+
+
+def add_comment(e, s):
+    '''
+    Add comment s to e from doc.
+    '''
+    if e is None or not s:
+        return
+    comm_elem = etree.Comment(s)
+    firstelem_idx = 0
+    for c in e.iterchildren():
+        firstelem_idx = e.index(c)
+        break
+    e.insert(firstelem_idx, comm_elem)
+
+
+def stuff_comments(node, comments):
+    if not comments:
+        return
+    for s in reversed(comments):
+        add_comment(node, s)
+
+
+def fix_comments(e):
+    'Make sure that comments start with #'
+    celems = [x for x in e.iterchildren() if is_comment(x)]
+    for c in celems:
+        c.text = c.text.strip()
+        if not c.text.startswith("#"):
+            c.text = "# %s" % c.text
+
+
+def set_id_used_attr(e):
+    e.set("__id_used", "Yes")
+
+
+def is_id_used_attr(e):
+    return e.get("__id_used") == "Yes"
+
+
+def remove_id_used_attr(e, lvl):
+    if is_id_used_attr(e):
+        del e.attrib["__id_used"]
+
+
+def remove_id_used_attributes(e):
+    if e is not None:
+        xmltraverse(e, remove_id_used_attr)
+
+
+def lookup_node(node, oldnode, location_only=False, ignore_id=False):
+    '''
+    Find a child of oldnode which matches node.
+    This is used to "harvest" existing ids in order to prevent
+    irrelevant changes to the XML code.
+    The list of attributes to match is in the dictionary
+    match_list.
+    The "id" attribute is treated differently. In case the new node
+    (the first parameter here) contains the id, then the "id"
+    attribute is added to the match list.
+    '''
+    if oldnode is None:
+        return None
+    attr_list = list(match_list[node.tag])
+    if not ignore_id and node.get("id"):
+        attr_list.append("id")
+    for c in oldnode.iterchildren():
+        if not location_only and is_id_used_attr(c):
+            continue
+        if node.tag == c.tag:
+            for a in attr_list:
+                if node.get(a) != c.get(a):
+                    break
+            else:
+                return c
+    return None
+
+
+def find_operation(rsc_node, name, interval=None):
+    '''
+    Setting interval to "non-0" means get the first op with interval
+    different from 0.
+    Not setting interval at all means get the only matching op, or the
+    0 op (if any)
+    '''
+    matching_name = []
+    for ops in rsc_node.findall("operations"):
+        matching_name.extend([op for op in ops.iterchildren("op")
+                              if op.get("name") == name])
+    if interval is None and len(matching_name) == 1:
+        return matching_name[0]
+    interval = interval or "0"
+    for op in matching_name:
+        opint = op.get("interval")
+        if interval == "non-0" and crm_msec(opint) > 0:
+            return op
+        if crm_time_cmp(opint, interval) == 0:
+            return op
+    return None
+
+
+def get_op_timeout(rsc_node, op, default_timeout):
+    interval = (op == "monitor" and "non-0" or "0")
+    op_n = find_operation(rsc_node, op == "probe" and "monitor" or op, interval)
+    timeout = op_n is not None and op_n.get("timeout") or default_timeout
+    return crm_msec(timeout)
+
+
+def op2list(node):
+    pl = []
+    action = ""
+    for name in node.keys():
+        if name == "name":
+            action = node.get(name)
+        elif name != "id":  # skip the id
+            pl.append([name, node.get(name)])
+    if not action:
+        common_err("op is invalid (no name)")
+    return action, pl
+
+
+def get_rsc_operations(rsc_node):
+    actions = [op2list(op) for op in rsc_node.xpath('.//operations/op')]
+    actions = [[op, pl] for op, pl in actions if op]
+    return actions
+
+
+# lower score = earlier sort
+def make_sort_map(*order):
+    m = {}
+    for i, o in enumerate(order):
+        if isinstance(o, basestring):
+            m[o] = i
+        else:
+            for k in o:
+                m[k] = i
+    return m
+
+
+_sort_xml_order = make_sort_map('node', 'template', 'primitive',
+                                'group', 'master', 'clone',
+                                'rsc_location', 'rsc_colocation',
+                                'rsc_order', 'rsc_ticket', 'fencing-topology',
+                                'cluster_property_set', 'rsc_defaults', 'op_defaults',
+                                'op', 'acl_role', 'acl_user', 'tag')
+
+_sort_cli_order = make_sort_map('node', 'rsc_template', 'primitive',
+                                'group', 'ms', 'master', 'clone',
+                                'location', 'colocation', 'collocation',
+                                'order', 'rsc_ticket', 'fencing_topology',
+                                'property', 'rsc_defaults', 'op_defaults',
+                                'op', 'role', 'user', 'tag')
+
+_SORT_LAST = 1000
+
+
+def processing_sort(nl):
+    '''
+    It's usually important to process cib objects in this order,
+    i.e. simple objects first.
+    '''
+    if config.core.sort_elements:
+        sortfn = lambda k: (_sort_xml_order.get(k.tag, _SORT_LAST), k.get('id'))
+    else:
+        sortfn = lambda k: _sort_xml_order.get(k.tag, _SORT_LAST)
+    return sorted(nl, key=sortfn)
+
+
+def processing_sort_cli(cl):
+    '''
+    cl: list of objects (CibObject)
+    Returns the given list in order
+    '''
+    if config.core.sort_elements:
+        sortfn = lambda k: (_sort_cli_order.get(k.obj_type, _SORT_LAST), k.obj_id)
+    else:
+        sortfn = lambda k: _sort_cli_order.get(k.obj_type, _SORT_LAST)
+    return sorted(cl, key=sortfn)
+
+
+def is_resource_cli(s):
+    return s in olist(constants.resource_cli_names)
+
+
+def is_constraint_cli(s):
+    return s in olist(constants.constraint_cli_names)
+
+
+def referenced_resources(node):
+    if not is_constraint(node):
+        return []
+    xml_obj_type = node.tag
+    rsc_list = []
+    if xml_obj_type == "rsc_location" and node.get("rsc"):
+        rsc_list = [node.get("rsc")]
+    elif node.xpath("resource_set/resource_ref"):
+        # resource sets
+        rsc_list = [x.get("id")
+                    for x in node.xpath("resource_set/resource_ref")]
+    elif xml_obj_type == "rsc_colocation":
+        rsc_list = [node.get("rsc"), node.get("with-rsc")]
+    elif xml_obj_type == "rsc_order":
+        rsc_list = [node.get("first"), node.get("then")]
+    elif xml_obj_type == "rsc_ticket":
+        rsc_list = [node.get("rsc")]
+    return [rsc for rsc in rsc_list if rsc is not None]
+
+
+def rename_id(node, old_id, new_id):
+    if node.get("id") == old_id:
+        node.set("id", new_id)
+
+
+def rename_rscref_simple(c_obj, old_id, new_id):
+    c_modified = False
+    for attr in c_obj.node.keys():
+        if attr in constants.constraint_rsc_refs and \
+                c_obj.node.get(attr) == old_id:
+            c_obj.node.set(attr, new_id)
+            c_obj.updated = True
+            c_modified = True
+    return c_modified
+
+
+def delete_rscref_simple(c_obj, rsc_id):
+    c_modified = False
+    for attr in c_obj.node.keys():
+        if attr in constants.constraint_rsc_refs and \
+                c_obj.node.get(attr) == rsc_id:
+            del c_obj.node.attrib[attr]
+            c_obj.updated = True
+            c_modified = True
+    return c_modified
+
+
+def rset_uniq(c_obj, d):
+    '''
+    Drop duplicate resource references.
+    '''
+    l = []
+    for rref in c_obj.node.xpath("resource_set/resource_ref"):
+        rsc_id = rref.get("id")
+        if d[rsc_id] > 1:
+            # drop one
+            l.append(rref)
+            d[rsc_id] -= 1
+    rmnodes(l)
+
+
+def delete_rscref_rset(c_obj, rsc_id):
+    '''
+    Drop all reference to rsc_id.
+    '''
+    c_modified = False
+    l = []
+    for rref in c_obj.node.xpath("resource_set/resource_ref"):
+        if rsc_id == rref.get("id"):
+            l.append(rref)
+            c_obj.updated = True
+            c_modified = True
+    rmnodes(l)
+    l = []
+    cnt = 0
+    nonseq_rset = False
+    for rset in c_obj.node.findall("resource_set"):
+        rref_cnt = len(rset.findall("resource_ref"))
+        if rref_cnt == 0:
+            l.append(rset)
+            c_obj.updated = True
+            c_modified = True
+        elif not get_boolean(rset.get("sequential"), True) and rref_cnt > 1:
+            nonseq_rset = True
+        cnt += rref_cnt
+    rmnodes(l)
+    if not nonseq_rset and cnt == 2:
+        rset_convert(c_obj)
+    return c_modified
+
+
+def rset_convert(c_obj):
+    l = c_obj.node.xpath("resource_set/resource_ref")
+    if len(l) != 2:
+        return  # eh?
+    rsetcnt = 0
+    for rset in c_obj.node.findall("resource_set"):
+        # in case there are multiple non-sequential sets
+        if rset.get("sequential"):
+            del rset.attrib["sequential"]
+        rsetcnt += 1
+    c_obj.modified = True
+    cli = c_obj.repr_cli(format=-1)
+    cli = cli.replace("_rsc_set_ ", "")
+    newnode = c_obj.cli2node(cli)
+    if newnode is not None:
+        c_obj.node.getparent().replace(c_obj.node, newnode)
+        c_obj.node = newnode
+        if rsetcnt == 1 and c_obj.obj_type == "colocation":
+            # exchange the elements in colocations
+            rsc = newnode.get("rsc")
+            with_rsc = newnode.get("with-rsc")
+            if with_rsc is not None:
+                newnode.set("rsc", with_rsc)
+            if rsc is not None:
+                newnode.set("with-rsc", rsc)
+
+
+def rename_rscref_rset(c_obj, old_id, new_id):
+    c_modified = False
+    d = {}
+    for rref in c_obj.node.xpath("resource_set/resource_ref"):
+        rsc_id = rref.get("id")
+        if rsc_id == old_id:
+            rref.set("id", new_id)
+            rsc_id = new_id
+            c_obj.updated = True
+            c_modified = True
+        if rsc_id not in d:
+            d[rsc_id] = 1
+        else:
+            d[rsc_id] += 1
+    rset_uniq(c_obj, d)
+    # if only two resource references remained then, to preserve
+    # sanity, convert it to a simple constraint (sigh)
+    cnt = 0
+    for key in d:
+        cnt += d[key]
+    if cnt == 2:
+        rset_convert(c_obj)
+    return c_modified
+
+
+def rename_rscref(c_obj, old_id, new_id):
+    if rename_rscref_simple(c_obj, old_id, new_id) or \
+            rename_rscref_rset(c_obj, old_id, new_id):
+        err_buf.info("resource references in %s updated" % str(c_obj))
+
+
+def delete_rscref(c_obj, rsc_id):
+    return delete_rscref_simple(c_obj, rsc_id) or \
+        delete_rscref_rset(c_obj, rsc_id)
+
+
+def silly_constraint(c_node, rsc_id):
+    '''
+    Remove a constraint from rsc_id to rsc_id.
+    Or an invalid one.
+    '''
+    if c_node.xpath("resource_set/resource_ref"):
+        # it's a resource set
+        # the resource sets have already been uniq-ed
+        cnt = len(c_node.xpath("resource_set/resource_ref"))
+        if c_node.tag in ("rsc_location", "rsc_ticket"):  # locations and tickets are never silly
+            return cnt < 1
+        return cnt <= 1
+    cnt = 0  # total count of referenced resources have to be at least two
+    rsc_cnt = 0
+    for attr in c_node.keys():
+        if attr in constants.constraint_rsc_refs:
+            cnt += 1
+            if c_node.get(attr) == rsc_id:
+                rsc_cnt += 1
+    if c_node.tag in ("rsc_location", "rsc_ticket"):  # locations and tickets are never silly
+        return cnt < 1
+    else:
+        return rsc_cnt == 2 or cnt < 2
+
+
+def is_climove_location(node):
+    'Figure out if the location was created by crm resource move.'
+    rule_l = node.findall("rule")
+    expr_l = node.xpath(".//expression")
+    return len(rule_l) == 1 and len(expr_l) == 1 and \
+        node.get("id").startswith("cli-") and \
+        expr_l[0].get("attribute") == "#uname" and \
+        expr_l[0].get("operation") == "eq"
+
+
+def is_pref_location(node):
+    'Figure out if the location is a node preference.'
+    rule_l = node.findall("rule")
+    expr_l = node.xpath(".//expression")
+    return len(rule_l) == 1 and len(expr_l) == 1 and \
+        expr_l[0].get("attribute") == "#uname" and \
+        expr_l[0].get("operation") == "eq"
+
+
+def get_rsc_ref_ids(node):
+    return [x.get("id")
+            for x in node.xpath("./resource_ref")]
+
+
+def get_rsc_children_ids(node):
+    return [x.get("id")
+            for x in node.iterchildren() if is_child_rsc(x)]
+
+
+def get_prim_children_ids(node):
+    l = [x for x in node.iterchildren() if is_child_rsc(x)]
+    if len(l) and l[0].tag == "group":
+        l = [x for x in l[0].iterchildren() if is_child_rsc(x)]
+    return [x.get("id") for x in l]
+
+
+def get_child_nvset_node(node, attr_set="meta_attributes"):
+    if node is None:
+        return None
+    for c in node.iterchildren():
+        if c.tag != attr_set:
+            continue
+        return c
+    return None
+
+
+def get_rscop_defaults_meta_node(node):
+    return get_child_nvset_node(node)
+
+
+def get_rsc_meta_node(node):
+    return get_child_nvset_node(node)
+
+
+def get_properties_node(node):
+    return get_child_nvset_node(node, attr_set="cluster_property_set")
+
+
+def new_cib():
+    cib_elem = etree.Element("cib")
+    conf_elem = etree.SubElement(cib_elem, "configuration")
+    for name in schema.get('sub', "configuration", 'r'):
+        etree.SubElement(conf_elem, name)
+    return cib_elem
+
+
+def get_conf_elems(cib_elem, path):
+    '''
+    Get a list of configuration elements. All elements are within
+    /configuration
+    '''
+    if cib_elem is None:
+        return None
+    return cib_elem.xpath("//configuration/%s" % path)
+
+
+def get_first_conf_elem(cib_elem, path):
+    try:
+        elems = get_conf_elems(cib_elem, path)
+        return elems[0] if elems else None
+    except IndexError:
+        return None
+
+
+def get_topnode(cib_elem, tag):
+    "Get configuration element or create/append if there's none."
+    conf_elem = cib_elem.find("configuration")
+    if conf_elem is None:
+        common_err("no configuration element found!")
+        return None
+    if tag == "configuration":
+        return conf_elem
+    e = cib_elem.find("configuration/%s" % tag)
+    if e is None:
+        common_debug("create configuration section %s" % tag)
+        e = etree.SubElement(conf_elem, tag)
+    return e
+
+
+def get_attr_in_set(e, attr):
+    if e is None:
+        return None
+    for c in e.iterchildren("nvpair"):
+        if c.get("name") == attr:
+            return c
+    return None
+
+
+def get_attr_value(e, attr):
+    try:
+        return get_attr_in_set(e, attr).get("value")
+    except:
+        return None
+
+
+def set_attr(e, attr, value):
+    '''
+    Set an attribute in the attribute set.
+    '''
+    nvpair = get_attr_in_set(e, attr)
+    if nvpair is None:
+        import idmgmt
+        nvpair = etree.SubElement(e, "nvpair", id="", name=attr, value=value)
+        nvpair.set("id", idmgmt.new(nvpair, e.get("id")))
+    else:
+        nvpair.set("name", attr)
+        nvpair.set("value", value)
+
+
+def get_set_nodes(e, setname, create=False):
+    """Return the attributes set nodes (create one if requested)
+    setname can for example be meta_attributes
+    """
+    l = [c for c in e.iterchildren(setname)]
+    if l:
+        return l
+    if create:
+        import idmgmt
+        elem = etree.SubElement(e, setname, id="")
+        elem.set("id", idmgmt.new(elem, e.get("id")))
+        l.append(elem)
+    return l
+
+
+_checker = doctestcompare.LXMLOutputChecker()
+
+
+def xml_equals_unordered(a, b):
+    "used by xml_equals to compare xml trees without ordering"
+    def fail(msg):
+        common_debug("%s!=%s: %s" % (a.tag, b.tag, msg))
+        return False
+
+    def tagflat(x):
+        return isinstance(x.tag, basestring) and x.tag or x.text
+
+    def sortby(v):
+        if v.tag == 'primitive':
+            return v.tag
+        return tagflat(v) + ''.join(sorted(v.attrib.keys() + v.attrib.values()))
+
+    def safe_strip(text):
+        return text is not None and text.strip() or ''
+
+    if a.tag != b.tag:
+        return fail("tags differ: %s != %s" % (a.tag, b.tag))
+    elif a.attrib != b.attrib:
+        return fail("attributes differ: %s != %s" % (a.attrib, b.attrib))
+    elif safe_strip(a.text) != safe_strip(b.text):
+        return fail("text differ %s != %s" % (repr(a.text), repr(b.text)))
+    elif safe_strip(a.tail) != safe_strip(b.tail):
+        return fail("tails differ: %s != %s" % (a.tail, b.tail))
+    elif len(a) != len(b):
+        return fail("number of children differ")
+    elif len(a) == 0:
+        return True
+
+    # order matters here, but in a strange way:
+    # all primitive tags should sort the same..
+    sorted_children = zip(sorted(a, key=sortby), sorted(b, key=sortby))
+    return all(xml_equals_unordered(a, b) for a, b in sorted_children)
+
+
+def xml_equals(n, m, show=False):
+    rc = xml_equals_unordered(n, m)
+    if not rc and show and config.core.debug:
+        # somewhat strange, but that's how this works
+        from doctest import Example
+        example = Example("etree.tostring(n)", etree.tostring(n))
+        got = etree.tostring(m)
+        print _checker.output_difference(example, got, 0)
+    return rc
+
+
+def merge_attributes(dnode, snode, tag):
+    rc = False
+    add_children = []
+    for sc in snode.iterchildren(tag):
+        dc = lookup_node(sc, dnode, ignore_id=True)
+        if dc is not None:
+            for a, v in sc.items():
+                if a == "id":
+                    continue
+                if v != dc.get(a):
+                    dc.set(a, v)
+                    rc = True
+        else:
+            add_children.append(sc)
+            rc = True
+    for c in add_children:
+        dnode.append(copy.deepcopy(c))
+    return rc
+
+
+def merge_nodes(dnode, snode):
+    '''
+    Import elements from snode into dnode.
+    If an element is attributes set (constants.nvpairs_tags) or
+    "operations", then merge attributes in the children.
+    Otherwise, replace the whole element. (TBD)
+    '''
+    rc = False  # any changes done?
+    if dnode is None or snode is None:
+        return rc
+    add_children = []
+    for sc in snode.iterchildren():
+        dc = lookup_node(sc, dnode, ignore_id=True)
+        if dc is None:
+            if sc.tag in constants.nvpairs_tags or sc.tag == "operations":
+                add_children.append(sc)
+                rc = True
+        elif dc.tag in constants.nvpairs_tags:
+            rc = merge_attributes(dc, sc, "nvpair") or rc
+        elif dc.tag == "operations":
+            rc = merge_attributes(dc, sc, "op") or rc
+    for c in add_children:
+        dnode.append(copy.deepcopy(c))
+    return rc
+
+
+def merge_tmpl_into_prim(prim_node, tmpl_node):
+    '''
+    Create a new primitive element which is a merge of a
+    rsc_template and a primitive which references it.
+    '''
+    dnode = etree.Element(prim_node.tag)
+    merge_nodes(dnode, tmpl_node)
+    merge_nodes(dnode, prim_node)
+    # the resulting node should inherit all primitives attributes
+    for a, v in prim_node.items():
+        dnode.set(a, v)
+    # but class/provider/type are coming from the template
+    # savannah#41410: stonith resources do not have the provider
+    # attribute
+    for a in ("class", "provider", "type"):
+        v = tmpl_node.get(a)
+        if v is not None:
+            dnode.set(a, v)
+    return dnode
+
+
+# vim:ts=4:sw=4:et:
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..fd7c75d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+lxml
+PyYAML
+nosexcover
diff --git a/scripts/Makefile.am b/scripts/Makefile.am
new file mode 100644
index 0000000..4045865
--- /dev/null
+++ b/scripts/Makefile.am
@@ -0,0 +1,2 @@
+SUBDIRS = check-uptime health init add remove
+
diff --git a/scripts/add/Makefile.am b/scripts/add/Makefile.am
new file mode 100644
index 0000000..d01f5b8
--- /dev/null
+++ b/scripts/add/Makefile.am
@@ -0,0 +1,28 @@
+#
+# crmsh init script
+#
+# Copyright (C) 2014 Kristoffer Gronlund
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+scriptadddir 		= $(datadir)/@PACKAGE@/scripts/add
+
+scriptadd_DATA	= main.yml
+scriptadd_SCRIPTS	= add.py
+
+EXTRA_DIST	= $(scriptadd_DATA) $(scriptadd_SCRIPTS)
+
diff --git a/scripts/add/add.py b/scripts/add/add.py
new file mode 100755
index 0000000..68859f1
--- /dev/null
+++ b/scripts/add/add.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+import sys
+import os
+import crm_script
+import crm_init
+
+COROSYNC_AUTH = '/etc/corosync/authkey'
+COROSYNC_CONF = '/etc/corosync/corosync.conf'
+
+host = crm_script.host()
+add_nodes = crm_script.param('node').split(',')
+
+def run_collect():
+    if host not in add_nodes:
+        crm_script.exit_ok(host)
+
+    rc, out, err = crm_script.service('pacemaker', 'is-active')
+    if rc == 0 and out.strip() == 'active':
+        crm_script.exit_fail("Pacemaker already running on %s" % (host))
+    crm_script.exit_ok(crm_init.info())
+
+
+def make_opts():
+    from psshlib import api as pssh
+    opts = pssh.Options()
+    opts.timeout = 60
+    opts.recursive = True
+    opts.user = 'root'
+    opts.ssh_options += ['PasswordAuthentication=no',
+                         'StrictHostKeyChecking=no',
+                         'ControlPersist=no']
+    return opts
+
+
+def run_validate():
+    try:
+        from psshlib import api
+    except ImportError:
+        crm_script.exit_fail("Command node needs pssh installed")
+
+    if host in add_nodes:
+        crm_script.exit_fail("Run script from node in cluster")
+
+    crm_script.exit_ok(host)
+
+
+def run_install():
+    if host not in add_nodes:
+        crm_script.exit_ok(host)
+    packages = ['cluster-glue', 'corosync', 'crmsh', 'pacemaker', 'resource-agents']
+    crm_init.install_packages(packages)
+    crm_script.exit_ok(host)
+
+
+def check_results(pssh, results):
+    failures = []
+    for host, result in results.items():
+        if isinstance(result, pssh.Error):
+            failures.add("%s: %s" % (host, str(result)))
+    if failures:
+        crm_script.exit_fail(', '.join(failures))
+
+
+def run_copy():
+    try:
+        from psshlib import api as pssh
+    except ImportError:
+        crm_script.exit_fail("Command node needs pssh installed")
+    opts = make_opts()
+    has_auth = os.path.isfile(COROSYNC_AUTH)
+    if has_auth:
+        results = pssh.copy(add_nodes, COROSYNC_AUTH, COROSYNC_AUTH, opts)
+        check_results(pssh, results)
+        results = pssh.call(add_nodes,
+                            "chown root:root %s;chmod 400 %s" % (COROSYNC_AUTH, COROSYNC_AUTH),
+                            opts)
+        check_results(pssh, results)
+
+    # Add new nodes to corosync.conf before copying
+    for node in add_nodes:
+        rc, _, err = crm_script.call(['crm', 'corosync', 'add-node', node])
+        if rc != 0:
+            crm_script.exit_fail('Failed to add %s to corosync.conf: %s' % (node, err))
+
+    results = pssh.copy(add_nodes, COROSYNC_CONF, COROSYNC_CONF, opts)
+    check_results(pssh, results)
+
+    # reload corosync config here?
+    rc, _, err = crm_script.call(['crm', 'corosync', 'reload'])
+    if rc != 0:
+        crm_script.exit_fail('Failed to reload corosync configuration: %s' % (err))
+
+    crm_script.exit_ok(host)
+
+
+def run_firewall():
+    if host not in add_nodes:
+        crm_script.exit_ok(host)
+    crm_init.configure_firewall()
+    crm_script.exit_ok(host)
+
+
+def start_new_node():
+    if host not in add_nodes:
+        crm_script.exit_ok(host)
+    rc, _, err = crm_script.call(['crm', 'cluster', 'start'])
+    if rc == 0:
+        crm_script.exit_ok(host)
+    crm_script.exit_fail(err)
+
+
+if __name__ == "__main__":
+    if len(sys.argv) < 2:
+        crm_script.exit_fail("Missing argument")
+    elif sys.argv[1] == 'collect':
+        run_collect()
+    elif sys.argv[1] == 'validate':
+        run_validate()
+    elif sys.argv[1] == 'install':
+        run_install()
+    elif sys.argv[1] == 'copy':
+        run_copy()
+    elif sys.argv[1] == 'firewall':
+        run_firewall()
+    elif sys.argv[1] == 'start':
+        start_new_node()
+    else:
+        crm_script.exit_fail("Unknown argument: %s" % sys.argv[1])
diff --git a/scripts/add/main.yml b/scripts/add/main.yml
new file mode 100644
index 0000000..28b26ea
--- /dev/null
+++ b/scripts/add/main.yml
@@ -0,0 +1,34 @@
+---
+- name: Add a new node to an already existing cluster
+  description: >
+    Installs missing packages and copies corosync.conf
+    from one of the existing cluster nodes.
+
+    This script is somewhat special: The nodes parameter
+    must contain at least one node already in the cluster 
+    as well as the new node to add.
+
+    A more user-friendly interface to this script is 
+    provided via the cluster add command.
+  parameters:
+    - name: node
+      description: Node to add to the cluster
+  steps:
+    - name: Check cluster
+      collect: add.py collect
+
+    - name: Validate parameters
+      validate: add.py validate
+
+    - name: Install required packages
+      apply: add.py install
+
+    - name: Copy configuration files
+      apply_local: add.py copy
+
+    - name: Configure firewall
+      apply: add.py firewall
+
+    - name: Start cluster on new node
+      apply: add.py start
+
diff --git a/scripts/check-uptime/Makefile.am b/scripts/check-uptime/Makefile.am
new file mode 100644
index 0000000..f4aa605
--- /dev/null
+++ b/scripts/check-uptime/Makefile.am
@@ -0,0 +1,28 @@
+#
+# crmsh sample script
+#
+# Copyright (C) 2014 Kristoffer Gronlund
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+uptimedir 		= $(datadir)/@PACKAGE@/scripts/check-uptime
+
+uptime_DATA	= main.yml
+uptime_SCRIPTS	= fetch.py report.py
+
+EXTRA_DIST	= $(uptime_DATA) $(uptime_SCRIPTS)
+
diff --git a/scripts/check-uptime/fetch.py b/scripts/check-uptime/fetch.py
new file mode 100755
index 0000000..435f34a
--- /dev/null
+++ b/scripts/check-uptime/fetch.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python
+import crm_script
+try:
+    uptime = open('/proc/uptime').read().split()[0]
+    crm_script.exit_ok(uptime)
+except Exception, e:
+    crm_script.exit_fail("Couldn't open /proc/uptime: %s" % (e))
diff --git a/scripts/check-uptime/main.yml b/scripts/check-uptime/main.yml
new file mode 100644
index 0000000..419d0ad
--- /dev/null
+++ b/scripts/check-uptime/main.yml
@@ -0,0 +1,17 @@
+---
+- name: Check uptime of nodes
+  description: >
+    Fetches the uptime of all nodes and reports which
+    node has lived longest.
+
+  parameters:
+    - name: show_all
+      description: Show all uptimes
+      default: false
+
+  steps:
+    - name: Fetch uptimes
+      collect: fetch.py
+
+    - name: Report uptime
+      report: report.py
diff --git a/scripts/check-uptime/report.py b/scripts/check-uptime/report.py
new file mode 100755
index 0000000..445cfd0
--- /dev/null
+++ b/scripts/check-uptime/report.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+import crm_script
+show_all = crm_script.is_true(crm_script.param('show_all'))
+uptimes = crm_script.output(1).items()
+max_uptime = '', 0
+for host, uptime in uptimes:
+    if uptime > max_uptime[1]:
+        max_uptime = host, uptime
+if show_all:
+    print "Uptimes: %s" % (', '.join("%s: %s" % v for v in uptimes))
+print "Longest uptime is %s seconds on host %s" % (max_uptime[1], max_uptime[0])
diff --git a/scripts/health/Makefile.am b/scripts/health/Makefile.am
new file mode 100644
index 0000000..d683faf
--- /dev/null
+++ b/scripts/health/Makefile.am
@@ -0,0 +1,27 @@
+#
+# crmsh health monitoring script
+#
+# Copyright (C) 2014 Kristoffer Gronlund
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+healthdir 		= $(datadir)/@PACKAGE@/scripts/health
+
+health_DATA	= main.yml
+health_SCRIPTS	= collect.py report.py hahealth.py
+
+EXTRA_DIST	= $(health_DATA) $(health_SCRIPTS)
diff --git a/scripts/health/collect.py b/scripts/health/collect.py
new file mode 100755
index 0000000..8600973
--- /dev/null
+++ b/scripts/health/collect.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+import os
+import hashlib
+import platform
+import crm_script
+data = crm_script.get_input()
+
+PACKAGES = ['booth', 'cluster-glue', 'corosync', 'crmsh', 'csync2', 'drbd',
+            'fence-agents', 'gfs2', 'gfs2-utils', 'ha-cluster-bootstrap',
+            'haproxy', 'hawk', 'libdlm', 'libqb', 'ocfs2', 'ocfs2-tools',
+            'pacemaker', 'pacemaker-mgmt', 'pssh', 'resource-agents', 'sbd']
+
+def rpm_info():
+    return crm_script.rpmcheck(PACKAGES)
+
+def logrotate_info():
+    rc, _, _ = crm_script.call(
+        'grep -r corosync.conf /etc/logrotate.d',
+        shell=True)
+    return {'corosync.conf': rc == 0}
+
+def sys_info():
+    sysname, nodename, release, version, machine = os.uname()
+    #The first three columns measure CPU and IO utilization of the
+    #last one, five, and 15 minute periods. The fourth column shows
+    #the number of currently running processes and the total number of
+    #processes. The last column displays the last process ID used.
+    system, node, release, version, machine, processor = platform.uname()
+    distname, distver, distid = platform.linux_distribution()
+    hostname = platform.node().split('.')[0]
+
+    uptime = open('/proc/uptime').read().split()
+    loadavg = open('/proc/loadavg').read().split()
+
+    return {'system': system,
+            'node': node,
+            'release': release,
+            'version': version,
+            'machine': machine,
+            'processor': processor,
+            'distname': distname,
+            'distver': distver,
+            'distid': distid,
+            'user': os.getlogin(),
+            'hostname': hostname,
+            'uptime': uptime[0],
+            'idletime': uptime[1],
+            'loadavg': loadavg[2]  # 15 minute average
+            }
+
+def disk_info():
+    rc, out, err = crm_script.call(['df'], shell=False)
+    if rc == 0:
+        disk_use = []
+        for line in out.split('\n')[1:]:
+            line = line.strip()
+            if line:
+                data = line.split()
+                if len(data) >= 6:
+                    disk_use.append((data[5], data[4]))
+        return disk_use
+    return []
+
+# configurations out of sync
+
+FILES = [
+    '/etc/csync2/key_hagroup',
+    '/etc/csync2/csync2.cfg',
+    '/etc/corosync/corosync.conf',
+    '/etc/sysconfig/sbd',
+    '/etc/sysconfig/SuSEfirewall2',
+    '/etc/sysconfig/SuSEfirewall2.d/services/cluster'
+    ]
+
+
+def files_info():
+    ret = {}
+    for f in FILES:
+        if os.path.isfile(f):
+            try:
+                ret[f] = hashlib.sha1(open(f).read()).hexdigest()
+            except IOError, e:
+                ret[f] = "error: %s" % (e)
+        else:
+            ret[f] = ""
+    return ret
+
+
+try:
+    data = {
+        'rpm': rpm_info(),
+        'logrotate': logrotate_info(),
+        'system': sys_info(),
+        'disk': disk_info(),
+        'files': files_info()
+    }
+    crm_script.exit_ok(data)
+except Exception, e:
+    crm_script.exit_fail(str(e))
diff --git a/scripts/health/hahealth.py b/scripts/health/hahealth.py
new file mode 100755
index 0000000..44b3a53
--- /dev/null
+++ b/scripts/health/hahealth.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+import os
+import crm_script as crm
+
+if not os.path.isfile('/usr/sbin/crm'):
+    # crm not installed
+    crm.exit_ok({'status': 'crm not installed'})
+
+def get_from_date():
+    rc, out, err = crm.call("date '+%F %H:%M' --date='1 day ago'", shell=True)
+    return out.strip()
+
+def create_report():
+    cmd = ['crm', 'report',
+           '-f', get_from_date(),
+           '-D', '-Z', 'health-report']
+    rc, out, err = crm.call(cmd, shell=False)
+    return rc == 0
+
+if not create_report():
+    crm.exit_ok({'status': 'Failed to create report'})
+
+def extract_report():
+    rc, out, err = crm.call(['tar', 'xjf', 'health-report.tar.bz2'], shell=False)
+    return rc == 0
+
+if not extract_report():
+    crm.exit_ok({'status': 'Failed to extract report'})
+
+analysis = ''
+if os.path.isfile('health-report/analysis.txt'):
+    analysis = open('health-report/analysis.txt').read()
+
+crm.exit_ok({'status': 'OK', 'analysis': analysis})
diff --git a/scripts/health/main.yml b/scripts/health/main.yml
new file mode 100644
index 0000000..e79c82a
--- /dev/null
+++ b/scripts/health/main.yml
@@ -0,0 +1,12 @@
+---
+- name: Check the health of the cluster
+  description: >
+    Runs various checks to verify the health of the cluster nodes
+  parameters: []
+  steps:
+    - name: Collect cluster information
+      collect: collect.py
+    - name: Run HA health check
+      apply_local: hahealth.py
+    - name: Report cluster state
+      report: report.py
diff --git a/scripts/health/report.py b/scripts/health/report.py
new file mode 100755
index 0000000..ecca270
--- /dev/null
+++ b/scripts/health/report.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+import crm_script
+data = crm_script.get_input()
+health_report = data[1]
+
+print "Processing collected information..."
+
+CORE_PACKAGES = ['corosync', 'pacemaker', 'resource-agents']
+
+warnings = []
+errors = []
+
+def warn(fmt, *args):
+    warnings.append(fmt % args)
+
+def error(fmt, *args):
+    errors.append(fmt % args)
+
+# sort {package: {version: [host]}}
+rpm_versions = {}
+
+LOW_UPTIME = 60.0
+HIGH_LOAD = 1.0
+
+for node, info in health_report.iteritems():
+    if node != info['system']['hostname']:
+        error("Hostname mismatch: %s is not %s" %
+              (node, info['system']['hostname']))
+
+    if float(info['system']['uptime']) < LOW_UPTIME:
+        warn("%s: Uptime is low: %ss" % (node, info['system']['uptime']))
+
+    if float(info['system']['loadavg']) > HIGH_LOAD:
+        warn("%s: 15 minute load average is %s" % (node, info['system']['loadavg']))
+
+    for rpm in info['rpm']:
+        if 'error' in rpm:
+            if rpm['name'] not in rpm_versions:
+                rpm_versions[rpm['name']] = {rpm['error']: [node]}
+            else:
+                versions = rpm_versions[rpm['name']]
+                if rpm['error'] in versions:
+                    versions[rpm['error']].append(node)
+                else:
+                    versions[rpm['error']] = [node]
+        else:
+            if rpm['name'] not in rpm_versions:
+                rpm_versions[rpm['name']] = {rpm['version']: [node]}
+            else:
+                versions = rpm_versions[rpm['name']]
+                if rpm['version'] in versions:
+                    versions[rpm['version']].append(node)
+                else:
+                    versions[rpm['version']] = [node]
+    for disk, use in info['disk']:
+        use = int(use[:-1])
+        if use > 90:
+            warn("On %s, disk %s usage is %s%%", node, disk, use)
+
+    for logfile, state in info['logrotate'].iteritems():
+        if not state:
+            warn("%s: No log rotation configured for %s" % (node, logfile))
+
+for cp in CORE_PACKAGES:
+    if cp not in rpm_versions:
+        error("Core package '%s' not installed on any node", cp)
+
+for name, versions in rpm_versions.iteritems():
+    if len(versions) > 1:
+        desc = ', '.join('%s (%s)' % (v, ', '.join(nodes)) for v, nodes in versions.items())
+        warn("Package %s: Versions differ! %s", name, desc)
+
+    all_hosts = set(sum([hosts for hosts in versions.values()], []))
+    for node in health_report.keys():
+        if len(all_hosts) > 0 and node not in all_hosts:
+            warn("Package '%s' not installed on host '%s'" % (name, node))
+
+
+def compare_system(systems):
+    def check(value, msg):
+        vals = set([system[value] for host, system in systems])
+        if len(vals) > 1:
+            info = ', '.join('%s: %s' % (h, system[value]) for h, system in systems)
+            warn("%s: %s" % (msg, info))
+
+    check('machine', 'Architecture differs')
+    check('release', 'Kernel release differs')
+    check('distname', 'Distribution differs')
+    check('distver', 'Distribution version differs')
+    #check('version', 'Kernel version differs')
+
+def compare_files(systems):
+    keys = set()
+    for host, files in systems:
+        keys.update(files.keys())
+    for filename in keys:
+        vals = set([files.get(filename) for host, files in systems])
+        if len(vals) > 1:
+            info = ', '.join('%s: %s' % (h, files.get(filename)) for h, files in systems)
+            warn("%s: %s" % ("Files differ", info))
+
+compare_system((h, info['system']) for h, info in health_report.iteritems())
+compare_files((h, info['files']) for h, info in health_report.iteritems())
+
+if crm_script.output(2):
+    report = crm_script.output(2)
+    status = report.get('status')
+    analysis = report.get('analysis')
+    if status and not analysis:
+        warn("Cluster report: %s" % (status))
+    elif analysis:
+        print "INFO: Cluster report:"
+        print analysis
+    else:
+        warn("No cluster report generated")
+
+if errors:
+    for e in errors:
+        print "ERROR:", e
+if warnings:
+    for w in warnings:
+        print "WARNING:", w
+
+if not errors and not warnings:
+    print "No issues found."
diff --git a/scripts/init/Makefile.am b/scripts/init/Makefile.am
new file mode 100644
index 0000000..3e05afb
--- /dev/null
+++ b/scripts/init/Makefile.am
@@ -0,0 +1,28 @@
+#
+# crmsh init script
+#
+# Copyright (C) 2014 Kristoffer Gronlund
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+scriptinitdir 		= $(datadir)/@PACKAGE@/scripts/init
+
+scriptinit_DATA	= main.yml corosync.conf.template basic.cib.template
+scriptinit_SCRIPTS	= collect.py verify.py configure.py authkey.py init.py
+
+EXTRA_DIST	= $(scriptinit_DATA) $(scriptinit_SCRIPTS)
+
diff --git a/scripts/init/authkey.py b/scripts/init/authkey.py
new file mode 100755
index 0000000..d6af222
--- /dev/null
+++ b/scripts/init/authkey.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+import crm_script
+import os
+import stat
+
+host = crm_script.host()
+others = crm_script.output(2).keys()
+others.remove(host)
+
+COROSYNC_AUTH = '/etc/corosync/authkey'
+COROSYNC_CONF = '/etc/corosync/corosync.conf'
+
+
+def make_opts():
+    from psshlib import api as pssh
+    opts = pssh.Options()
+    opts.timeout = 60
+    opts.recursive = True
+    opts.user = 'root'
+    opts.ssh_options += ['PasswordAuthentication=no',
+                         'StrictHostKeyChecking=no',
+                         'ControlPersist=no']
+    return opts
+
+
+def check_results(pssh, results):
+    failures = []
+    for host, result in results.items():
+        if isinstance(result, pssh.Error):
+            failures.add("%s: %s" % (host, str(result)))
+    if failures:
+        crm_script.exit_fail(', '.join(failures))
+
+
+def gen_authkey():
+    if not os.path.isfile(COROSYNC_AUTH):
+        rc, out, err = crm_script.sudo_call(['corosync-keygen', '-l'])
+        if rc != 0:
+            crm_script.exit_fail("Error generating key: %s" % (err))
+    elif stat.S_IMODE(os.stat(COROSYNC_AUTH)[stat.ST_MODE]) != stat.S_IRUSR:
+        os.chmod(COROSYNC_AUTH, stat.S_IRUSR)
+
+
+def run_copy():
+    try:
+        from psshlib import api as pssh
+    except ImportError:
+        crm_script.exit_fail("Command node needs pssh installed")
+    opts = make_opts()
+    results = pssh.copy(others, COROSYNC_AUTH, COROSYNC_AUTH, opts)
+    check_results(pssh, results)
+    results = pssh.call(others,
+                        "chown root:root %s;chmod 400 %s" % (COROSYNC_AUTH, COROSYNC_AUTH),
+                        opts)
+    check_results(pssh, results)
+
+
+if __name__ == "__main__":
+    gen_authkey()
+    if others:
+        run_copy()
diff --git a/scripts/init/basic.cib.template b/scripts/init/basic.cib.template
new file mode 100644
index 0000000..2b8ceae
--- /dev/null
+++ b/scripts/init/basic.cib.template
@@ -0,0 +1,13 @@
+# Note: STONITH must be enabled for proper functionality!
+property $id="cib-bootstrap-options" \
+    stonith-enabled="false" \
+    no-quorum-policy="%(no_quorum_policy)s" \
+    placement-strategy="balanced"
+
+op_defaults $id="op-options" \
+    timeout="600" \
+    record-pending="true"
+
+rsc_defaults $id="rsc-options" \
+    resource-stickiness="1" \
+    migration-threshold="3"
diff --git a/scripts/init/collect.py b/scripts/init/collect.py
new file mode 100755
index 0000000..4d3f176
--- /dev/null
+++ b/scripts/init/collect.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python
+import crm_script
+import crm_init
+try:
+    crm_script.exit_ok(crm_init.info())
+except Exception, e:
+    crm_script.exit_fail(str(e))
diff --git a/scripts/init/configure.py b/scripts/init/configure.py
new file mode 100755
index 0000000..1b87ecf
--- /dev/null
+++ b/scripts/init/configure.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+import sys
+import os
+import crm_script
+import crm_init
+
+
+def _authorize_key(keypath):
+    "add key to authorized_keys"
+    pubkeypath = ''.join([keypath, '.pub'])
+    if os.path.exists('/root/.ssh/authorized_keys'):
+        pubkey = open(pubkeypath).read()
+        if pubkey not in open('/root/.ssh/authorized_keys').read():
+            crm_script.sudo_call("cat %s >> /root/.ssh/authorized_keys" % (pubkeypath))
+    else:
+        crm_script.sudo_call(["cp", pubkeypath, '/root/.ssh/authorized_keys'])
+
+
+def run_ssh():
+    try:
+        crm_script.service('sshd', 'start')
+        rc, _, _ = crm_script.sudo_call(["mkdir", "-m", "700", "-p", "/root/.ssh"])
+        if rc != 0:
+            crm_script.exit_fail("Failed to create /root/.ssh directory")
+        keypath = None
+        for key in ('id_rsa', 'id_dsa', 'id_ecdsa'):
+            if os.path.exists(os.path.join('/root/.ssh', key)):
+                keypath = os.path.join('/root/.ssh', key)
+                break
+        if not keypath:
+            keypath = os.path.join('/root/.ssh', 'id_rsa')
+            keygen = ['ssh-keygen', '-q', '-f', keypath,
+                      '-C', 'Cluster Internal', '-N', '']
+            rc, out, err = crm_script.sudo_call(keygen)
+            if rc != 0:
+                crm_script.exit_fail("Failed to generate SSH key")
+        _authorize_key(keypath)
+        crm_script.exit_ok(True)
+    except IOError, e:
+        crm_script.exit_fail(str(e))
+
+
+def run_install():
+    packages = ['cluster-glue', 'corosync', 'crmsh', 'pacemaker', 'resource-agents']
+    crm_init.install_packages(packages)
+    crm_init.configure_firewall()
+    crm_script.exit_ok(True)
+
+
+def make_bindnetaddr():
+    host = crm_script.host()
+    hostinfo = crm_script.output(2)[host]
+    ba = crm_script.param('bindnetaddr')
+    if ba:
+        return ba
+
+    # if not, try to figure it out based
+    # on preferred interface
+    iface = crm_script.param('iface')
+    if isinstance(iface, dict):
+        iface = iface[host]
+    interfaces = hostinfo['net']['interfaces']
+    if not iface:
+        for info in interfaces:
+            if info.get('Destination') == '0.0.0.0':
+                iface = info.get('Iface')
+                break
+    try:
+        for info in interfaces:
+            if info.get('Iface') != iface:
+                continue
+            dst = info.get('Destination')
+            if dst != '0.0.0.0':
+                return info.get('Destination')
+    except:
+        pass
+    crm_script.fail_exit("Could not discover appropriate bindnetaddr")
+
+
+# configure corosync
+def run_corosync():
+    # create corosync.conf
+
+    nodelist = crm_script.output(2).keys()
+    nodelist_txt = ""
+    for i, node in enumerate(nodelist):
+        nodelist_txt += """
+    node {
+        ring0_addr: %s
+        nodeid: %s
+    }
+""" % (node, i + 1)
+
+    quorum_txt = ""
+    if len(nodelist) == 1:
+        quorum_txt = ''
+    if len(nodelist) == 2:
+        quorum_txt = """    two_node: 1
+"""
+    else:
+        quorum_txt = """    provider: corosync_votequorum
+    expected_votes: %s
+""" % ((len(nodelist) / 2) + 1)
+
+    try:
+        crm_script.save_template('./corosync.conf.template',
+                                 '/etc/corosync/corosync.conf',
+                                 bindnetaddr=make_bindnetaddr(),
+                                 mcastaddr=crm_script.param('mcastaddr'),
+                                 mcastport=crm_script.param('mcastport'),
+                                 transport=crm_script.param('transport'),
+                                 nodelist=nodelist_txt,
+                                 quorum=quorum_txt)
+    except Exception, e:
+        crm_script.exit_fail(str(e))
+
+    # start cluster
+    rc, out, err = crm_script.call(['crm', 'cluster', 'start'])
+    if rc != 0:
+        crm_script.exit_fail("Failed to start cluster: %s" % (err))
+
+    crm_script.exit_ok(True)
+
+if __name__ == "__main__":
+    if len(sys.argv) < 2:
+        crm_script.exit_fail("Missing argument to configure.py")
+    elif sys.argv[1] == 'ssh':
+        run_ssh()
+    elif sys.argv[1] == 'install':
+        run_install()
+    elif sys.argv[1] == 'corosync':
+        run_corosync()
+    else:
+        crm_script.exit_fail("Bad argument to configure.py: %s" % (sys.argv[1]))
diff --git a/scripts/init/corosync.conf.template b/scripts/init/corosync.conf.template
new file mode 100644
index 0000000..3fab031
--- /dev/null
+++ b/scripts/init/corosync.conf.template
@@ -0,0 +1,46 @@
+# Please read the corosync.conf.5 manual page
+totem {
+	version: 2
+
+	crypto_cipher: none
+	crypto_hash: none
+
+	interface {
+		ringnumber: 0
+		bindnetaddr: %(bindnetaddr)s
+		mcastaddr: %(mcastaddr)s
+		mcastport: %(mcastport)s
+		ttl: 1
+	}
+	transport: %(transport)s
+}
+
+logging {
+	fileline: off
+	to_logfile: no
+	to_syslog: yes
+	logfile: /var/log/cluster/corosync.log
+	debug: off
+	timestamp: on
+	logger_subsys {
+		subsys: QUORUM
+		debug: off
+	}
+}
+
+nodelist {
+%(nodelist)s
+
+#    node {
+#		ring0_addr: 192.168.122.120
+#		nodeid: 1
+#	}
+}
+
+quorum {
+	# Enable and configure quorum subsystem (default: off)
+	# see also corosync.conf.5 and votequorum.5
+	#provider: corosync_votequorum
+	#expected_votes: 2
+%(quorum)s
+}
\ No newline at end of file
diff --git a/scripts/init/init.py b/scripts/init/init.py
new file mode 100755
index 0000000..f101dbe
--- /dev/null
+++ b/scripts/init/init.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+import crm_script
+
+rc, _, err = crm_script.call(['crm', 'cluster', 'wait_for_startup', '30'])
+if rc != 0:
+    crm_script.exit_fail("Cluster not responding")
+
+def check_for_primitives():
+    rc, out, err = crm_script.call("crm configure show type:primitive | grep primitive", shell=True)
+    if rc == 0 and out:
+        return True
+    return False
+
+if check_for_primitives():
+    crm_script.debug("Joined existing cluster - will not reconfigure")
+    crm_script.exit_ok(True)
+
+try:
+    nodelist = crm_script.param('nodes')
+    if len(nodelist) < 3:
+        policy = 'ignore'
+    else:
+        policy = 'stop'
+    crm_script.save_template('./basic.cib.template',
+                             './basic.cib',
+                             no_quorum_policy=policy)
+except IOError, e:
+    crm_script.exit_fail("IO error: %s" % (str(e)))
+except ValueError, e:
+    crm_script.exit_fail("Value error: %s" % (str(e)))
+
+rc, _, err = crm_script.call(['crm', 'configure', 'load', 'replace', './basic.cib'])
+if rc != 0:
+    crm_script.exit_fail("Failed to load CIB configuration: %s" % (err))
+
+crm_script.exit_ok(True)
diff --git a/scripts/init/main.yml b/scripts/init/main.yml
new file mode 100644
index 0000000..ef5d35b
--- /dev/null
+++ b/scripts/init/main.yml
@@ -0,0 +1,52 @@
+---
+- name: Initialize a new cluster
+  description: >
+    Initializes a new cluster on the nodes provided. Will try to 
+    configure SSH if not already configured, and install missing 
+    packages.
+
+    A more user-friendly interface to this script is provided by the 
+    cluster init command.
+  parameters:
+    - name: iface
+      description: "Use the given interface. Try to auto-detect interface by default."
+      default: ""
+
+    - name: transport
+      description: "Corosync transport (mcast or udpu)"
+      default: "udpu"
+
+    - name: bindnetaddr
+      description: "Network address to bind to (e.g.: 192.168.1.0)"
+      default: ""
+
+    - name: mcastaddr
+      description: "Multicast address (e.g.: 239.x.x.x)"
+      default: ""
+
+    - name: mcastport
+      description: "Multicast port"
+      default: 5405
+
+  steps:
+    - name: Configure SSH
+      apply_local: configure.py ssh
+
+    - name: Check state of nodes
+      collect: collect.py
+
+    - name: Verify parameters
+      validate: verify.py
+
+    - name: Install packages
+      apply: configure.py install
+
+    - name: Generate corosync authkey
+      apply_local: authkey.py
+
+    - name: Configure cluster nodes
+      apply: configure.py corosync
+
+    - name: Initialize cluster
+      apply_local: init.py
+
diff --git a/scripts/init/verify.py b/scripts/init/verify.py
new file mode 100755
index 0000000..f63f5e4
--- /dev/null
+++ b/scripts/init/verify.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+import crm_script
+import crm_init
+
+
+def select_interfaces(user_iface, data):
+    selections = dict([(host, user_iface) for host in data.keys()])
+    if not user_iface:
+        for host, info in data.iteritems():
+            for i in info['net']['interfaces']:
+                if i.get('Destination') == '0.0.0.0':
+                    selections[host] = i['Iface']
+                    break
+
+    def invalid(host, iface):
+        for i in data[host]['net']['interfaces']:
+            if i['Iface'] == iface:
+                return False
+        return True
+
+    for host, iface in selections.iteritems():
+        if not iface or invalid(host, iface):
+            crm_script.exit_fail("No usable network interface on %s: %s" % (host, iface))
+
+    return user_iface
+
+
+def make_mcastaddr():
+    import random
+    random.seed()
+    b, c, d = random.randint(1, 254), random.randint(1, 254), random.randint(1, 254)
+    return "%d.%d.%d.%d" % (239, b, c, d)
+
+try:
+    data = crm_script.output(2)
+
+    crm_init.verify(data)
+
+    ret = {}
+    ret['iface'] = select_interfaces(crm_script.param('iface'), data)
+
+    if not crm_script.param('mcastaddr'):
+        ret['mcastaddr'] = make_mcastaddr()
+
+    crm_script.exit_ok(ret)
+
+except Exception, e:
+    crm_script.exit_fail("Verification failed: %s" % (e))
diff --git a/scripts/remove/Makefile.am b/scripts/remove/Makefile.am
new file mode 100644
index 0000000..969a729
--- /dev/null
+++ b/scripts/remove/Makefile.am
@@ -0,0 +1,28 @@
+#
+# crmsh init script
+#
+# Copyright (C) 2014 Kristoffer Gronlund
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+scriptremovedir 		= $(datadir)/@PACKAGE@/scripts/remove
+
+scriptremove_DATA	= main.yml
+scriptremove_SCRIPTS	= remove.py
+
+EXTRA_DIST	= $(scriptremove_DATA) $(scriptremove_SCRIPTS)
+
diff --git a/scripts/remove/main.yml b/scripts/remove/main.yml
new file mode 100644
index 0000000..41af2e3
--- /dev/null
+++ b/scripts/remove/main.yml
@@ -0,0 +1,20 @@
+---
+- name: Remove node from cluster
+  description: >
+    Removes the node from the cluster. Resources currently
+    allocated to the node will be moved elsewhere if possible.
+
+    A more usable interface to this script is provided via
+    the cluster remove command.
+  parameters:
+    - name: node
+      description: Node to remove from the cluster
+  steps:
+    - name: Check nodes
+      collect: remove.py collect
+
+    - name: Validate parameters
+      validate: remove.py validate
+
+    - name: Remove node from cluster
+      apply_local: remove.py apply
diff --git a/scripts/remove/remove.py b/scripts/remove/remove.py
new file mode 100755
index 0000000..2dbf72c
--- /dev/null
+++ b/scripts/remove/remove.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+import sys
+import crm_script
+
+host = crm_script.host()
+remove_nodes = crm_script.param('node').split(',')
+
+
+def run_collect():
+    crm_script.exit_ok(host)
+
+
+def run_validate():
+    data = crm_script.output(1)
+    for node in remove_nodes:
+        if data.get(node) != node:
+            crm_script.exit_fail("%s not found or not responding: %s" % (node, data.get(node)))
+        if host == node:
+            crm_script.exit_fail("Call from another node: %s = %s" % (node, host))
+    crm_script.exit_ok(host)
+
+
+def run_apply():
+    for node in remove_nodes:
+        rc, out, err = crm_script.call(['ssh',
+                                        '-o', 'PasswordAuthentication=no',
+                                        'root@%s' % (node),
+                                        'systemctl stop corosync.service'])
+        if rc != 0:
+            crm_script.exit_fail("Failed to stop corosync on %s: %s" % (node, err))
+
+        rc, out, err = crm_script.call(['crm', 'node', 'delete', node])
+        if rc != 0:
+            crm_script.exit_fail("Failed to remove %s from CIB: %s" % (node, err))
+
+    crm_script.exit_ok({"removed": remove_nodes})
+
+if __name__ == "__main__":
+    if len(sys.argv) < 2 or sys.argv[1] == 'collect':
+        run_collect()
+    elif sys.argv[1] == 'validate':
+        run_validate()
+    elif sys.argv[1] == 'apply':
+        run_apply()
+    else:
+        crm_script.exit_fail("Unknown argument: %s" % sys.argv[1])
diff --git a/templates/Makefile.am b/templates/Makefile.am
new file mode 100644
index 0000000..c31ca3c
--- /dev/null
+++ b/templates/Makefile.am
@@ -0,0 +1,26 @@
+#
+# crmsh templates
+#
+# Copyright (C) 2008 Andrew Beekhof
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+templatedir 		= $(datadir)/@PACKAGE@/templates
+
+template_DATA	= apache virtual-ip filesystem ocfs2 clvm gfs2-base gfs2 sbd
+
+EXTRA_DIST	= $(template_DATA)
diff --git a/templates/apache b/templates/apache
new file mode 100644
index 0000000..955257b
--- /dev/null
+++ b/templates/apache
@@ -0,0 +1,61 @@
+%name apache
+
+# Copyright (C) 2009 Dejan Muhamedagic
+#
+# License: GNU General Public License (GPL)
+
+# Apache web server
+#
+# This template generates a single primitive resource of type apache
+
+%depends_on virtual-ip
+%suggests filesystem
+
+# NB:
+# The apache RA monitor operation requires the status module to
+# be loaded and access to its page (/server-status) allowed from
+# localhost (127.0.0.1). Typically, the status module is not
+# loaded by default. How to enable it depends on your
+# distribution. For instance, on recent openSUSE or SLES
+# releases, it is enough to add word "status" to the list in
+# variable APACHE_MODULES in file /etc/sysconfig/apache2 and then
+# start and stop apache once using rcapache2.
+
+%required
+
+# Name the apache resource
+# For example, to name the resource web-1, edit the line below
+# as follows:
+# %% id web-1
+%% id
+
+# The full pathname of the Apache configuration file
+# Example:
+# %% configfile /etc/apache2/httpd.conf
+%% configfile 
+
+%optional
+
+# Extra options to apply when starting apache. See man httpd(8).
+
+%% options 
+
+# Files (one or more) which contain extra environment variables,
+# such as /etc/apache2/envvars
+
+%% envfiles 
+
+%generate
+
+primitive %apache ocf:heartbeat:apache
+	params configfile=%_:configfile
+	opt options=%_:options
+	opt envfiles=%_:envfiles
+
+monitor %apache 120s:60s
+
+group %_:id
+	%if %filesystem
+		%filesystem
+	%fi
+	%apache %virtual-ip
diff --git a/templates/clvm b/templates/clvm
new file mode 100644
index 0000000..96c4fff
--- /dev/null
+++ b/templates/clvm
@@ -0,0 +1,59 @@
+%name clvm
+
+# Copyright (C) 2009 Dejan Muhamedagic
+#
+# License: GNU General Public License (GPL)
+
+# Cluster-aware lvm (cloned)
+#
+# This template generates a cloned instance of clvm and one
+# volume group
+#
+# NB: You need just one clvm, regardless of how many volume
+# groups. In other words, you can use this template only for one
+# volume group and to make another one, you'll have to edit the
+# resulting configuration yourself.
+
+%required
+
+# Name the volume group (for example: vg-1)
+# The LVM resource will be in a cloned group with the rest
+# of the prerequisite resources. The clone is going to be named c-<id>
+# (e.g. c-vg-1)
+
+# For example, to name the resource vg-1, edit the line below
+# as follows:
+# %% id vg-1
+%% id 
+
+# The volume group name
+# Example:
+# %% volgrpname myvolgroup
+%% volgrpname 
+
+%generate
+
+primitive %_:id ocf:heartbeat:LVM
+	params volgrpname="%_:volgrpname"
+	op start timeout=60s
+	op stop timeout=60s
+	op monitor interval=30s timeout=60s
+
+primitive dlm ocf:pacemaker:controld
+	op start timeout=90s
+	op stop timeout=100s
+
+primitive clvm ocf:lvm2:clvmd
+	params daemon_timeout="30"
+	op start timeout=90s
+	op stop timeout=100s
+
+primitive cmirror ocf:lvm2:cmirrord
+	params daemon_timeout="30"
+	op start timeout=90s
+	op stop timeout=100s
+
+group g-%_:id dlm clvm cmirror %_:id
+
+clone c-%_:id g-%_:id
+	meta interleave="true" ordered="true"
diff --git a/templates/filesystem b/templates/filesystem
new file mode 100644
index 0000000..2699699
--- /dev/null
+++ b/templates/filesystem
@@ -0,0 +1,44 @@
+%name filesystem
+
+# Copyright (C) 2009 Dejan Muhamedagic
+#
+# License: GNU General Public License (GPL)
+
+# Filesystem
+#
+# This template generates a single primitive resource of type
+# Filesystem
+
+%required
+
+# The name of block device for the filesystem, or -U, -L
+# options for mount, or NFS mount specification.
+# Example:
+# %% device /dev/hda
+%% device 
+
+# The mount point for the filesystem.
+# Example:
+# %% directory /mnt/fs
+%% directory 
+
+# The type of filesystem to be mounted.
+# Example:
+# %% fstype xfs
+%% fstype 
+
+%optional
+
+# Any extra options to be given as -o options to mount.
+# 
+# For bind mounts, add "bind" here and set fstype to "none".
+# We will do the right thing for options such as "bind,ro".
+%% options 
+
+%generate
+
+primitive %_ ocf:heartbeat:Filesystem
+	params
+	device=%_:device
+	directory=%_:directory
+	fstype=%_:fstype
diff --git a/templates/gfs2 b/templates/gfs2
new file mode 100644
index 0000000..244befd
--- /dev/null
+++ b/templates/gfs2
@@ -0,0 +1,74 @@
+%name gfs2
+
+# Copyright (C) 2009 Andrew Beekhof
+#
+# License: GNU General Public License (GPL)
+
+# gfs2 filesystem (cloned)
+#
+# This template generates a cloned instance of the ocfs2 filesystem
+#
+# The filesystem should be on the device, unless clvm is used
+# To use clvm, pull it along with this template:
+# new myfs ocfs2 clvm
+#
+# NB: You need just one dlm and o2cb, regardless of how many
+# filesystems. In other words, you can use this template only for
+# one filesystem and to make another one, you'll have to edit the
+# resulting configuration yourself.
+
+%depends_on gfs2-base
+%suggests clvm 
+
+%required
+
+# Name the gfs2 filesystem
+# (for example: bigfs)
+# NB: The clone is going to be named c-<id> (e.g. c-bigfs)
+# Example:
+# %% id bigfs
+%% id 
+
+# The mount point
+# Example:
+# %% directory /mnt/bigfs
+%% directory 
+
+# The device
+
+%% device 
+
+# optional parameters for the gfs2 filesystem
+
+%optional
+
+# mount options
+
+%% options
+
+%generate
+
+primitive %_:id ocf:heartbeat:Filesystem
+	params
+		directory="%_:directory"
+		fstype="gfs2"
+		device="%_:device"
+		opt options="%_:options"
+
+monitor %_:id 20:40
+
+clone c-%_:id %_:id
+	meta interleave="true" ordered="true"
+
+colocation colo-%_:id-gfs inf: c-%_:id gfs-clone
+
+order order-%_:id-gfs inf: gfs-clone c-%_:id
+
+# if there's clvm, generate some constraints too
+#
+
+%if %clvm
+colocation colo-%_:id-%clvm:id inf: c-%_:id c-%clvm:id
+
+order order-%_:id-%clvm:id inf: c-%clvm:id c-%_:id
+%fi
diff --git a/templates/gfs2-base b/templates/gfs2-base
new file mode 100644
index 0000000..d385ed4
--- /dev/null
+++ b/templates/gfs2-base
@@ -0,0 +1,46 @@
+%name gfs2-base
+
+# Copyright (C) 2009 Andrew Beekhof
+#
+# License: GNU General Public License (GPL)
+
+# gfs2 filesystem base (cloned)
+#
+# This template generates a cloned instance of the ocfs2 filesystem
+#
+# The filesystem should be on the device, unless clvm is used
+# To use clvm, pull it along with this template:
+# new myfs ocfs2 clvm
+#
+# NB: You need just one dlm and o2cb, regardless of how many
+# filesystems. In other words, you can use this template only for
+# one filesystem and to make another one, you'll have to edit the
+# resulting configuration yourself.
+
+%suggests clvm
+%required
+
+%generate
+
+primitive dlm ocf:pacemaker:controld
+
+clone dlm-clone dlm
+	meta interleave="true" ordered="true"
+
+primitive gfs-controld ocf:pacemaker:controld
+
+clone gfs-clone gfs-controld
+	meta interleave="true" ordered="true"
+
+colocation colo-gfs-dlm inf: gfs-clone dlm-clone
+
+order order-gfs-dlm inf: dlm-clone gfs-clone
+
+# if there's clvm, generate some constraints too
+#
+
+%if %clvm
+colocation colo-clvm-dlm inf: clvm-clone dlm-clone
+
+order order-clvm-dlm inf: dlm-clone clvm-clone
+%fi
diff --git a/templates/ocfs2 b/templates/ocfs2
new file mode 100644
index 0000000..ae07e8b
--- /dev/null
+++ b/templates/ocfs2
@@ -0,0 +1,61 @@
+%name ocfs2
+
+# Copyright (C) 2009 Dejan Muhamedagic
+#
+# License: GNU General Public License (GPL)
+
+# ocfs2 filesystem (cloned)
+#
+# This template generates a cloned instance of the ocfs2 filesystem
+#
+# NB: You need only one dlm, regardless of how many
+# filesystems. In other words, you can use this template only for
+# one filesystem and to make another one, you'll have to edit the
+# resulting configuration yourself.
+
+%required
+
+# Name the ocfs2 filesystem (for example: bigfs)
+# Example:
+# %% id bigfs
+%% id 
+
+# The mount point
+# Example:
+# %% directory /mnt/bigfs
+%% directory 
+
+# The device
+
+%% device 
+
+# optional parameters for the ocfs2 filesystem
+
+%optional
+
+# mount options
+
+%% options
+
+%generate
+
+primitive %_:id ocf:heartbeat:Filesystem
+	params
+		directory="%_:directory"
+		fstype="ocfs2"
+		device="%_:device"
+		opt options="%_:options"
+	op start timeout=60s
+	op stop timeout=60s
+
+monitor %_:id 20s:40s
+
+primitive dlm ocf:pacemaker:controld
+	op start timeout=90s
+	op stop timeout=100s
+	op monitor interval=60s timeout=60s
+
+clone base-%_:id dlm meta interleave="true"
+clone clusterfs-%_:id clusterfs meta interleave="true"
+order base-then-clusterfs-%_:id inf: base-%_:id clusterfs-%_:id
+colocation clusterfs-with-base-%_:id inf: clusterfs-%_:id base-%_:id
diff --git a/templates/sbd b/templates/sbd
new file mode 100644
index 0000000..9ab201a
--- /dev/null
+++ b/templates/sbd
@@ -0,0 +1,34 @@
+%name sbd
+
+# Copyright (C) 2009 Dejan Muhamedagic
+#
+# License: GNU General Public License (GPL)
+
+# Shared storage based fencing.
+#
+# This template generates a single instance of external/sbd.
+#
+# There is quite a bit more to do to make this stonith operational.
+# See http://www.linux-ha.org/wiki/SBD_Fencing for information.
+#
+
+%required
+
+# The resource id (name).
+# Example:
+# %% id stonith-sbd
+%% id 
+
+# Name of the device (shared disk).
+# NB: Make sure that the device remains the same on reboots. It's
+# preferable to use udev generated names rather than the usual
+# /dev/sd?
+# %% sbd_device /dev/sda
+%% sbd_device 
+
+%generate
+
+primitive %_:id stonith:external/sbd
+	params sbd_device="%_:sbd_device"
+	op monitor interval=15s timeout=60s
+	op start timeout=60s
diff --git a/templates/virtual-ip b/templates/virtual-ip
new file mode 100644
index 0000000..c6ae46e
--- /dev/null
+++ b/templates/virtual-ip
@@ -0,0 +1,39 @@
+%name virtual-ip
+
+# Copyright (C) 2009 Dejan Muhamedagic
+#
+# License: GNU General Public License (GPL)
+
+# Virtual IP address
+#
+# This template generates a single primitive resource of type IPaddr
+
+%required
+
+# Specify an IP address
+# (for example: 192.168.1.101)
+# Example:
+# %% ip 192.168.1.101
+
+%% ip 
+
+%optional
+
+# If your network has a mask different from its class mask, then
+# specify it here either in CIDR format or as a dotted quad
+# (for example: 24 or 255.255.255.0)
+# Example:
+# %% netmask 24
+
+%% netmask 
+
+# Need LVS support? Set this to true then.
+
+%% lvs_support 
+
+%generate
+
+primitive %_ ocf:heartbeat:IPaddr
+	params ip=%_:ip
+	opt cidr_netmask=%_:netmask
+	opt lvs_support=%_:lvs_support
diff --git a/test/Makefile.am b/test/Makefile.am
new file mode 100644
index 0000000..0ccc59d
--- /dev/null
+++ b/test/Makefile.am
@@ -0,0 +1,28 @@
+#
+# Author: Sun Jiang Dong <sunjd at cn.ibm.com>
+# Copyright (c) 2004 International Business Machines
+#
+# 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.
+#
+MAINTAINERCLEANFILES = Makefile.in
+
+SUBDIRS = testcases cibtests
+
+testdir		= 	$(datadir)/$(PACKAGE)/tests
+test_SCRIPTS	=	regression.sh evaltest.sh cib-tests.sh
+test_DATA	=	README.regression defaults descriptions \
+			crm-interface history-test.tar.bz2
+#				shouldn't need this, but we do for some versions of autofoo tools
+EXTRA_DIST	=	$(test_SCRIPTS) $(test_DATA)
diff --git a/test/README.regression b/test/README.regression
new file mode 100644
index 0000000..839ce59
--- /dev/null
+++ b/test/README.regression
@@ -0,0 +1,154 @@
+CRM shell regression tests
+
+* WARNING * WARNING * WARNING * WARNING * WARNING * WARNING *
+* 
+* evaltest.sh uses eval to an extent you don't really want to
+* know about. Beware. Beware twice. Any input from the testcases
+* directory is considered to be trusted. So, think twice before
+* devising your tests lest you kill your precious data. Got it?
+* Good.
+*
+* Furthermore, we are deliberately small on testing the user
+* input and no one should try to predict what is to happen on
+* random input from the testcases.
+* 
+* WARNING * WARNING * WARNING * WARNING * WARNING * WARNING *
+
+Manifest
+
+	regression.sh: the top level program
+	evaltest.sh: the engine test engine
+
+	crm-interface: interface to crm
+	descriptions: describe what we are about to do
+	defaults: the default settings for test commands
+
+	testcases/: here are the testcases and filters
+	crmtestout/: here goes the output
+
+All volatile data lives in the testcases/ directory.
+
+NB: You should never ever need to edit regression.sh and
+evaltest.sh. If you really have to, please talk to me and I will
+try to fix it so that you do not have to.
+
+Please write new test cases. The more the merrier :)
+
+Usage
+
+The usage is:
+
+	./regression.sh ["prepare"] ["set:"<setname>|<testcase>]
+
+Test cases are collected in test sets. The default test set is
+basicset and running regression.sh without arguments will do all
+tests from that set.
+
+To show progress, for each test a '.' is printed. Once all tests
+have been evaluated, the output is checked against the expect
+file. If successful, "PASS" is printed, otherwise "FAIL".
+
+Specifying "prepare" will make regression.sh create expect
+output files for the given set of tests or testcase.
+
+The script may start and stop lrmd and stonithd if they are not
+running to support the crm ra set of commands.
+
+The following files may be generated:
+
+	output/<testcase>.out: the output of the testcase
+	output/regression.out: the output of regression.sh
+	output/crm.out: the output of crm tools/lrmd/stonithd etc
+
+On success output from testcases is removed and regression.out is
+empty.
+
+Driving the test cases yourself
+
+evaltest.sh accepts input from stdin, evaluates it immediately,
+and prints results to stdout/stderr. One can perhaps get a better
+feeling of what's actually going on by running it interactively.
+
+Test cases
+
+Tests are mainly written in the crm shell language with some simple
+regression test directives (starting with '%' and
+session/show/showxml).
+
+Special operations
+
+There are special operations with which it is possible to change
+environment and do other useful things. All special ops start
+with the '%' sign and may be followed by additional parameters.
+
+%setenv
+	change the environment variable; see defaults for the
+	set of global variables and resetvars() in evaltest.sh
+
+%stop
+	skip the rest of the tests
+
+%extcheck
+	feed the output of the next test case to the specified
+	external program/filter; the program should either reside in
+	testcases/ or be in the PATH, i.e.
+	
+	%extcheck cat
+	
+	simulates a null op :)
+
+	see testcases/metadata for some examples
+
+%ext
+	run an external command provided in the rest of the line; for
+	example:
+
+	%ext date
+
+	would print the current time (not very useful for regression
+	testing).
+
+%repeat num
+	repeat the next test num times
+	there are several variables which are substituted in the test
+	lines, so that we can simulate a for loop:
+
+	s/%t/$test_cnt/g
+	s/%l/$line/g
+	s/%j/$job_cnt/g
+	s/%i/$repeat_cnt/g
+
+	for example, to add 10 resources:
+
+	%repeat 10
+	configure primitive p-%i ocf:pacemaker:Dummy
+
+Filters and other auxiliary files
+
+Some output is necessarily very volatile, such as time stamps.
+It is possible to specify a filter for each testcase to get rid
+of superfluous information. A filter is a filter in UNIX
+sense, it takes input from stdin and prints results to stdout.
+
+There is a common filter called very inventively
+testcases/common.filter which is applied to all test cases.
+
+Except files are a list of extended regular expressions fed to
+egrep(1). That way one can filter out lines which are not
+interesting. Again, the one applied to all is
+testcases/common.excl.
+
+A test may need an arbitrary script executed before or after the
+test itself in order to ascertain some state. The two scripts
+have extensions .pre and .post respectively. Their output is sent
+to /dev/null and the exit status ignored. 
+
+Finally, the daemon log files may be filtered using log_filter.
+
+The full collection of auxiliary files follows:
+
+	<TEST>.filter
+	<TEST>.excl
+	<TEST>.log_filter
+	<TEST>.pre
+	<TEST>.post
diff --git a/test/cib-tests.sh b/test/cib-tests.sh
new file mode 100755
index 0000000..89f6df5
--- /dev/null
+++ b/test/cib-tests.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+# Copyright (C) 2009 Lars Marowsky-Bree <lmb at suse.de>
+# 
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+BASE=${1:-`pwd`}/cibtests
+AUTOCREATE=1
+
+logt() {
+	local msg="$1"
+	echo $(date) "$msg" >>$LOGF
+	echo "$msg"
+}
+
+difft() {
+	crm_diff -V -u -o $1 -n $2
+}
+
+run() {
+	local cmd="$1"
+	local erc="$2"
+	local msg="$3"
+	local rc
+	local out
+
+	echo $(date) "$1" >>$LOGF
+	CIB_file=$CIB_file $1 >>$LOGF 2>&1 ; rc=$?
+	echo $(date) "Returned: $rc (expected $erc)" >>$LOGF
+	if [ $erc != "I" ]; then
+		if [ $rc -ne $erc ]; then
+			logt "$msg: FAILED ($erc != $rc)"
+			cat $LOGF
+			return 1
+		fi
+	fi
+	echo "$msg: ok"
+	return 0
+}
+
+runt() {
+	local T="$1"
+	local CIBE="$BASE/$(basename $T .input).exp.xml"
+	cp $BASE/shadow.base $CIB_file
+	run "crm" 0 "Running testcase: $T" <$T
+
+	# strip <cib> attributes from CIB_file
+	echo "<cib>" > $CIB_file.$$
+	tail -n +2 $CIB_file >> $CIB_file.$$
+	mv $CIB_file.$$ $CIB_file
+
+	local rc
+	if [ ! -e $CIBE ]; then
+		if [ "$AUTOCREATE" = "1" ]; then
+			logt "Creating new expected output for $T."
+			cp $CIB_file $CIBE
+			return 0
+		else
+			logt "$T: No expected output."
+			return 0
+		fi
+	fi
+
+	if ! crm_diff -u -o $CIBE -n $CIB_file >/dev/null 2>&1 ; then
+		logt "$T: XML: $CIBE does not match $CIB_file"
+		difft $CIBE $CIB_file
+		return 1
+	fi
+	return 0
+}
+
+LOGF=$(mktemp)
+export PATH=/usr/sbin:$PATH
+
+export CIB_file=$BASE/shadow.test
+
+failed=0
+for T in $(ls $BASE/*.input) ; do
+	runt $T
+	failed=$(($? + $failed))
+done
+
+if [ $failed -gt 0 ]; then
+	logt "$failed tests failed!"
+	echo "Log:" $LOGF "CIB:" $CIB_file
+	exit 1
+fi
+
+logt "All tests passed!"
+#rm $LOGF $CIB_file
+exit 0
+
diff --git a/test/cibtests/001.exp.xml b/test/cibtests/001.exp.xml
new file mode 100644
index 0000000..c76e9d1
--- /dev/null
+++ b/test/cibtests/001.exp.xml
@@ -0,0 +1,20 @@
+<cib>
+  <configuration>
+    <crm_config>
+      <cluster_property_set id="cib-bootstrap-options">
+        <nvpair name="stonith-enabled" value="false" id="cib-bootstrap-options-stonith-enabled"/>
+      </cluster_property_set>
+    </crm_config>
+    <nodes/>
+    <resources>
+      <primitive id="rsc_dummy" class="ocf" provider="heartbeat" type="Dummy">
+        <operations>
+          <op name="monitor" interval="30" id="rsc_dummy-monitor-30"/>
+        </operations>
+      </primitive>
+    </resources>
+    <constraints/>
+    <acls/>
+  </configuration>
+  <status/>
+</cib>
diff --git a/test/cibtests/001.input b/test/cibtests/001.input
new file mode 100644
index 0000000..8449a44
--- /dev/null
+++ b/test/cibtests/001.input
@@ -0,0 +1,6 @@
+configure
+property stonith-enabled=false
+primitive rsc_dummy ocf:heartbeat:Dummy
+monitor rsc_dummy 30
+commit
+quit
diff --git a/test/cibtests/002.exp.xml b/test/cibtests/002.exp.xml
new file mode 100644
index 0000000..1c9e497
--- /dev/null
+++ b/test/cibtests/002.exp.xml
@@ -0,0 +1,28 @@
+<cib>
+  <configuration>
+    <crm_config>
+      <cluster_property_set id="cib-bootstrap-options">
+        <nvpair name="stonith-enabled" value="false" id="cib-bootstrap-options-stonith-enabled"/>
+      </cluster_property_set>
+    </crm_config>
+    <nodes/>
+    <resources>
+      <clone id="testfs-clone">
+        <meta_attributes id="testfs-clone-meta_attributes">
+          <nvpair name="ordered" value="true" id="testfs-clone-meta_attributes-ordered"/>
+          <nvpair name="interleave" value="true" id="testfs-clone-meta_attributes-interleave"/>
+        </meta_attributes>
+        <primitive id="testfs" class="ocf" provider="heartbeat" type="Filesystem">
+          <instance_attributes id="testfs-instance_attributes">
+            <nvpair name="directory" value="/mnt" id="testfs-instance_attributes-directory"/>
+            <nvpair name="fstype" value="ocfs2" id="testfs-instance_attributes-fstype"/>
+            <nvpair name="device" value="/dev/sda1" id="testfs-instance_attributes-device"/>
+          </instance_attributes>
+        </primitive>
+      </clone>
+    </resources>
+    <constraints/>
+    <acls/>
+  </configuration>
+  <status/>
+</cib>
diff --git a/test/cibtests/002.input b/test/cibtests/002.input
new file mode 100644
index 0000000..a832f1b
--- /dev/null
+++ b/test/cibtests/002.input
@@ -0,0 +1,8 @@
+configure
+property stonith-enabled=false
+primitive testfs ocf:heartbeat:Filesystem \
+	params directory="/mnt" fstype="ocfs2" device="/dev/sda1"
+clone testfs-clone testfs \
+	meta ordered="true" interleave="true"
+commit
+quit
diff --git a/test/cibtests/003.exp.xml b/test/cibtests/003.exp.xml
new file mode 100644
index 0000000..ba1fb6f
--- /dev/null
+++ b/test/cibtests/003.exp.xml
@@ -0,0 +1,29 @@
+<cib>
+  <configuration>
+    <crm_config>
+      <cluster_property_set id="cib-bootstrap-options">
+        <nvpair name="stonith-enabled" value="false" id="cib-bootstrap-options-stonith-enabled"/>
+      </cluster_property_set>
+    </crm_config>
+    <nodes/>
+    <resources>
+      <clone id="testfs-clone">
+        <meta_attributes id="testfs-clone-meta_attributes">
+          <nvpair name="ordered" value="true" id="testfs-clone-meta_attributes-ordered"/>
+          <nvpair name="interleave" value="true" id="testfs-clone-meta_attributes-interleave"/>
+          <nvpair id="testfs-clone-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+        <primitive id="testfs" class="ocf" provider="heartbeat" type="Filesystem">
+          <instance_attributes id="testfs-instance_attributes">
+            <nvpair name="directory" value="/mnt" id="testfs-instance_attributes-directory"/>
+            <nvpair name="fstype" value="ocfs2" id="testfs-instance_attributes-fstype"/>
+            <nvpair name="device" value="/dev/sda1" id="testfs-instance_attributes-device"/>
+          </instance_attributes>
+        </primitive>
+      </clone>
+    </resources>
+    <constraints/>
+    <acls/>
+  </configuration>
+  <status/>
+</cib>
diff --git a/test/cibtests/003.input b/test/cibtests/003.input
new file mode 100644
index 0000000..129f025
--- /dev/null
+++ b/test/cibtests/003.input
@@ -0,0 +1,11 @@
+configure
+property stonith-enabled=false
+primitive testfs ocf:heartbeat:Filesystem \
+	params directory="/mnt" fstype="ocfs2" device="/dev/sda1"
+clone testfs-clone testfs \
+	meta ordered="true" interleave="true"
+commit
+up
+resource stop testfs-clone
+quit
+
diff --git a/test/cibtests/004.exp.xml b/test/cibtests/004.exp.xml
new file mode 100644
index 0000000..1829c6e
--- /dev/null
+++ b/test/cibtests/004.exp.xml
@@ -0,0 +1,29 @@
+<cib>
+  <configuration>
+    <crm_config>
+      <cluster_property_set id="cib-bootstrap-options">
+        <nvpair name="stonith-enabled" value="false" id="cib-bootstrap-options-stonith-enabled"/>
+      </cluster_property_set>
+    </crm_config>
+    <nodes/>
+    <resources>
+      <clone id="testfs-clone">
+        <meta_attributes id="testfs-clone-meta_attributes">
+          <nvpair name="ordered" value="true" id="testfs-clone-meta_attributes-ordered"/>
+          <nvpair name="interleave" value="true" id="testfs-clone-meta_attributes-interleave"/>
+          <nvpair id="testfs-clone-meta_attributes-target-role" name="target-role" value="Started"/>
+        </meta_attributes>
+        <primitive id="testfs" class="ocf" provider="heartbeat" type="Filesystem">
+          <instance_attributes id="testfs-instance_attributes">
+            <nvpair name="directory" value="/mnt" id="testfs-instance_attributes-directory"/>
+            <nvpair name="fstype" value="ocfs2" id="testfs-instance_attributes-fstype"/>
+            <nvpair name="device" value="/dev/sda1" id="testfs-instance_attributes-device"/>
+          </instance_attributes>
+        </primitive>
+      </clone>
+    </resources>
+    <constraints/>
+    <acls/>
+  </configuration>
+  <status/>
+</cib>
diff --git a/test/cibtests/004.input b/test/cibtests/004.input
new file mode 100644
index 0000000..8454d5d
--- /dev/null
+++ b/test/cibtests/004.input
@@ -0,0 +1,11 @@
+configure
+property stonith-enabled=false
+primitive testfs ocf:heartbeat:Filesystem \
+	params directory="/mnt" fstype="ocfs2" device="/dev/sda1"
+clone testfs-clone testfs \
+	meta ordered="true" interleave="true"
+commit
+up
+resource start testfs-clone
+quit
+
diff --git a/test/cibtests/Makefile.am b/test/cibtests/Makefile.am
new file mode 100644
index 0000000..0c288a3
--- /dev/null
+++ b/test/cibtests/Makefile.am
@@ -0,0 +1,29 @@
+#
+# Author: Sun Jiang Dong <sunjd at cn.ibm.com>
+# Copyright (c) 2004 International Business Machines
+#
+# 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.
+#
+MAINTAINERCLEANFILES = Makefile.in
+
+cibtestsdir		= 	$(datadir)/$(PACKAGE)/tests/cibtests
+cibtests_DATA	= shadow.base \
+	001.exp.xml 001.input \
+	002.exp.xml 002.input \
+	003.exp.xml 003.input \
+	004.exp.xml 004.input
+
+#			shouldn't need this next line...
+EXTRA_DIST =		$(cibtests_DATA)
diff --git a/test/cibtests/shadow.base b/test/cibtests/shadow.base
new file mode 100644
index 0000000..a4b376d
--- /dev/null
+++ b/test/cibtests/shadow.base
@@ -0,0 +1,10 @@
+<cib crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="59" num_updates="0" admin_epoch="0" cib-last-written="Tue Sep  2 12:08:39 2014">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources/>
+    <constraints/>
+    <acls/>
+  </configuration>
+  <status/>
+</cib>
diff --git a/test/crm-interface b/test/crm-interface
new file mode 100644
index 0000000..6389e22
--- /dev/null
+++ b/test/crm-interface
@@ -0,0 +1,99 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# 
+# 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.1 of the License, or (at your option) any later version.
+# 
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+CIB=__crmsh_regtest
+
+filter_epoch() {
+	sed '/^<cib /s/ epoch="[0-9]*"/ epoch="1"/'
+}
+filter_date() {
+	sed '/^<cib /s/cib-last-written=".*"/cib-last-written="Sun Apr 12 21:37:48 2009"/'
+}
+filter_cib() {
+	sed -n '/^<?xml/,/^<\/cib>/p' | filter_date | filter_epoch
+}
+
+crm_setup() {
+	$CRM_NO_REG options reset
+	$CRM_NO_REG options check-frequency on-verify
+	$CRM_NO_REG options check-mode relaxed
+	$CRM_NO_REG cib delete $CIB 2>/dev/null
+}
+
+crm_mksample() {
+	$CRM_NO_REG cib new $CIB empty 2>/dev/null
+	$CRM_NO_REG -c $CIB<<EOF
+configure
+node node1
+primitive p0 ocf:pacemaker:Dummy
+primitive p1 ocf:pacemaker:Dummy
+primitive p2 ocf:heartbeat:Delay \
+	params startdelay=2 mondelay=2 stopdelay=2
+primitive p3 ocf:pacemaker:Dummy
+primitive st stonith:null params hostlist=node1
+clone c1 p1
+ms m1 p2
+property default-action-timeout=60s
+commit
+end
+EOF
+}
+crm_show() {
+	$CRM -c $CIB<<EOF
+configure
+_regtest on
+erase
+erase nodes
+`cat`
+show
+commit
+EOF
+}
+crm_showxml() {
+	$CRM -c $CIB<<EOF | filter_cib
+configure
+_regtest on
+erase
+erase nodes
+`cat`
+show xml
+commit
+EOF
+}
+crm_session() {
+	$CRM -c $CIB <<EOF
+`cat`
+EOF
+}
+crm_filesession() {
+	local _file=`mktemp`
+	$CRM -c $CIB configure save xml $_file
+	CIB_file=$_file $CRM <<EOF
+`cat`
+EOF
+	rm -f $_file
+}
+crm_single() {
+	$CRM -c $CIB $*
+}
+crm_showobj() {
+	$CRM -c $CIB<<EOF | filter_date | filter_epoch
+configure
+_regtest on
+show xml $1
+EOF
+}
diff --git a/test/defaults b/test/defaults
new file mode 100644
index 0000000..50a7a6a
--- /dev/null
+++ b/test/defaults
@@ -0,0 +1,2 @@
+# defaults
+dflt_args=""
diff --git a/test/descriptions b/test/descriptions
new file mode 100644
index 0000000..8c549c1
--- /dev/null
+++ b/test/descriptions
@@ -0,0 +1,33 @@
+# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# 
+# 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.1 of the License, or (at your option) any later version.
+# 
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+lead=".TRY"
+describe_show() {
+	echo $lead $*
+}
+describe_showxml() {
+	: echo $lead $*
+}
+describe_session() {
+	echo $lead $*
+}
+describe_filesession() {
+	echo $lead $*
+}
+describe_single() {
+	echo $lead $*
+}
diff --git a/test/evaltest.sh b/test/evaltest.sh
new file mode 100755
index 0000000..b715135
--- /dev/null
+++ b/test/evaltest.sh
@@ -0,0 +1,127 @@
+#!/bin/sh
+
+ # Copyright (C) 2007 Dejan Muhamedagic <dejan at suse.de>
+ # 
+ # 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 software 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 library; if not, write to the Free Software
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ #
+
+: ${TESTDIR:=testcases}
+: ${CRM:=/usr/sbin/crm}
+CRM_NO_REG="$CRM"
+CRM="$CRM -R"
+export PYTHONUNBUFFERED=1
+
+if [ "$1" = prof ]; then
+	CRM="$CRM -X regtest.profile"
+fi
+
+. ./defaults
+. ./crm-interface
+. ./descriptions
+
+resetvars() {
+	unset args
+	unset extcheck
+}
+
+#
+# special operations squad
+#
+specopt_setenv() {
+	eval $rest
+}
+specopt_ext() {
+	eval $rest
+}
+specopt_extcheck() {
+	extcheck="$rest"
+	set $extcheck
+	which "$1" >/dev/null 2>&1 ||  # a program in the PATH
+		extcheck="$TESTDIR/$extcheck"  # or our script
+}
+specopt_repeat() {
+	repeat_limit=$rest
+}
+specopt() {
+	cmd=`echo $cmd | sed 's/%//'`  # strip leading '%'
+	echo ".`echo $cmd | tr '[a-z]' '[A-Z]'` $rest"  # show what we got
+	specopt_$cmd  # do what they asked for
+}
+
+#
+# substitute variables in the test line
+#
+substvars() {
+	sed "
+	s/%t/$test_cnt/g
+	s/%l/$line/g
+	s/%i/$repeat_cnt/g
+	"
+}
+
+dotest_session() {
+	echo -n "." >&3
+	test_cnt=$(($test_cnt+1))
+	describe_$cmd $*  # show what we are about to do
+	crm_$cmd |  # and execute the command
+		{ [ "$extcheck" ] && $extcheck || cat;}
+}
+dotest_single() {
+	echo -n "." >&3
+	test_cnt=$(($test_cnt+1))
+	describe_single $* # show what we are about to do
+	crm_single $* |  # and execute the command
+		{ [ "$extcheck" ] && $extcheck || cat;}
+	if [ "$showobj" ]; then
+		crm_showobj $showobj
+	fi
+}
+runtest_session() {
+	while read line; do
+		if [ "$line" = . ]; then
+			break
+		fi
+		echo "$line"
+	done | dotest_session $*
+}
+runtest_single() {
+	while [ $repeat_cnt -le $repeat_limit ]; do
+		dotest_single $*
+		resetvars  # unset all variables
+		repeat_cnt=$(($repeat_cnt+1))
+	done
+	repeat_limit=1 repeat_cnt=1
+}
+
+#
+# run the tests
+#
+repeat_limit=1 repeat_cnt=1
+line=1
+test_cnt=1
+
+crm_setup
+crm_mksample
+while read cmd rest; do
+	case "$cmd" in
+		"") : empty ;;
+		"#"*) : a comment ;;
+		"%stop") break ;;
+		"%"*) specopt ;;
+		show|showxml|session|filesession) runtest_session $rest ;;
+		*) runtest_single $cmd $rest ;;
+	esac
+	line=$(($line+1))
+done
diff --git a/test/history-test.tar.bz2 b/test/history-test.tar.bz2
new file mode 100644
index 0000000..a33acad
Binary files /dev/null and b/test/history-test.tar.bz2 differ
diff --git a/test/list-undocumented-commands.py b/test/list-undocumented-commands.py
new file mode 100755
index 0000000..0bf04cb
--- /dev/null
+++ b/test/list-undocumented-commands.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+#
+# Script to discover and report undocumented commands.
+
+import os
+import sys
+
+parent, bindir = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0])))
+if os.path.exists(os.path.join(parent, 'modules')):
+    sys.path.insert(0, parent)
+
+
+from modules.ui_root import Root
+import modules.help
+
+modules.help.HELP_FILE = "doc/crm.8.txt"
+modules.help._load_help()
+
+_IGNORED_COMMANDS = ('help', 'quit', 'cd', 'up', 'ls')
+
+def check_help(ui):
+    for name, child in ui._children.iteritems():
+        if child.type == 'command':
+            try:
+                h = modules.help.help_command(ui.name, name)
+                if h.generated and name not in _IGNORED_COMMANDS:
+                    print "Undocumented: %s %s" % (ui.name, name)
+            except:
+                print "Undocumented: %s %s" % (ui.name, name)
+        elif child.type == 'level':
+            h = modules.help.help_level(name)
+            if h.generated:
+                print "Undocumented: %s %s" % (ui.name, name)
+            check_help(child.level)
+
+check_help(Root())
diff --git a/test/regression.sh b/test/regression.sh
new file mode 100755
index 0000000..a395435
--- /dev/null
+++ b/test/regression.sh
@@ -0,0 +1,213 @@
+#!/bin/sh
+
+ # Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+ # 
+ # 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 software 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 library; if not, write to the Free Software
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ #
+
+rootdir=`dirname $0`
+TESTDIR=${TESTDIR:-$rootdir/testcases}
+DFLT_TESTSET=basicset
+OUTDIR=${OUTDIR:-crmtestout}
+CRM_OUTF="$OUTDIR/crm.out"
+CRM_LOGF="$OUTDIR/crm.log"
+CRM_DEBUGF="$OUTDIR/crm.debug"
+OUTF="$OUTDIR/regression.out"
+LRMD_OPTS=""
+DIFF_OPTS="--ignore-all-space -U 1"
+common_filter=$TESTDIR/common.filter
+common_exclf=$TESTDIR/common.excl
+export OUTDIR
+
+logmsg() {
+	echo "`date`: $*" | tee -a $CRM_DEBUGF | tee -a $CRM_LOGF
+}
+abspath() {
+	echo $1 | grep -qs "^/" &&
+		echo $1 ||
+		echo `pwd`/$1
+}
+
+usage() {
+	cat<<EOF
+
+usage: $0 [-q] [-P] [testcase...|set:testset]
+
+Test crm shell using supplied testcases. If none are given,
+set:basicset is used. All testcases and sets are in testcases/.
+See also README.regression for description.
+
+-q: quiet operation (no progress shown)
+-P: profile test
+
+EOF
+exit 2
+}
+
+if [ ! -d "$TESTDIR" ]; then
+	echo "$0: $TESTDIR does not exit"
+	usage
+fi
+
+rm -f $CRM_LOGF $CRM_DEBUGF
+
+# make tools/lrmd/stonithd log to our files only
+HA_logfile=`abspath $CRM_LOGF`
+HA_debugfile=`abspath $CRM_DEBUGF`
+HA_use_logd=no
+HA_logfacility=""
+export HA_logfile HA_debugfile HA_use_logd HA_logfacility
+
+mkdir -p $OUTDIR
+. /etc/ha.d/shellfuncs
+
+args=`getopt hqPc:p:m: $*`
+[ $? -ne 0 ] && usage
+eval set -- "$args"
+
+output_mode="normal"
+while [ x"$1" != x ]; do
+	case "$1" in
+		-h) usage;;
+	        -m) output_mode=$2; shift 1;;	    
+		-q) output_mode="silent";;
+		-P) do_profile=1;;
+	        -c) CRM=$2; export CRM; shift 1;;
+	        -p) PATH="$2:$PATH"; export PATH; shift 1;;
+		--) shift 1; break;;
+		*) usage;;
+	esac
+	shift 1
+done
+
+exec >$OUTF 2>&1
+
+# Where to send user output
+# evaltest.sh also uses >&3 for printing progress dots
+case $output_mode in 
+    silent) exec 3>/dev/null;;
+    buildbot) exec 3>$CRM_OUTF;;
+    *) exec 3>/dev/tty;;
+esac
+
+setenvironment() {
+	filterf=$TESTDIR/$testcase.filter
+	pref=$TESTDIR/$testcase.pre
+	postf=$TESTDIR/$testcase.post
+	exclf=$TESTDIR/$testcase.excl
+	log_filter=$TESTDIR/$testcase.log_filter
+	expf=$TESTDIR/$testcase.exp
+	outf=$OUTDIR/$testcase.out
+	difff=$OUTDIR/$testcase.diff
+}
+
+filter_output() {
+	{ [ -x $common_filter ] && $common_filter || cat;} |
+	{ [ -f $common_exclf ] && egrep -vf $common_exclf || cat;} |
+	{ [ -x $filterf ] && $filterf || cat;} |
+	{ [ -f $exclf ] && egrep -vf $exclf || cat;}
+}
+
+dumpcase() {
+	cat<<EOF
+----------
+testcase $testcase failed
+output is in $outf
+diff (from $difff):
+`cat $difff`
+----------
+EOF
+}
+
+runtestcase() {
+	setenvironment
+	(
+	cd $rootdir
+	[ -x "$pref" ] && $pref >/dev/null 2>&1
+	)
+	echo -n "$testcase" >&3
+	logmsg "BEGIN testcase $testcase"
+	(
+	cd $rootdir
+	./evaltest.sh $testargs
+	) < $TESTDIR/$testcase > $outf 2>&1
+
+	filter_output < $outf |
+	if [ "$prepare" ]; then
+		echo " saving to expect file" >&3
+		cat > $expf
+	else
+		(
+		cd $rootdir
+		[ -x "$postf" ] && $postf >/dev/null 2>&1
+		)
+		echo -n " checking..." >&3
+		if head -2 $expf | grep -qs '^<cib'; then
+			crm_diff -o $expf -n -
+		else
+			diff $DIFF_OPTS $expf -
+		fi > $difff
+		if [ $? -ne 0 ]; then
+			echo " FAIL" >&3
+			dumpcase
+			return 1
+		else
+			echo " PASS" >&3
+			rm -f $outf $difff
+		fi
+	fi
+	sed -n "/BEGIN testcase $testcase/,\$p" $CRM_LOGF |
+		{ [ -x $log_filter ] && $log_filter || cat;} |
+		egrep '(CRIT|ERROR):'
+	logmsg "END testcase $testcase"
+}
+
+[ "$1" = prepare ] && { prepare=1; shift 1;}
+[ $# -eq 0 ] && set "set:$DFLT_TESTSET"
+testargs=""
+if [ -n "$do_profile" ]; then
+	if echo $1 | grep -qs '^set:'; then
+		echo you can profile just one test
+		echo 'really!'
+		exit 1
+	fi
+	testargs="prof"
+fi
+
+for a; do
+	if [ "$a" -a -f "$TESTDIR/$a" ]; then
+		testcase=$a
+		runtestcase
+	elif echo "$a" | grep -q "^set:"; then
+		TESTSET=$TESTDIR/`echo $a | sed 's/set://'`
+		if [ -f "$TESTSET" ]; then
+			while read testcase; do
+				runtestcase
+			done < $TESTSET
+		else
+			echo "testset $TESTSET does not exist" >&3
+		fi
+	else
+		echo "test $TESTDIR/$a does not exist" >&3
+	fi
+done
+
+if egrep -wv '(BEGIN|END) testcase' $OUTF >/dev/null
+then
+	echo "seems like some tests failed or else something not expected"
+	echo "check $OUTF and diff files in $OUTDIR"
+	echo "in case you wonder what lrmd was doing, read $CRM_LOGF and $CRM_DEBUGF"
+	exit 1
+fi >&3
diff --git a/test/testcases/Makefile.am b/test/testcases/Makefile.am
new file mode 100644
index 0000000..7cc44ef
--- /dev/null
+++ b/test/testcases/Makefile.am
@@ -0,0 +1,36 @@
+#
+# Author: Sun Jiang Dong <sunjd at cn.ibm.com>
+# Copyright (c) 2004 International Business Machines
+#
+# 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.
+#
+MAINTAINERCLEANFILES = Makefile.in
+
+testcasesdir		= 	$(datadir)/$(PACKAGE)/tests/testcases
+testcases_SCRIPTS	=	ra.filter common.filter xmlonly.sh history.pre history.post
+testcases_DATA		=	basicset common.excl \
+	confbasic confbasic-xml delete file \
+	node ra resource shadow acl options \
+	edit edit.excl rset rset-xml history \
+	confbasic-xml.exp confbasic.exp delete.exp file.exp \
+	node.exp ra.exp resource.exp shadow.exp acl.exp options.exp \
+	edit.exp rset.exp rset-xml.exp history.exp history.excl \
+	newfeatures newfeatures.exp acl.excl commit commit.exp
+
+confbasic-xml.filter:
+	ln xmlonly.sh $@
+
+#			shouldn't need this next line...
+EXTRA_DIST =		$(testcases_SCRIPTS) $(testcases_DATA)
diff --git a/test/testcases/acl b/test/testcases/acl
new file mode 100644
index 0000000..6c986be
--- /dev/null
+++ b/test/testcases/acl
@@ -0,0 +1,60 @@
+show ACL
+node node1
+property enable-acl=true
+primitive st stonith:ssh \
+	params hostlist='node1' \
+	meta target-role="Started" \
+	op start requires=nothing timeout=60s \
+	op monitor interval=60m timeout=60s
+primitive d0 ocf:pacemaker:Dummy
+primitive d1 ocf:pacemaker:Dummy
+role basic-read \
+       read status \
+       read type:node attribute:uname \
+       read type:node attribute:type \
+       read property
+role basic-read-basic \
+	read cib
+role d0-admin \
+	write meta:d0:target-role \
+	write meta:d0:is-managed \
+	read ref:d0
+role silly-role \
+	write meta:d0:target-role \
+	write meta:d0:is-managed \
+	read ref:d0 \
+	read status \
+	read type:node attribute:uname \
+	read type:node attribute:type \
+	read utilization:d0 \
+	read property:stonith-enabled \
+	write property \
+	read node \
+	read node:node1 \
+	read nodeattr \
+	read nodeattr:a1 \
+	read nodeutil \
+	read nodeutil:node1 \
+	read status \
+	read cib
+role silly-role-two \
+	read xpath:"//nodes//attributes" \
+	deny tag:nvpair \
+	deny ref:d0
+acl_target alice \
+	basic-read-basic
+acl_target bob \
+	d0-admin \
+	basic-read-basic
+role cyrus-role \
+	write meta:d0:target-role \
+	write meta:d0:is-managed \
+	read ref:d0 \
+	read status \
+	read type:node attribute:uname \
+	read type:node attribute:type \
+	read property
+acl_target cyrus cyrus-role
+_test
+verify
+.
diff --git a/test/testcases/acl.excl b/test/testcases/acl.excl
new file mode 100644
index 0000000..31d13f7
--- /dev/null
+++ b/test/testcases/acl.excl
@@ -0,0 +1 @@
+INFO: 5: already using schema pacemaker-1.2
diff --git a/test/testcases/acl.exp b/test/testcases/acl.exp
new file mode 100644
index 0000000..cc1fc8d
--- /dev/null
+++ b/test/testcases/acl.exp
@@ -0,0 +1,87 @@
+.TRY ACL
+.INP: configure
+.INP: _regtest on
+.INP: erase
+.INP: erase nodes
+.INP: node node1
+.INP: property enable-acl=true
+.INP: primitive st stonith:ssh 	params hostlist='node1' 	meta target-role="Started" 	op start requires=nothing timeout=60s 	op monitor interval=60m timeout=60s
+.INP: primitive d0 ocf:pacemaker:Dummy
+.INP: primitive d1 ocf:pacemaker:Dummy
+.INP: role basic-read        read status        read type:node attribute:uname        read type:node attribute:type        read property
+.INP: role basic-read-basic 	read cib
+.INP: role d0-admin 	write meta:d0:target-role 	write meta:d0:is-managed 	read ref:d0
+.INP: role silly-role 	write meta:d0:target-role 	write meta:d0:is-managed 	read ref:d0 	read status 	read type:node attribute:uname 	read type:node attribute:type 	read utilization:d0 	read property:stonith-enabled 	write property 	read node 	read node:node1 	read nodeattr 	read nodeattr:a1 	read nodeutil 	read nodeutil:node1 	read status 	read cib
+.INP: role silly-role-two 	read xpath:"//nodes//attributes" 	deny tag:nvpair 	deny ref:d0
+.INP: acl_target alice 	basic-read-basic
+.INP: acl_target bob 	d0-admin 	basic-read-basic
+.INP: role cyrus-role 	write meta:d0:target-role 	write meta:d0:is-managed 	read ref:d0 	read status 	read type:node attribute:uname 	read type:node attribute:type 	read property
+.INP: acl_target cyrus cyrus-role
+.INP: _test
+.INP: verify
+.EXT crm_resource --show-metadata stonith:ssh
+.EXT stonithd metadata
+.EXT crm_resource --show-metadata ocf:pacemaker:Dummy
+.EXT crmd metadata
+.EXT pengine metadata
+.EXT cib metadata
+.INP: show
+node node1
+primitive d0 ocf:pacemaker:Dummy
+primitive d1 ocf:pacemaker:Dummy
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+property cib-bootstrap-options: \
+	enable-acl=true
+role basic-read \
+	read status \
+	read attr:uname type:node \
+	read attr:type type:node \
+	read property
+role basic-read-basic \
+	read cib
+role cyrus-role \
+	write meta:d0:target-role \
+	write meta:d0:is-managed \
+	read ref:d0 \
+	read status \
+	read attr:uname type:node \
+	read attr:type type:node \
+	read property
+role d0-admin \
+	write meta:d0:target-role \
+	write meta:d0:is-managed \
+	read ref:d0
+role silly-role \
+	write meta:d0:target-role \
+	write meta:d0:is-managed \
+	read ref:d0 \
+	read status \
+	read attr:uname type:node \
+	read attr:type type:node \
+	read utilization:d0 \
+	read property:stonith-enabled \
+	write property \
+	read node \
+	read node:node1 \
+	read nodeattr \
+	read nodeattr:a1 \
+	read nodeutil \
+	read nodeutil:node1 \
+	read status \
+	read cib
+role silly-role-two \
+	read xpath:"//nodes//attributes" \
+	deny type:nvpair \
+	deny ref:d0
+acl_target alice \
+	basic-read-basic
+acl_target bob \
+	d0-admin \
+	basic-read-basic
+acl_target cyrus \
+	cyrus-role
+.INP: commit
diff --git a/test/testcases/basicset b/test/testcases/basicset
new file mode 100644
index 0000000..fd0009c
--- /dev/null
+++ b/test/testcases/basicset
@@ -0,0 +1,15 @@
+confbasic
+confbasic-xml
+edit
+rset
+rset-xml
+delete
+node
+resource
+file
+shadow
+ra
+acl
+history
+newfeatures
+commit
diff --git a/test/testcases/commit b/test/testcases/commit
new file mode 100644
index 0000000..e811135
--- /dev/null
+++ b/test/testcases/commit
@@ -0,0 +1,40 @@
+show Commits of all kinds
+property default-action-timeout=2m
+primitive st stonith:null \
+	params hostlist='node1' \
+	meta yoyo-meta="yoyo 2" \
+	op start requires=nothing \
+	op monitor interval=60m
+commit
+node node1 \
+	attributes mem=16G
+primitive p1 ocf:heartbeat:Dummy \
+	op monitor interval=60m \
+	op monitor interval=120m OCF_CHECK_LEVEL=10
+primitive p2 ocf:heartbeat:Dummy
+primitive p3 ocf:heartbeat:Dummy
+group g1 p1 p2
+clone c1 g1
+location l1 p3 100: node1
+order o1 inf: p3 c1
+colocation cl1 inf: c1 p3
+primitive d1 ocf:heartbeat:Dummy
+primitive d2 ocf:heartbeat:Dummy
+primitive d3 ocf:heartbeat:Dummy
+commit
+rename p3 pp3
+commit
+rename pp3 p3
+delete c1
+commit
+group g2 d1 d2
+commit
+delete g2
+commit
+filter "sed '/g1/s/p1/d1/'"
+group g2 d3 d2
+delete d2
+commit
+_test
+verify
+.
diff --git a/test/testcases/commit.exp b/test/testcases/commit.exp
new file mode 100644
index 0000000..f087d04
--- /dev/null
+++ b/test/testcases/commit.exp
@@ -0,0 +1,77 @@
+.TRY Commits of all kinds
+.INP: configure
+.INP: _regtest on
+.INP: erase
+.INP: erase nodes
+.INP: property default-action-timeout=2m
+.INP: primitive st stonith:null 	params hostlist='node1' 	meta yoyo-meta="yoyo 2" 	op start requires=nothing 	op monitor interval=60m
+.INP: commit
+.EXT crm_resource --show-metadata stonith:null
+.EXT stonithd metadata
+.EXT crmd metadata
+.EXT pengine metadata
+.EXT cib metadata
+ERROR: 7: st: attribute yoyo-meta does not exist
+.INP: node node1 	attributes mem=16G
+.INP: primitive p1 ocf:heartbeat:Dummy 	op monitor interval=60m 	op monitor interval=120m OCF_CHECK_LEVEL=10
+.INP: primitive p2 ocf:heartbeat:Dummy
+.INP: primitive p3 ocf:heartbeat:Dummy
+.INP: group g1 p1 p2
+.INP: clone c1 g1
+.INP: location l1 p3 100: node1
+.INP: order o1 inf: p3 c1
+.INP: colocation cl1 inf: c1 p3
+.INP: primitive d1 ocf:heartbeat:Dummy
+.INP: primitive d2 ocf:heartbeat:Dummy
+.INP: primitive d3 ocf:heartbeat:Dummy
+.INP: commit
+.EXT crm_resource --show-metadata ocf:heartbeat:Dummy
+.INP: rename p3 pp3
+INFO: 21: resource references in location:l1 updated
+INFO: 21: resource references in colocation:cl1 updated
+INFO: 21: resource references in order:o1 updated
+.INP: commit
+.INP: rename pp3 p3
+INFO: 23: resource references in location:l1 updated
+INFO: 23: resource references in colocation:cl1 updated
+INFO: 23: resource references in order:o1 updated
+.INP: delete c1
+INFO: 24: resource references in colocation:cl1 updated
+INFO: 24: resource references in order:o1 updated
+.INP: commit
+.INP: group g2 d1 d2
+.INP: commit
+.INP: delete g2
+.INP: commit
+.INP: filter "sed '/g1/s/p1/d1/'"
+.INP: group g2 d3 d2
+.INP: delete d2
+.INP: commit
+.INP: _test
+.INP: verify
+ERROR: 35: st: attribute yoyo-meta does not exist
+.INP: show
+node node1 \
+	attributes mem=16G
+primitive d1 Dummy
+primitive d3 Dummy
+primitive p1 Dummy \
+	op monitor interval=60m \
+	op monitor interval=120m OCF_CHECK_LEVEL=10
+primitive p2 Dummy
+primitive p3 Dummy
+primitive st stonith:null \
+	params hostlist=node1 \
+	meta yoyo-meta="yoyo 2" \
+	op start requires=nothing interval=0 \
+	op monitor interval=60m
+group g1 d1 p2
+group g2 d3
+location l1 p3 100: node1
+colocation cl1 inf: g1 p3
+order o1 inf: p3 g1
+property cib-bootstrap-options: \
+	default-action-timeout=2m
+.INP: commit
+INFO: 37: apparently there is nothing to commit
+INFO: 37: try changing something first
diff --git a/test/testcases/common.excl b/test/testcases/common.excl
new file mode 100644
index 0000000..8b14622
--- /dev/null
+++ b/test/testcases/common.excl
@@ -0,0 +1,20 @@
+Could not send fail-count-p0=\(null\) update via attrd: connection failed
+Could not send fail-count-p0=<none> update via attrd: connection failed
+Could not send s1=\(null\) update via attrd: connection failed
+Could not send s1=<none> update via attrd: connection failed
+Error performing operation: The object/attribute does not exist
+Error setting fail-count-p0=5 \(section=status, set=status-node1\): The object/attribute does not exist
+Error setting s1=1 2 3 \(section=status, set=status-node1\): The object/attribute does not exist
+Error signing on to the CRMd service
+.EXT crm_resource --list-standards
+.EXT crm_resource --list-ocf-alternatives Delay
+.EXT crm_resource --list-ocf-alternatives Dummy
+^\.EXT crmd version
+^\.EXT cibadmin \-Ql
+^\.EXT crm_verify \-\-verbose \-p
+^\.EXT cibadmin \-p \-P
+^\.EXT crm_diff \-\-help
+^\.EXT crm_diff \-o [^ ]+ \-n \-
+^\.EXT crm_diff \-\-no\-version \-o [^ ]+ \-n \-
+^\.EXT sed ['][^']+
+^\.EXT sed ["][^"]+
diff --git a/test/testcases/common.filter b/test/testcases/common.filter
new file mode 100755
index 0000000..957352d
--- /dev/null
+++ b/test/testcases/common.filter
@@ -0,0 +1,5 @@
+#!/usr/bin/awk -f
+# 1. replace .EXT [path/]<cmd> <parameter> with .EXT <cmd> <parameter>
+/\.EXT \/(.+)/ { gsub(/\/.*\//, "", $2) }
+/\.EXT >\/dev\/null 2>&1 \/(.+)/ { gsub(/\/.*\//, "", $4) }
+{ print }
diff --git a/test/testcases/confbasic b/test/testcases/confbasic
new file mode 100644
index 0000000..872e9f8
--- /dev/null
+++ b/test/testcases/confbasic
@@ -0,0 +1,76 @@
+show Basic configure
+node node1
+delete node1
+node node1 \
+	attributes mem=16G
+node node2 utilization cpu=4
+primitive st stonith:ssh \
+	params hostlist='node1 node2' \
+	meta target-role="Started" \
+	op start requires=nothing timeout=60s \
+	op monitor interval=60m timeout=60s
+primitive st2 stonith:ssh \
+	params hostlist='node1 node2'
+primitive d1 ocf:pacemaker:Dummy \
+	operations $id=d1-ops \
+	op monitor interval=60m \
+	op monitor interval=120m OCF_CHECK_LEVEL=10
+monitor d1 60s:30s
+primitive d2 ocf:heartbeat:Delay \
+	params mondelay=60 \
+	op start timeout=60s \
+	op stop timeout=60s
+monitor d2:Started 60s:30s
+group g1 d1 d2
+primitive d3 ocf:pacemaker:Dummy
+clone c d3 \
+	meta clone-max=1
+primitive d4 ocf:pacemaker:Dummy
+ms m d4
+delete m
+master m d4
+primitive s5 ocf:pacemaker:Stateful \
+	operations $id-ref=d1-ops
+primitive s6 ocf:pacemaker:Stateful \
+	operations $id-ref=d1
+ms m5 s5
+ms m6 s6
+primitive d7 Dummy \
+	params rule inf: #uname eq node1 fake=1 \
+	params rule inf: #uname eq node2 fake=2
+location l1 g1 100: node1
+location l2 c \
+	rule $id=l2-rule1 100: #uname eq node1
+location l3 m5 \
+	rule inf: #uname eq node1 and pingd gt 0
+location l4 m5 \
+	rule -inf: not_defined pingd or pingd lte 0
+location l5 m5 \
+	rule -inf: not_defined pingd or pingd lte 0 \
+	rule inf: #uname eq node1 and pingd gt 0 \
+	rule inf: date lt "2009-05-26" and \
+		date in start="2009-05-26" end="2009-07-26" and \
+		date in start="2009-05-26" years="2009" and \
+		date spec years="2009" hours="09-17"
+location l6 m5 \
+	rule $id-ref=l2-rule1
+location l7 m5 \
+	rule $id-ref=l2
+collocation c1 inf: m6 m5
+collocation c2 inf: m5:Master d1:Started
+order o1 Mandatory: m5 m6
+order o2 Optional: d1:start m5:promote
+order o3 Serialize: m5 m6
+order o4 inf: m5 m6
+rsc_ticket ticket-A_m6 ticket-A: m6
+rsc_ticket ticket-B_m6_m5 ticket-B: m6 m5 loss-policy=fence
+rsc_ticket ticket-C_master ticket-C: m6 m5:Master loss-policy=fence
+fencing_topology st st2
+property stonith-enabled=true
+property $id=cpset2 maintenance-mode=true
+rsc_defaults failure-timeout=10m
+op_defaults $id=opsdef2 rule 100: #uname eq node1 record-pending=true
+tag t1: m5 m6
+_test
+verify
+.
diff --git a/test/testcases/confbasic-xml b/test/testcases/confbasic-xml
new file mode 100644
index 0000000..cc0bfae
--- /dev/null
+++ b/test/testcases/confbasic-xml
@@ -0,0 +1,72 @@
+showxml Basic configure (xml dump)
+node node1
+delete node1
+node node1 \
+	attributes mem=16G
+node node2 utilization cpu=4
+primitive st stonith:ssh \
+	params hostlist='node1 node2' \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s \
+	op monitor interval=60m timeout=60s
+primitive st2 stonith:ssh \
+	params hostlist='node1 node2'
+primitive d1 ocf:pacemaker:Dummy \
+	operations $id=d1-ops \
+	op monitor interval=60m \
+	op monitor interval=120m OCF_CHECK_LEVEL=10
+monitor d1 60s:30s
+primitive d2 ocf:heartbeat:Delay \
+	params mondelay=60 \
+	op start timeout=60s \
+	op stop timeout=60s
+monitor d2:Started 60s:30s
+group g1 d1 d2
+primitive d3 ocf:pacemaker:Dummy
+clone c d3 \
+	meta clone-max=1
+primitive d4 ocf:pacemaker:Dummy
+ms m d4
+delete m
+master m d4
+primitive s5 ocf:pacemaker:Stateful \
+	operations $id-ref=d1-ops
+primitive s6 ocf:pacemaker:Stateful \
+	operations $id-ref=d1
+ms m5 s5
+ms m6 s6
+location l1 g1 100: node1
+location l2 c \
+	rule $id=l2-rule1 100: #uname eq node1
+location l3 m5 \
+	rule inf: #uname eq node1 and pingd gt 0
+location l4 m5 \
+	rule -inf: not_defined pingd or pingd lte 0
+location l5 m5 \
+	rule -inf: not_defined pingd or pingd lte 0 \
+	rule inf: #uname eq node1 and pingd gt 0 \
+	rule inf: date lt 2009-05-26 and \
+		date in start=2009-05-26 end=2009-07-26 and \
+		date in start=2009-05-26 years=2009 and \
+		date spec years=2009 hours=09-17
+location l6 m5 \
+	rule $id-ref=l2-rule1
+location l7 m5 \
+	rule $id-ref=l2
+collocation c1 inf: m6 m5
+collocation c2 inf: m5:Master d1:Started
+order o1 Mandatory: m5 m6
+order o2 Optional: d1:start m5:promote
+order o3 Serialize: m5 m6
+order o4 inf: m5 m6
+rsc_ticket ticket-A_m6 ticket-A: m6
+rsc_ticket ticket-B_m6_m5 ticket-B: m6 m5 loss-policy=fence
+rsc_ticket ticket-C_master ticket-C: m6 m5:Master loss-policy=fence
+fencing_topology st st2
+property stonith-enabled=true
+property $id=cpset2 maintenance-mode=true
+rsc_defaults failure-timeout=10m
+op_defaults $id=opsdef2 record-pending=true
+_test
+verify
+.
diff --git a/test/testcases/confbasic-xml.exp b/test/testcases/confbasic-xml.exp
new file mode 100644
index 0000000..3b52e6c
--- /dev/null
+++ b/test/testcases/confbasic-xml.exp
@@ -0,0 +1,169 @@
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config>
+      <cluster_property_set id="cib-bootstrap-options">
+        <nvpair name="stonith-enabled" value="true" id="cib-bootstrap-options-stonith-enabled"/>
+      </cluster_property_set>
+      <cluster_property_set id="cpset2">
+        <nvpair name="maintenance-mode" value="true" id="cpset2-maintenance-mode"/>
+      </cluster_property_set>
+    </crm_config>
+    <nodes>
+      <node uname="node1" id="node1">
+        <instance_attributes id="node1-instance_attributes">
+          <nvpair name="mem" value="16G" id="node1-instance_attributes-mem"/>
+        </instance_attributes>
+      </node>
+      <node uname="node2" id="node2">
+        <utilization id="node2-utilization">
+          <nvpair name="cpu" value="4" id="node2-utilization-cpu"/>
+        </utilization>
+      </node>
+    </nodes>
+    <resources>
+      <group id="g1">
+        <primitive id="d1" class="ocf" provider="pacemaker" type="Dummy">
+          <operations id="d1-ops">
+            <op name="monitor" interval="60m" id="d1-ops-monitor-60m"/>
+            <op name="monitor" interval="120m" id="d1-ops-monitor-120m">
+              <instance_attributes id="d1-ops-monitor-120m-instance_attributes">
+                <nvpair name="OCF_CHECK_LEVEL" value="10" id="d1-ops-monitor-120m-instance_attributes-OCF_CHECK_LEVEL"/>
+              </instance_attributes>
+            </op>
+            <op name="monitor" interval="60s" timeout="30s" id="d1-monitor-60s"/>
+          </operations>
+        </primitive>
+        <primitive id="d2" class="ocf" provider="heartbeat" type="Delay">
+          <instance_attributes id="d2-instance_attributes">
+            <nvpair name="mondelay" value="60" id="d2-instance_attributes-mondelay"/>
+          </instance_attributes>
+          <operations>
+            <op name="start" timeout="60s" interval="0" id="d2-start-0"/>
+            <op name="stop" timeout="60s" interval="0" id="d2-stop-0"/>
+            <op name="monitor" role="Started" interval="60s" timeout="30s" id="d2-monitor-60s"/>
+          </operations>
+        </primitive>
+      </group>
+      <clone id="c">
+        <meta_attributes id="c-meta_attributes">
+          <nvpair name="clone-max" value="1" id="c-meta_attributes-clone-max"/>
+        </meta_attributes>
+        <primitive id="d3" class="ocf" provider="pacemaker" type="Dummy"/>
+      </clone>
+      <master id="m">
+        <primitive id="d4" class="ocf" provider="pacemaker" type="Dummy"/>
+      </master>
+      <master id="m5">
+        <primitive id="s5" class="ocf" provider="pacemaker" type="Stateful">
+          <operations id-ref="d1-ops"/>
+        </primitive>
+      </master>
+      <master id="m6">
+        <primitive id="s6" class="ocf" provider="pacemaker" type="Stateful">
+          <operations id-ref="d1-ops"/>
+        </primitive>
+      </master>
+      <primitive id="st" class="stonith" type="ssh">
+        <instance_attributes id="st-instance_attributes">
+          <nvpair name="hostlist" value="node1 node2" id="st-instance_attributes-hostlist"/>
+        </instance_attributes>
+        <meta_attributes id="st-meta_attributes">
+          <nvpair name="target-role" value="Started" id="st-meta_attributes-target-role"/>
+        </meta_attributes>
+        <operations>
+          <op name="start" requires="nothing" timeout="60s" interval="0" id="st-start-0"/>
+          <op name="monitor" interval="60m" timeout="60s" id="st-monitor-60m"/>
+        </operations>
+      </primitive>
+      <primitive id="st2" class="stonith" type="ssh">
+        <instance_attributes id="st2-instance_attributes">
+          <nvpair name="hostlist" value="node1 node2" id="st2-instance_attributes-hostlist"/>
+        </instance_attributes>
+      </primitive>
+    </resources>
+    <constraints>
+      <rsc_order id="o3" kind="Serialize" first="m5" then="m6"/>
+      <rsc_ticket id="ticket-C_master" ticket="ticket-C" loss-policy="fence">
+        <resource_set id="ticket-C_master-0">
+          <resource_ref id="m6"/>
+        </resource_set>
+        <resource_set role="Master" id="ticket-C_master-1">
+          <resource_ref id="m5"/>
+        </resource_set>
+      </rsc_ticket>
+      <rsc_location id="l3" rsc="m5">
+        <rule score="INFINITY" id="l3-rule">
+          <expression attribute="#uname" operation="eq" value="node1" id="l3-rule-expression"/>
+          <expression attribute="pingd" operation="gt" value="0" id="l3-rule-expression-0"/>
+        </rule>
+      </rsc_location>
+      <rsc_location id="l1" rsc="g1" score="100" node="node1"/>
+      <rsc_location id="l2" rsc="c">
+        <rule id="l2-rule1" score="100">
+          <expression attribute="#uname" operation="eq" value="node1" id="l2-rule1-expression"/>
+        </rule>
+      </rsc_location>
+      <rsc_ticket id="ticket-B_m6_m5" ticket="ticket-B" loss-policy="fence">
+        <resource_set id="ticket-B_m6_m5-0">
+          <resource_ref id="m6"/>
+          <resource_ref id="m5"/>
+        </resource_set>
+      </rsc_ticket>
+      <rsc_location id="l4" rsc="m5">
+        <rule score="-INFINITY" boolean-op="or" id="l4-rule">
+          <expression attribute="pingd" operation="not_defined" id="l4-rule-expression"/>
+          <expression attribute="pingd" operation="lte" value="0" id="l4-rule-expression-0"/>
+        </rule>
+      </rsc_location>
+      <rsc_location id="l5" rsc="m5">
+        <rule score="-INFINITY" boolean-op="or" id="l5-rule">
+          <expression attribute="pingd" operation="not_defined" id="l5-rule-expression"/>
+          <expression attribute="pingd" operation="lte" value="0" id="l5-rule-expression-0"/>
+        </rule>
+        <rule score="INFINITY" id="l5-rule-0">
+          <expression attribute="#uname" operation="eq" value="node1" id="l5-rule-0-expression"/>
+          <expression attribute="pingd" operation="gt" value="0" id="l5-rule-0-expression-0"/>
+        </rule>
+        <rule score="INFINITY" id="l5-rule-1">
+          <date_expression operation="lt" end="2009-05-26" id="l5-rule-1-expression"/>
+          <date_expression operation="in_range" start="2009-05-26" end="2009-07-26" id="l5-rule-1-expression-0"/>
+          <date_expression operation="in_range" start="2009-05-26" id="l5-rule-1-expression-1">
+            <duration years="2009" id="l5-rule-1-expression-1-duration"/>
+          </date_expression>
+          <date_expression operation="date_spec" id="l5-rule-1-expression-2">
+            <date_spec years="2009" hours="09-17" id="l5-rule-1-expression-2-date_spec"/>
+          </date_expression>
+        </rule>
+      </rsc_location>
+      <rsc_colocation id="c2" score="INFINITY" rsc="m5" rsc-role="Master" with-rsc="d1" with-rsc-role="Started"/>
+      <rsc_ticket id="ticket-A_m6" ticket="ticket-A" rsc="m6"/>
+      <rsc_order id="o2" kind="Optional" first="d1" first-action="start" then="m5" then-action="promote"/>
+      <rsc_location id="l6" rsc="m5">
+        <rule id-ref="l2-rule1"/>
+      </rsc_location>
+      <rsc_order id="o1" kind="Mandatory" first="m5" then="m6"/>
+      <rsc_colocation id="c1" score="INFINITY" rsc="m6" with-rsc="m5"/>
+      <rsc_location id="l7" rsc="m5">
+        <rule id-ref="l2-rule1"/>
+      </rsc_location>
+      <rsc_order id="o4" score="INFINITY" first="m5" then="m6"/>
+    </constraints>
+    <rsc_defaults>
+      <meta_attributes id="rsc-options">
+        <nvpair name="failure-timeout" value="10m" id="rsc-options-failure-timeout"/>
+      </meta_attributes>
+    </rsc_defaults>
+    <fencing-topology>
+      <fencing-level devices="st" index="1" target="node1" id="fencing"/>
+      <fencing-level devices="st2" index="2" target="node1" id="fencing-0"/>
+      <fencing-level devices="st" index="1" target="node2" id="fencing-1"/>
+      <fencing-level devices="st2" index="2" target="node2" id="fencing-2"/>
+    </fencing-topology>
+    <op_defaults>
+      <meta_attributes id="opsdef2">
+        <nvpair name="record-pending" value="true" id="opsdef2-record-pending"/>
+      </meta_attributes>
+    </op_defaults>
+  </configuration>
+</cib>
diff --git a/test/testcases/confbasic.exp b/test/testcases/confbasic.exp
new file mode 100644
index 0000000..36680bb
--- /dev/null
+++ b/test/testcases/confbasic.exp
@@ -0,0 +1,133 @@
+.TRY Basic configure
+.INP: configure
+.INP: _regtest on
+.INP: erase
+.INP: erase nodes
+.INP: node node1
+.INP: delete node1
+.INP: node node1 	attributes mem=16G
+.INP: node node2 utilization cpu=4
+.INP: primitive st stonith:ssh 	params hostlist='node1 node2' 	meta target-role="Started" 	op start requires=nothing timeout=60s 	op monitor interval=60m timeout=60s
+.INP: primitive st2 stonith:ssh 	params hostlist='node1 node2'
+.INP: primitive d1 ocf:pacemaker:Dummy 	operations $id=d1-ops 	op monitor interval=60m 	op monitor interval=120m OCF_CHECK_LEVEL=10
+.INP: monitor d1 60s:30s
+.INP: primitive d2 ocf:heartbeat:Delay 	params mondelay=60 	op start timeout=60s 	op stop timeout=60s
+.INP: monitor d2:Started 60s:30s
+.INP: group g1 d1 d2
+.INP: primitive d3 ocf:pacemaker:Dummy
+.INP: clone c d3 	meta clone-max=1
+.INP: primitive d4 ocf:pacemaker:Dummy
+.INP: ms m d4
+.INP: delete m
+.INP: master m d4
+.INP: primitive s5 ocf:pacemaker:Stateful 	operations $id-ref=d1-ops
+.INP: primitive s6 ocf:pacemaker:Stateful 	operations $id-ref=d1
+.INP: ms m5 s5
+.INP: ms m6 s6
+.INP: primitive d7 Dummy 	params rule inf: #uname eq node1 fake=1 	params rule inf: #uname eq node2 fake=2
+.INP: location l1 g1 100: node1
+.INP: location l2 c 	rule $id=l2-rule1 100: #uname eq node1
+.INP: location l3 m5 	rule inf: #uname eq node1 and pingd gt 0
+.INP: location l4 m5 	rule -inf: not_defined pingd or pingd lte 0
+.INP: location l5 m5 	rule -inf: not_defined pingd or pingd lte 0 	rule inf: #uname eq node1 and pingd gt 0 	rule inf: date lt "2009-05-26" and 		date in start="2009-05-26" end="2009-07-26" and 		date in start="2009-05-26" years="2009" and 		date spec years="2009" hours="09-17"
+.INP: location l6 m5 	rule $id-ref=l2-rule1
+.INP: location l7 m5 	rule $id-ref=l2
+.INP: collocation c1 inf: m6 m5
+.INP: collocation c2 inf: m5:Master d1:Started
+.INP: order o1 Mandatory: m5 m6
+.INP: order o2 Optional: d1:start m5:promote
+.INP: order o3 Serialize: m5 m6
+.INP: order o4 inf: m5 m6
+.INP: rsc_ticket ticket-A_m6 ticket-A: m6
+.INP: rsc_ticket ticket-B_m6_m5 ticket-B: m6 m5 loss-policy=fence
+.INP: rsc_ticket ticket-C_master ticket-C: m6 m5:Master loss-policy=fence
+.INP: fencing_topology st st2
+.INP: property stonith-enabled=true
+.INP: property $id=cpset2 maintenance-mode=true
+.INP: rsc_defaults failure-timeout=10m
+.INP: op_defaults $id=opsdef2 rule 100: #uname eq node1 record-pending=true
+.INP: tag t1: m5 m6
+.INP: _test
+.INP: verify
+.EXT crm_resource --show-metadata stonith:ssh
+.EXT stonithd metadata
+.EXT crm_resource --show-metadata ocf:pacemaker:Dummy
+.EXT crm_resource --show-metadata ocf:heartbeat:Delay
+.EXT crm_resource --show-metadata ocf:pacemaker:Stateful
+.EXT crm_resource --show-metadata ocf:heartbeat:Dummy
+.EXT pengine metadata
+.EXT crmd metadata
+.EXT cib metadata
+.INP: show
+node node1 \
+	attributes mem=16G
+node node2 \
+	utilization cpu=4
+primitive d1 ocf:pacemaker:Dummy \
+	operations $id=d1-ops \
+	op monitor interval=60m \
+	op monitor interval=120m OCF_CHECK_LEVEL=10 \
+	op monitor interval=60s timeout=30s
+primitive d2 Delay \
+	params mondelay=60 \
+	op start timeout=60s interval=0 \
+	op stop timeout=60s interval=0 \
+	op monitor role=Started interval=60s timeout=30s
+primitive d3 ocf:pacemaker:Dummy
+primitive d4 ocf:pacemaker:Dummy
+primitive d7 Dummy \
+	params rule #uname eq node1 fake=1 \
+	params rule #uname eq node2 fake=2
+primitive s5 ocf:pacemaker:Stateful \
+	operations  $id-ref=d1-ops
+primitive s6 ocf:pacemaker:Stateful \
+	operations  $id-ref=d1-ops
+primitive st stonith:ssh \
+	params hostlist="node1 node2" \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+primitive st2 stonith:ssh \
+	params hostlist="node1 node2"
+group g1 d1 d2
+ms m d4
+ms m5 s5
+ms m6 s6
+clone c d3 \
+	meta clone-max=1
+location l1 g1 100: node1
+location l2 c \
+	rule $id=l2-rule1 100: #uname eq node1
+location l3 m5 \
+	rule #uname eq node1 and pingd gt 0
+location l4 m5 \
+	rule -inf: not_defined pingd or pingd lte 0
+location l5 m5 \
+	rule -inf: not_defined pingd or pingd lte 0 \
+	rule #uname eq node1 and pingd gt 0 \
+	rule date lt 2009-05-26 and date in start=2009-05-26 end=2009-07-26 and date in start=2009-05-26 years=2009 and date spec years=2009 hours=09-17
+location l6 m5 \
+	rule $id-ref=l2-rule1
+location l7 m5 \
+	rule $id-ref=l2-rule1
+colocation c1 inf: m6 m5
+colocation c2 inf: m5:Master d1:Started
+order o1 Mandatory: m5 m6
+order o2 Optional: d1:start m5:promote
+order o3 Serialize: m5 m6
+order o4 inf: m5 m6
+rsc_ticket ticket-A_m6 ticket-A: m6
+rsc_ticket ticket-B_m6_m5 ticket-B: m6 m5 loss-policy=fence
+rsc_ticket ticket-C_master ticket-C: m6 m5:Master loss-policy=fence
+fencing_topology st st2
+property cib-bootstrap-options: \
+	stonith-enabled=true
+property cpset2: \
+	maintenance-mode=true
+rsc_defaults rsc-options: \
+	failure-timeout=10m
+op_defaults opsdef2: \
+	rule 100: #uname eq node1 \
+	record-pending=true
+tag t1: m5 m6
+.INP: commit
diff --git a/test/testcases/delete b/test/testcases/delete
new file mode 100644
index 0000000..7bd759e
--- /dev/null
+++ b/test/testcases/delete
@@ -0,0 +1,64 @@
+session Delete/Rename test
+configure
+# erase to start from scratch
+erase
+erase nodes
+node node1
+# create one stonith so that verify does not complain
+primitive st stonith:ssh \
+	params hostlist='node1' \
+	meta target-role="Started" \
+	op start requires=nothing timeout=60s \
+	op monitor interval=60m timeout=60s
+primitive d1 ocf:pacemaker:Dummy
+primitive d2 ocf:pacemaker:Dummy
+location d1-pref d1 100: node1
+show
+_test
+rename d1 p1
+show
+# delete primitive
+delete d2
+_test
+show
+# delete primitive with constraint
+delete p1
+_test
+show
+primitive d1 ocf:pacemaker:Dummy
+location d1-pref d1 100: node1
+_test
+# delete primitive belonging to a group
+primitive d2 ocf:pacemaker:Dummy
+_test
+group g1 d2 d1
+delete d2
+show
+_test
+delete g1
+show
+verify
+# delete a group which is in a clone
+primitive d2 ocf:pacemaker:Dummy
+group g1 d2 d1
+clone c1 g1
+delete g1
+show
+_test
+group g1 d2 d1
+clone c1 g1
+_test
+# delete group from a clone (again)
+delete g1
+show
+_test
+group g1 d2 d1
+clone c1 g1
+# delete primitive and its group and their clone
+delete d2 d1 c1 g1
+show
+_test
+# verify
+verify
+commit
+.
diff --git a/test/testcases/delete.exp b/test/testcases/delete.exp
new file mode 100644
index 0000000..47f7153
--- /dev/null
+++ b/test/testcases/delete.exp
@@ -0,0 +1,153 @@
+.TRY Delete/Rename test
+.INP: configure
+.INP: # erase to start from scratch
+.INP: erase
+.INP: erase nodes
+.INP: node node1
+.INP: # create one stonith so that verify does not complain
+.INP: primitive st stonith:ssh 	params hostlist='node1' 	meta target-role="Started" 	op start requires=nothing timeout=60s 	op monitor interval=60m timeout=60s
+.INP: primitive d1 ocf:pacemaker:Dummy
+.INP: primitive d2 ocf:pacemaker:Dummy
+.INP: location d1-pref d1 100: node1
+.INP: show
+node node1
+primitive d1 ocf:pacemaker:Dummy
+primitive d2 ocf:pacemaker:Dummy
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+location d1-pref d1 100: node1
+.INP: _test
+.INP: rename d1 p1
+INFO: 13: resource references in location:d1-pref updated
+.INP: show
+node node1
+primitive d2 ocf:pacemaker:Dummy
+primitive p1 ocf:pacemaker:Dummy
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+location d1-pref p1 100: node1
+.INP: # delete primitive
+.INP: delete d2
+.INP: _test
+.INP: show
+node node1
+primitive p1 ocf:pacemaker:Dummy
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+location d1-pref p1 100: node1
+.INP: # delete primitive with constraint
+.INP: delete p1
+INFO: 20: hanging location:d1-pref deleted
+.INP: _test
+.INP: show
+node node1
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+.INP: primitive d1 ocf:pacemaker:Dummy
+.INP: location d1-pref d1 100: node1
+.INP: _test
+.INP: # delete primitive belonging to a group
+.INP: primitive d2 ocf:pacemaker:Dummy
+.INP: _test
+.INP: group g1 d2 d1
+INFO: 29: resource references in location:d1-pref updated
+.INP: delete d2
+.INP: show
+node node1
+primitive d1 ocf:pacemaker:Dummy
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+group g1 d1
+location d1-pref g1 100: node1
+.INP: _test
+.INP: delete g1
+INFO: 33: resource references in location:d1-pref updated
+.INP: show
+node node1
+primitive d1 ocf:pacemaker:Dummy
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+location d1-pref d1 100: node1
+.INP: verify
+.EXT crm_resource --show-metadata stonith:ssh
+.EXT stonithd metadata
+.EXT crm_resource --show-metadata ocf:pacemaker:Dummy
+.EXT pengine metadata
+.INP: # delete a group which is in a clone
+.INP: primitive d2 ocf:pacemaker:Dummy
+.INP: group g1 d2 d1
+INFO: 38: resource references in location:d1-pref updated
+.INP: clone c1 g1
+INFO: 39: resource references in location:d1-pref updated
+.INP: delete g1
+INFO: 40: resource references in location:d1-pref updated
+INFO: 40: resource references in location:d1-pref updated
+.INP: show
+node node1
+primitive d1 ocf:pacemaker:Dummy
+primitive d2 ocf:pacemaker:Dummy
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+location d1-pref d2 100: node1
+.INP: _test
+.INP: group g1 d2 d1
+INFO: 43: resource references in location:d1-pref updated
+.INP: clone c1 g1
+INFO: 44: resource references in location:d1-pref updated
+.INP: _test
+.INP: # delete group from a clone (again)
+.INP: delete g1
+INFO: 47: resource references in location:d1-pref updated
+INFO: 47: resource references in location:d1-pref updated
+.INP: show
+node node1
+primitive d1 ocf:pacemaker:Dummy
+primitive d2 ocf:pacemaker:Dummy
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+location d1-pref d2 100: node1
+.INP: _test
+.INP: group g1 d2 d1
+INFO: 50: resource references in location:d1-pref updated
+.INP: clone c1 g1
+INFO: 51: resource references in location:d1-pref updated
+.INP: # delete primitive and its group and their clone
+.INP: delete d2 d1 c1 g1
+INFO: 53: resource references in location:d1-pref updated
+INFO: 53: resource references in location:d1-pref updated
+INFO: 53: hanging location:d1-pref deleted
+.INP: show
+node node1
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+.INP: _test
+.INP: # verify
+.INP: verify
+.INP: commit
diff --git a/test/testcases/edit b/test/testcases/edit
new file mode 100644
index 0000000..2e2df15
--- /dev/null
+++ b/test/testcases/edit
@@ -0,0 +1,59 @@
+show Configuration editing
+property default-action-timeout=2m
+node node1 \
+	attributes mem=16G
+primitive st stonith:null \
+	params hostlist='node1' \
+	meta description="some description here" \
+	op start requires=nothing \
+	op monitor interval=60m
+primitive p1 ocf:heartbeat:Dummy \
+	op monitor interval=60m \
+	op monitor interval=120m OCF_CHECK_LEVEL=10
+filter "sed '$aprimitive p2 ocf:heartbeat:Dummy'"
+filter "sed '$agroup g1 p1 p2'"
+filter "sed 's/p2/p3/;$aprimitive p3 ocf:heartbeat:Dummy'" g1
+filter "sed '$aclone c1 p2'"
+filter "sed 's/p2/g1/'" c1
+filter "sed '/clone/s/g1/p2/'" c1 g1
+filter "sed '/clone/s/p2/g1/;s/p3/p2/'" c1 g1
+filter "sed '1,$d'" c1 g1
+filter "sed -e '$aclone c1 g1' -e '$agroup g1 p1 p2'"
+location l1 p3 100: node1
+order o1 inf: p3 c1
+colocation cl1 inf: c1 p3
+filter "sed '/cl1/s/p3/p2/'"
+filter "sed '/cl1/d'"
+primitive d1 ocf:heartbeat:Dummy
+primitive d2 ocf:heartbeat:Dummy
+primitive d3 ocf:heartbeat:Dummy
+group g2 d1 d2
+filter "sed '/g2/s/d1/p1/;/g1/s/p1/d1/'"
+filter "sed '/g1/s/d1/p1/;/g2/s/p1/d1/'"
+filter "sed '$alocation loc-d1 d1 rule $id=r1 -inf: not_defined webserver rule $id=r2 webserver: defined webserver'"
+filter "sed 's/not_defined webserver/& or mem number:lte 0/'" loc-d1
+filter "sed 's/ or mem number:lte 0//'" loc-d1
+filter "sed 's/not_defined webserver/& rule -inf: not_defined a2/'" loc-d1
+filter "sed 's/not_defined webserver/& or mem number:lte 0/'" loc-d1
+modgroup g1 add d3
+modgroup g1 remove p1
+modgroup g1 add p1 after p2
+modgroup g1 remove p1
+modgroup g1 add p1 before p2
+modgroup g1 add p1
+modgroup g1 remove st
+modgroup g1 remove c1
+modgroup g1 remove nosuch
+modgroup g1 add c1
+modgroup g1 add nosuch
+filter "sed 's/^/# this is a comment\\n/'" loc-d1
+_test
+verify
+.
+configure rsc_defaults $id="rsc_options" failure-timeout=10m
+configure filter "sed 's/2m/60s/'" cib-bootstrap-options
+configure show rsc_options
+configure property stonith-enabled=true
+configure show cib-bootstrap-options
+configure filter 'sed "s/stonith-enabled=true//"'
+configure show cib-bootstrap-options
diff --git a/test/testcases/edit.excl b/test/testcases/edit.excl
new file mode 100644
index 0000000..3589a25
--- /dev/null
+++ b/test/testcases/edit.excl
@@ -0,0 +1 @@
+^\.EXT sed \-[re] ['][^']
diff --git a/test/testcases/edit.exp b/test/testcases/edit.exp
new file mode 100644
index 0000000..b0b1e37
--- /dev/null
+++ b/test/testcases/edit.exp
@@ -0,0 +1,113 @@
+.TRY Configuration editing
+.INP: configure
+.INP: _regtest on
+.INP: erase
+.INP: erase nodes
+.INP: property default-action-timeout=2m
+.INP: node node1 	attributes mem=16G
+.INP: primitive st stonith:null 	params hostlist='node1' 	meta description="some description here" 	op start requires=nothing 	op monitor interval=60m
+.INP: primitive p1 ocf:heartbeat:Dummy 	op monitor interval=60m 	op monitor interval=120m OCF_CHECK_LEVEL=10
+.INP: filter "sed '$aprimitive p2 ocf:heartbeat:Dummy'"
+.INP: filter "sed '$agroup g1 p1 p2'"
+.INP: filter "sed 's/p2/p3/;$aprimitive p3 ocf:heartbeat:Dummy'" g1
+.INP: filter "sed '$aclone c1 p2'"
+.INP: filter "sed 's/p2/g1/'" c1
+.INP: filter "sed '/clone/s/g1/p2/'" c1 g1
+.INP: filter "sed '/clone/s/p2/g1/;s/p3/p2/'" c1 g1
+.INP: filter "sed '1,$d'" c1 g1
+.INP: filter "sed -e '$aclone c1 g1' -e '$agroup g1 p1 p2'"
+.INP: location l1 p3 100: node1
+.INP: order o1 inf: p3 c1
+.INP: colocation cl1 inf: c1 p3
+.INP: filter "sed '/cl1/s/p3/p2/'"
+.INP: filter "sed '/cl1/d'"
+.INP: primitive d1 ocf:heartbeat:Dummy
+.INP: primitive d2 ocf:heartbeat:Dummy
+.INP: primitive d3 ocf:heartbeat:Dummy
+.INP: group g2 d1 d2
+.INP: filter "sed '/g2/s/d1/p1/;/g1/s/p1/d1/'"
+ERROR: 27: Cannot create group:g1: Child primitive:d1 already in group:g2
+.INP: filter "sed '/g1/s/d1/p1/;/g2/s/p1/d1/'"
+.INP: filter "sed '$alocation loc-d1 d1 rule $id=r1 -inf: not_defined webserver rule $id=r2 webserver: defined webserver'"
+.INP: filter "sed 's/not_defined webserver/& or mem number:lte 0/'" loc-d1
+.INP: filter "sed 's/ or mem number:lte 0//'" loc-d1
+.INP: filter "sed 's/not_defined webserver/& rule -inf: not_defined a2/'" loc-d1
+.INP: filter "sed 's/not_defined webserver/& or mem number:lte 0/'" loc-d1
+.INP: modgroup g1 add d3
+.INP: modgroup g1 remove p1
+.INP: modgroup g1 add p1 after p2
+.INP: modgroup g1 remove p1
+.INP: modgroup g1 add p1 before p2
+.INP: modgroup g1 add p1
+ERROR: 1: syntax in group: child p1 listed more than once in group g1 parsing 'group g1 p1 p2 d3 p1'
+.INP: modgroup g1 remove st
+ERROR: 40: configure.modgroup: st is not member of g1
+.INP: modgroup g1 remove c1
+ERROR: 41: configure.modgroup: c1 is not member of g1
+.INP: modgroup g1 remove nosuch
+ERROR: 42: configure.modgroup: nosuch is not member of g1
+.INP: modgroup g1 add c1
+ERROR: 43: a group may contain only primitives; c1 is clone
+.INP: modgroup g1 add nosuch
+ERROR: 44: g1 refers to missing object nosuch
+.INP: filter "sed 's/^/# this is a comment\n/'" loc-d1
+.INP: _test
+.INP: verify
+.EXT crm_resource --show-metadata stonith:null
+.EXT stonithd metadata
+.EXT crm_resource --show-metadata ocf:heartbeat:Dummy
+.EXT crmd metadata
+.EXT pengine metadata
+.EXT cib metadata
+.INP: show
+node node1 \
+	attributes mem=16G
+primitive d1 Dummy
+primitive d2 Dummy
+primitive d3 Dummy
+primitive p1 Dummy \
+	op monitor interval=60m \
+	op monitor interval=120m OCF_CHECK_LEVEL=10
+primitive p2 Dummy
+primitive p3 Dummy
+primitive st stonith:null \
+	params hostlist=node1 \
+	meta description="some description here" \
+	op start requires=nothing interval=0 \
+	op monitor interval=60m
+group g1 p1 p2 d3
+group g2 d1 d2
+clone c1 g1
+location l1 p3 100: node1
+# this is a comment
+location loc-d1 d1 \
+	rule -inf: not_defined webserver or mem number:lte 0 \
+	rule -inf: not_defined a2 \
+	rule webserver: defined webserver
+order o1 inf: p3 c1
+property cib-bootstrap-options: \
+	default-action-timeout=2m
+.INP: commit
+.TRY configure rsc_defaults $id="rsc_options" failure-timeout=10m
+.TRY configure filter "sed 's/2m/60s/'" cib-bootstrap-options
+.EXT crmd metadata
+.EXT pengine metadata
+.EXT cib metadata
+.TRY configure show rsc_options
+rsc_defaults rsc_options: \
+	failure-timeout=10m
+.TRY configure property stonith-enabled=true
+.EXT crmd metadata
+.EXT pengine metadata
+.EXT cib metadata
+.TRY configure show cib-bootstrap-options
+property cib-bootstrap-options: \
+	default-action-timeout=60s \
+	stonith-enabled=true
+.TRY configure filter 'sed "s/stonith-enabled=true//"'
+.EXT crmd metadata
+.EXT pengine metadata
+.EXT cib metadata
+.TRY configure show cib-bootstrap-options
+property cib-bootstrap-options: \
+	default-action-timeout=60s
diff --git a/test/testcases/file b/test/testcases/file
new file mode 100644
index 0000000..5f215b7
--- /dev/null
+++ b/test/testcases/file
@@ -0,0 +1,14 @@
+configure save sample.txt
+%ext cat sample.txt
+configure erase nodes
+configure load replace sample.txt
+%ext sed -i 's/60s/2m/' sample.txt
+%ext sed -i '8a # comment' sample.txt
+session Load update
+configure
+delete m1 p1
+property cluster-recheck-interval="10m"
+load update sample.txt
+.
+configure show
+%ext rm sample.txt
diff --git a/test/testcases/file.exp b/test/testcases/file.exp
new file mode 100644
index 0000000..7b7dd07
--- /dev/null
+++ b/test/testcases/file.exp
@@ -0,0 +1,53 @@
+.TRY configure save sample.txt
+.EXT cat sample.txt
+node node1
+primitive p0 ocf:pacemaker:Dummy
+primitive p1 ocf:pacemaker:Dummy
+primitive p2 Delay \
+	params startdelay=2 mondelay=2 stopdelay=2
+primitive p3 ocf:pacemaker:Dummy
+primitive st stonith:null \
+	params hostlist=node1
+ms m1 p2
+clone c1 p1
+property cib-bootstrap-options: \
+	default-action-timeout=60s
+.TRY configure erase nodes
+.TRY configure load replace sample.txt
+.EXT crm_resource --show-metadata ocf:pacemaker:Dummy
+.EXT crm_resource --show-metadata ocf:heartbeat:Delay
+.EXT crm_resource --show-metadata stonith:null
+.EXT stonithd metadata
+.EXT crmd metadata
+.EXT pengine metadata
+.EXT cib metadata
+.EXT sed -i 's/60s/2m/' sample.txt
+.EXT sed -i '8a # comment' sample.txt
+.TRY Load update
+.INP: configure
+.INP: delete m1 p1
+.INP: property cluster-recheck-interval="10m"
+.INP: load update sample.txt
+.EXT crm_resource --show-metadata ocf:pacemaker:Dummy
+.EXT crm_resource --show-metadata ocf:heartbeat:Delay
+.EXT crm_resource --show-metadata stonith:null
+.EXT stonithd metadata
+.EXT crmd metadata
+.EXT pengine metadata
+.EXT cib metadata
+.TRY configure show
+node node1
+primitive p0 ocf:pacemaker:Dummy
+primitive p1 ocf:pacemaker:Dummy
+primitive p2 Delay \
+	params startdelay=2 mondelay=2 stopdelay=2
+primitive p3 ocf:pacemaker:Dummy
+primitive st stonith:null \
+	params hostlist=node1
+# comment
+ms m1 p2
+clone c1 p1
+property cib-bootstrap-options: \
+	default-action-timeout=2m \
+	cluster-recheck-interval=10m
+.EXT rm sample.txt
diff --git a/test/testcases/history b/test/testcases/history
new file mode 100644
index 0000000..3fc84f3
--- /dev/null
+++ b/test/testcases/history
@@ -0,0 +1,40 @@
+session History
+history
+source history-test.tar.bz2
+info
+node xen-d
+node xen-e
+node .*
+exclude pcmk_peer_update
+exclude
+node xen-e
+exclude clear
+exclude corosync|crmd|pengine|stonith-ng|cib|attrd|mgmtd|sshd
+log
+exclude clear
+peinputs
+peinputs v
+refresh
+resource d1
+# reduce report span
+timeframe "2012-12-14 20:07:30"
+peinputs
+resource d1
+exclude corosync|crmd|pengine|stonith-ng|cib|attrd|mgmtd|sshd
+transition log
+transition nograph
+transition -1 nograph
+transition save 0 _crmsh_regtest
+transition log 49
+# reset timeframe
+timeframe
+session save _crmsh_regtest
+session load _crmsh_regtest
+session
+session pack
+.
+session History 2
+history
+session load _crmsh_regtest
+exclude
+.
diff --git a/test/testcases/history.excl b/test/testcases/history.excl
new file mode 100644
index 0000000..a862e05
--- /dev/null
+++ b/test/testcases/history.excl
@@ -0,0 +1 @@
+^ptest.*:
diff --git a/test/testcases/history.exp b/test/testcases/history.exp
new file mode 100644
index 0000000..95fdeda
--- /dev/null
+++ b/test/testcases/history.exp
@@ -0,0 +1,288 @@
+.TRY History
+.INP: history
+.INP: source history-test.tar.bz2
+.INP: info
+.EXT tar -xj < history-test.tar.bz2
+WARNING: 3: end of transition xen-e:pe-input-47 not found in logs (transition not complete yet?)
+Source: history-test.tar.bz2
+Created on: Fri 14 Dec 19:08:38 UTC 2012
+By: unknown
+Period: 2012-12-14 20:06:34 - 2012-12-14 20:08:44
+Nodes: xen-d xen-e
+Groups: 
+Resources: d1 s-libvirt
+Transitions: 43 44 45 46 47 48 272 49 50
+.INP: node xen-d
+Dec 14 20:06:35 xen-d corosync[5649]:  [MAIN  ] Corosync Cluster Engine ('1.4.3'): started and ready to provide service.
+Dec 14 20:06:36 xen-d corosync[5649]:  [pcmk  ] info: pcmk_peer_update: memb: xen-d 906822154
+Dec 14 20:07:54 xen-e corosync[24218]:  [pcmk  ] info: pcmk_peer_update: lost: xen-d 906822154
+Dec 14 20:07:54 xen-e pengine: [24227]: WARN: pe_fence_node: Node xen-d will be fenced because it is un-expectedly down
+Dec 14 20:07:54 xen-e crmd: [24228]: notice: te_fence_node: Executing reboot fencing operation (12) on xen-d (timeout=60000)
+Dec 14 20:07:54 xen-e pengine: [24227]: WARN: stage6: Scheduling Node xen-d for STONITH
+Dec 14 20:07:56 xen-e stonith-ng: [24224]: notice: log_operation: Operation 'reboot' [24519] (call 0 from c0c111a5-d332-48f7-9375-739b91e04f0e) for host 'xen-d' with device 's-libvirt' returned: 0
+Dec 14 20:08:23 xen-d corosync[1874]:  [MAIN  ] Corosync Cluster Engine ('1.4.3'): started and ready to provide service.
+Dec 14 20:08:23 xen-d corosync[1874]:  [pcmk  ] info: pcmk_peer_update: memb: xen-d 906822154
+.INP: node xen-e
+Dec 14 20:06:35 xen-e corosync[24218]:  [MAIN  ] Corosync Cluster Engine ('1.4.3'): started and ready to provide service.
+Dec 14 20:07:54 xen-e corosync[24218]:  [pcmk  ] info: pcmk_peer_update: memb: xen-e 923599370
+Dec 14 20:08:40 xen-e corosync[24218]:  [pcmk  ] info: pcmk_peer_update: memb: xen-e 923599370
+.INP: node .*
+Dec 14 20:06:35 xen-e corosync[24218]:  [MAIN  ] Corosync Cluster Engine ('1.4.3'): started and ready to provide service.
+Dec 14 20:06:35 xen-d corosync[5649]:  [MAIN  ] Corosync Cluster Engine ('1.4.3'): started and ready to provide service.
+Dec 14 20:06:36 xen-d corosync[5649]:  [pcmk  ] info: pcmk_peer_update: memb: xen-d 906822154
+Dec 14 20:07:54 xen-e corosync[24218]:  [pcmk  ] info: pcmk_peer_update: memb: xen-e 923599370
+Dec 14 20:07:54 xen-e corosync[24218]:  [pcmk  ] info: pcmk_peer_update: lost: xen-d 906822154
+Dec 14 20:07:54 xen-e pengine: [24227]: WARN: pe_fence_node: Node xen-d will be fenced because it is un-expectedly down
+Dec 14 20:07:54 xen-e crmd: [24228]: notice: te_fence_node: Executing reboot fencing operation (12) on xen-d (timeout=60000)
+Dec 14 20:07:54 xen-e pengine: [24227]: WARN: stage6: Scheduling Node xen-d for STONITH
+Dec 14 20:07:56 xen-e stonith-ng: [24224]: notice: log_operation: Operation 'reboot' [24519] (call 0 from c0c111a5-d332-48f7-9375-739b91e04f0e) for host 'xen-d' with device 's-libvirt' returned: 0
+Dec 14 20:08:23 xen-d corosync[1874]:  [MAIN  ] Corosync Cluster Engine ('1.4.3'): started and ready to provide service.
+Dec 14 20:08:23 xen-d corosync[1874]:  [pcmk  ] info: pcmk_peer_update: memb: xen-d 906822154
+Dec 14 20:08:40 xen-e corosync[24218]:  [pcmk  ] info: pcmk_peer_update: memb: xen-e 923599370
+.INP: exclude pcmk_peer_update
+.INP: exclude
+pcmk_peer_update
+.INP: node xen-e
+Dec 14 20:06:35 xen-e corosync[24218]:  [MAIN  ] Corosync Cluster Engine ('1.4.3'): started and ready to provide service.
+.INP: exclude clear
+.INP: exclude corosync|crmd|pengine|stonith-ng|cib|attrd|mgmtd|sshd
+.INP: log
+Dec 14 20:06:35 xen-d lrmd: [5657]: info: max-children set to 4 (2 processors online)
+Dec 14 20:06:35 xen-d lrmd: [5657]: info: enabling coredumps
+Dec 14 20:06:35 xen-d lrmd: [5657]: info: Started.
+Dec 14 20:06:35 xen-e lrmd: [24225]: info: max-children set to 4 (2 processors online)
+Dec 14 20:06:35 xen-e lrmd: [24225]: info: enabling coredumps
+Dec 14 20:06:35 xen-e lrmd: [24225]: info: Started.
+Dec 14 20:06:36 xen-e lrmd: [24225]: info: max-children already set to 4
+Dec 14 20:06:37 xen-d lrmd: [5657]: info: max-children already set to 4
+Dec 14 20:07:19 xen-d lrmd: [5657]: info: rsc:d1 probe[2] (pid 5812)
+Dec 14 20:07:19 xen-d lrmd: [5657]: notice: lrmd_rsc_new(): No lrm_rprovider field in message
+Dec 14 20:07:19 xen-d lrmd: [5657]: info: rsc:s-libvirt probe[3] (pid 5813)
+Dec 14 20:07:19 xen-d lrmd: [5657]: info: operation monitor[3] on s-libvirt for client 5660: pid 5813 exited with return code 7
+Dec 14 20:07:19 xen-d lrmd: [5657]: info: operation monitor[2] on d1 for client 5660: pid 5812 exited with return code 7
+Dec 14 20:07:19 xen-d lrmd: [5657]: info: rsc:d1 start[4] (pid 5833)
+Dec 14 20:07:19 xen-d lrmd: [5657]: info: operation start[4] on d1 for client 5660: pid 5833 exited with return code 0
+Dec 14 20:07:19 xen-d lrmd: [5657]: info: rsc:d1 monitor[5] (pid 5840)
+Dec 14 20:07:19 xen-d lrmd: [5657]: info: operation monitor[5] on d1 for client 5660: pid 5840 exited with return code 0
+Dec 14 20:07:19 xen-e lrmd: [24225]: info: rsc:d1 probe[2] (pid 24243)
+Dec 14 20:07:19 xen-e lrmd: [24225]: notice: lrmd_rsc_new(): No lrm_rprovider field in message
+Dec 14 20:07:19 xen-e lrmd: [24225]: info: rsc:s-libvirt probe[3] (pid 24244)
+Dec 14 20:07:19 xen-e lrmd: [24225]: info: operation monitor[3] on s-libvirt for client 24228: pid 24244 exited with return code 7
+Dec 14 20:07:19 xen-e lrmd: [24225]: info: operation monitor[2] on d1 for client 24228: pid 24243 exited with return code 7
+Dec 14 20:07:19 xen-e lrmd: [24225]: info: rsc:s-libvirt start[4] (pid 24264)
+Dec 14 20:07:20 xen-e external/libvirt(s-libvirt)[24271]: [24288]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:07:21 xen-e lrmd: [24225]: info: operation start[4] on s-libvirt for client 24228: pid 24264 exited with return code 0
+Dec 14 20:07:21 xen-e lrmd: [24225]: info: rsc:s-libvirt monitor[5] (pid 24289)
+Dec 14 20:07:22 xen-e external/libvirt(s-libvirt)[24296]: [24313]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:07:23 xen-e lrmd: [24225]: info: operation monitor[5] on s-libvirt for client 24228: pid 24289 exited with return code 0
+Dec 14 20:07:29 xen-e external/libvirt(s-libvirt)[24321]: [24338]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:07:29 xen-d lrmd: [5657]: info: cancel_op: operation monitor[5] on d1 for client 5660, its parameters: CRM_meta_name=[monitor] crm_feature_set=[3.0.6] CRM_meta_timeout=[30000] CRM_meta_interval=[5000]  cancelled
+Dec 14 20:07:29 xen-d lrmd: [5657]: info: rsc:d1 stop[6] (pid 5926)
+Dec 14 20:07:29 xen-d lrmd: [5657]: info: operation stop[6] on d1 for client 5660: pid 5926 exited with return code 0
+Dec 14 20:07:29 xen-d lrmd: [5657]: info: rsc:d1 start[7] (pid 5929)
+Dec 14 20:07:29 xen-d lrmd: [5657]: info: operation start[7] on d1 for client 5660: pid 5929 exited with return code 0
+Dec 14 20:07:29 xen-d lrmd: [5657]: info: rsc:d1 monitor[8] (pid 5938)
+Dec 14 20:07:29 xen-d lrmd: [5657]: info: operation monitor[8] on d1 for client 5660: pid 5938 exited with return code 0
+Dec 14 20:07:36 xen-e external/libvirt(s-libvirt)[24371]: [24387]: ERROR: Failed to get status for xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen
+Dec 14 20:07:36 xen-e external/libvirt(s-libvirt)[24371]: [24393]: ERROR: setlocale: No such file or directory
+Dec 14 20:07:36 xen-e external/libvirt(s-libvirt)[24371]: [24393]: error: Cannot recv data: Warning: Identity file /root/.ssh/xen not accessible: No such file or directory.
+Dec 14 20:07:36 xen-e external/libvirt(s-libvirt)[24371]: [24393]: Permission denied (publickey,keyboard-interactive). : Connection reset by peer
+Dec 14 20:07:36 xen-e external/libvirt(s-libvirt)[24371]: [24393]: error: failed to connect to the hypervisor
+Dec 14 20:07:37 xen-e stonith: [24366]: WARN: external_status: 'libvirt status' failed with rc 1
+Dec 14 20:07:37 xen-e stonith: [24366]: ERROR: external/libvirt device not accessible.
+Dec 14 20:07:37 xen-e lrm-stonith: [24364]: WARN: map_ra_retvalue: Mapped the invalid return code -2.
+Dec 14 20:07:37 xen-e lrmd: [24225]: info: cancel_op: operation monitor[5] on s-libvirt for client 24228, its parameters: CRM_meta_name=[monitor] crm_feature_set=[3.0.6] CRM_meta_timeout=[60000] CRM_meta_interval=[5000] hostlist=[xen-d xen-e] hypervisor_uri=[xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen] reset_method=[reboot]  cancelled
+Dec 14 20:07:37 xen-e lrmd: [24225]: info: rsc:s-libvirt stop[6] (pid 24417)
+Dec 14 20:07:37 xen-e lrmd: [24225]: info: operation stop[6] on s-libvirt for client 24228: pid 24417 exited with return code 0
+Dec 14 20:07:37 xen-e lrmd: [24225]: info: rsc:s-libvirt start[7] (pid 24418)
+Dec 14 20:07:39 xen-e external/libvirt(s-libvirt)[24425]: [24442]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:07:40 xen-e lrmd: [24225]: info: operation start[7] on s-libvirt for client 24228: pid 24418 exited with return code 0
+Dec 14 20:07:40 xen-e lrmd: [24225]: info: rsc:s-libvirt monitor[8] (pid 24456)
+Dec 14 20:07:41 xen-e external/libvirt(s-libvirt)[24463]: [24480]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:07:42 xen-e lrmd: [24225]: info: operation monitor[8] on s-libvirt for client 24228: pid 24456 exited with return code 0
+Dec 14 20:07:44 xen-d lrmd: [5657]: info: cancel_op: operation monitor[8] on d1 for client 5660, its parameters: CRM_meta_name=[monitor] crm_feature_set=[3.0.6] CRM_meta_timeout=[30000] CRM_meta_interval=[5000]  cancelled
+Dec 14 20:07:48 xen-e external/libvirt(s-libvirt)[24488]: [24505]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:07:55 xen-e external/libvirt(s-libvirt)[24525]: [24540]: notice: Domain xen-d was rebooted
+Dec 14 20:07:55 xen-d shutdown[6093]: shutting down for system reboot
+Dec 14 20:07:55 xen-d init: Switching to runlevel: 6
+Dec 14 20:07:56 xen-e lrmd: [24225]: info: rsc:d1 start[9] (pid 24568)
+Dec 14 20:07:56 xen-e lrmd: [24225]: info: operation start[9] on d1 for client 24228: pid 24568 exited with return code 0
+Dec 14 20:07:57 xen-e lrmd: [24225]: info: rsc:d1 monitor[10] (pid 24577)
+Dec 14 20:07:57 xen-e lrmd: [24225]: info: operation monitor[10] on d1 for client 24228: pid 24577 exited with return code 0
+Dec 14 20:07:57 xen-e external/libvirt(s-libvirt)[24555]: [24588]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:07:57 xen-d logd: [6194]: debug: Stopping ha_logd with pid 1787
+Dec 14 20:07:57 xen-d logd: [6194]: info: Waiting for pid=1787 to exit
+Dec 14 20:07:57 xen-d logd: [1787]: debug: logd_term_action: received SIGTERM
+Dec 14 20:07:57 xen-d logd: [1787]: debug: logd_term_action: waiting for 0 messages to be read for process lrmd
+Dec 14 20:07:57 xen-d logd: [1787]: debug: logd_term_action: waiting for 0 messages to be read by write process
+Dec 14 20:07:57 xen-d logd: [1787]: debug: logd_term_action: sending SIGTERM to write process
+Dec 14 20:07:57 xen-d logd: [1790]: info: logd_term_write_action: received SIGTERM
+Dec 14 20:07:57 xen-d logd: [1790]: debug: Writing out 0 messages then quitting
+Dec 14 20:07:57 xen-d logd: [1790]: info: Exiting write process
+Dec 14 20:07:57 xen-d haveged: haveged stopping due to signal 15
+Dec 14 20:07:58 xen-d logd: [6194]: info: Pid 1787 exited
+Dec 14 20:07:58 xen-d rpcbind: rpcbind terminating on signal. Restart with "rpcbind -w"
+Dec 14 20:07:58 xen-d kernel: Kernel logging (proc) stopped.
+Dec 14 20:07:58 xen-d kernel: Kernel log daemon terminating.
+Dec 14 20:07:58 xen-d syslog-ng[1679]: Termination requested via signal, terminating;
+Dec 14 20:07:58 xen-d syslog-ng[1679]: syslog-ng shutting down; version='2.0.9'
+Dec 14 20:08:07 xen-e external/libvirt(s-libvirt)[24599]: [24616]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:08:15 xen-e external/libvirt(s-libvirt)[24630]: [24647]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:08:21 xen-d syslog-ng[1681]: syslog-ng starting up; version='2.0.9'
+Dec 14 20:08:21 xen-d iscsid: iSCSI logger with pid=1682 started!
+Dec 14 20:08:22 xen-d sm-notify[1716]: Version 1.2.3 starting
+Dec 14 20:08:22 xen-d haveged: haveged starting up
+Dec 14 20:08:22 xen-d haveged: arch:        x86 vendor:      amd generic:     0 i_cache:     64 d_cache:     64 loop_idx:    20 loop_idxmax: 40 loop_sz:     63724 loop_szmax:  124334 etime:       18207 havege_ndpt  0
+Dec 14 20:08:22 xen-d logd: [1789]: info: setting log facility to daemon
+Dec 14 20:08:22 xen-d logd: [1789]: info: logd started with /etc/logd.cf.
+Dec 14 20:08:22 xen-d iscsid: transport class version 2.0-870. iscsid version 2.0-872.suse
+Dec 14 20:08:22 xen-d iscsid: iSCSI daemon with pid=1683 started!
+Dec 14 20:08:22 xen-e external/libvirt(s-libvirt)[24658]: [24678]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:08:23 xen-d lrmd: [1945]: info: max-children set to 4 (2 processors online)
+Dec 14 20:08:23 xen-d lrmd: [1945]: info: enabling coredumps
+Dec 14 20:08:23 xen-d lrmd: [1945]: info: Started.
+Dec 14 20:08:25 xen-d lrmd: [1945]: info: max-children already set to 4
+Dec 14 20:08:25 xen-d ntpd[2127]: ntpd 4.2.4p8 at 1.1612-o Thu Nov 10 17:10:45 UTC 2011 (1)
+Dec 14 20:08:25 xen-d ntpd[2128]: precision = 2.000 usec
+Dec 14 20:08:25 xen-d ntpd[2128]: ntp_io: estimated max descriptors: 1024, initial socket boundary: 16
+Dec 14 20:08:25 xen-d ntpd[2128]: Listening on interface #0 wildcard, 0.0.0.0#123 Disabled
+Dec 14 20:08:25 xen-d ntpd[2128]: Listening on interface #1 wildcard, ::#123 Disabled
+Dec 14 20:08:25 xen-d ntpd[2128]: Listening on interface #2 eth0, fe80::216:3eff:fe65:738a#123 Enabled
+Dec 14 20:08:25 xen-d ntpd[2128]: Listening on interface #3 lo, ::1#123 Enabled
+Dec 14 20:08:25 xen-d ntpd[2128]: Listening on interface #4 lo, 127.0.0.1#123 Enabled
+Dec 14 20:08:25 xen-d ntpd[2128]: Listening on interface #5 lo, 127.0.0.2#123 Enabled
+Dec 14 20:08:25 xen-d ntpd[2128]: Listening on interface #6 eth0, 10.2.13.54#123 Enabled
+Dec 14 20:08:25 xen-d ntpd[2128]: kernel time sync status 2040
+Dec 14 20:08:25 xen-d ntpd[2128]: frequency initialized 29.933 PPM from /var/lib/ntp/drift/ntp.drift
+Dec 14 20:08:25 xen-d /usr/sbin/cron[2244]: (CRON) STARTUP (V5.0)
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: rsc:d1 probe[2] (pid 2384)
+Dec 14 20:08:26 xen-d lrmd: [1945]: notice: lrmd_rsc_new(): No lrm_rprovider field in message
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: rsc:s-libvirt probe[3] (pid 2385)
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: operation monitor[3] on s-libvirt for client 1948: pid 2385 exited with return code 7
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: operation monitor[2] on d1 for client 1948: pid 2384 exited with return code 7
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: rsc:d1 start[4] (pid 2405)
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: operation start[4] on d1 for client 1948: pid 2405 exited with return code 0
+Dec 14 20:08:26 xen-d kernel: klogd 1.4.1, log source = /proc/kmsg started.
+Dec 14 20:08:26 xen-d kernel: [   22.808182] Loading iSCSI transport class v2.0-870.
+Dec 14 20:08:26 xen-d kernel: [   22.815399] iscsi: registered transport (tcp)
+Dec 14 20:08:26 xen-d kernel: [   23.572989] BIOS EDD facility v0.16 2004-Jun-25, 0 devices found
+Dec 14 20:08:26 xen-d kernel: [   23.573005] EDD information not available.
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: rsc:d1 monitor[5] (pid 2409)
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: operation monitor[5] on d1 for client 1948: pid 2409 exited with return code 0
+Dec 14 20:08:29 xen-e external/libvirt(s-libvirt)[24689]: [24706]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:08:29 xen-d kernel: [   30.841076] eth0: no IPv6 routers present
+Dec 14 20:08:29 xen-d logger: Mark:HB_REPORT:1355512108
+Dec 14 20:08:36 xen-e external/libvirt(s-libvirt)[24717]: [24734]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:08:43 xen-e lrmd: [24225]: info: cancel_op: operation monitor[10] on d1 for client 24228, its parameters: CRM_meta_name=[monitor] crm_feature_set=[3.0.6] CRM_meta_timeout=[30000] CRM_meta_interval=[5000]  cancelled
+Dec 14 20:08:43 xen-e lrmd: [24225]: info: rsc:d1 stop[11] (pid 24774)
+Dec 14 20:08:43 xen-e lrmd: [24225]: info: operation stop[11] on d1 for client 24228: pid 24774 exited with return code 0
+Dec 14 20:08:43 xen-e external/libvirt(s-libvirt)[24748]: [24786]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+.INP: exclude clear
+.INP: peinputs
+history-test/xen-e/pengine/pe-input-43.bz2
+history-test/xen-e/pengine/pe-input-44.bz2
+history-test/xen-e/pengine/pe-input-45.bz2
+history-test/xen-e/pengine/pe-input-46.bz2
+history-test/xen-e/pengine/pe-input-47.bz2
+history-test/xen-e/pengine/pe-input-48.bz2
+history-test/xen-e/pengine/pe-warn-272.bz2
+history-test/xen-e/pengine/pe-input-49.bz2
+history-test/xen-e/pengine/pe-input-50.bz2
+.INP: peinputs v
+Date       Start    End       Filename      Client     User       Origin
+====       =====    ===       ========      ======     ====       ======
+2012-12-14 20:06:57 20:06:57  pe-input-43   crmd       hacluster  xen-e
+2012-12-14 20:07:19 20:07:23  pe-input-44   cibadmin   root       xen-d
+2012-12-14 20:07:29 20:07:29  pe-input-45   cibadmin   root       xen-d
+2012-12-14 20:07:29 20:07:29  pe-input-46   cibadmin   root       xen-d
+2012-12-14 20:07:37 20:07:37  pe-input-47   cibadmin   root       xen-d
+2012-12-14 20:07:37 20:07:42  pe-input-48   cibadmin   root       xen-d
+2012-12-14 20:07:54 20:07:56  pe-warn-272   cibadmin   root       xen-d
+2012-12-14 20:07:56 20:07:57  pe-input-49   cibadmin   root       xen-d
+2012-12-14 20:08:43 20:08:43  pe-input-50   cibadmin   root       xen-d
+.INP: refresh
+INFO: 16: nothing to refresh if source isn't live
+.INP: resource d1
+Dec 14 20:07:19 xen-d lrmd: [5657]: info: rsc:d1 start[4] (pid 5833)
+Dec 14 20:07:19 xen-d crmd: [5660]: info: process_lrm_event: LRM operation d1_start_0 (call=4, rc=0, cib-update=14, confirmed=true) ok
+Dec 14 20:07:29 xen-d lrmd: [5657]: info: rsc:d1 stop[6] (pid 5926)
+Dec 14 20:07:29 xen-d crmd: [5660]: info: process_lrm_event: LRM operation d1_stop_0 (call=6, rc=0, cib-update=17, confirmed=true) ok
+Dec 14 20:07:29 xen-d lrmd: [5657]: info: rsc:d1 start[7] (pid 5929)
+Dec 14 20:07:29 xen-d crmd: [5660]: info: process_lrm_event: LRM operation d1_start_0 (call=7, rc=0, cib-update=18, confirmed=true) ok
+Dec 14 20:07:56 xen-e lrmd: [24225]: info: rsc:d1 start[9] (pid 24568)
+Dec 14 20:07:56 xen-e crmd: [24228]: info: process_lrm_event: LRM operation d1_start_0 (call=9, rc=0, cib-update=96, confirmed=true) ok
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: rsc:d1 start[4] (pid 2405)
+Dec 14 20:08:26 xen-d crmd: [1948]: info: process_lrm_event: LRM operation d1_start_0 (call=4, rc=0, cib-update=9, confirmed=true) ok
+Dec 14 20:08:43 xen-e lrmd: [24225]: info: rsc:d1 stop[11] (pid 24774)
+Dec 14 20:08:43 xen-e crmd: [24228]: info: process_lrm_event: LRM operation d1_stop_0 (call=11, rc=0, cib-update=125, confirmed=true) ok
+.INP: # reduce report span
+.INP: timeframe "2012-12-14 20:07:30"
+WARNING: 19: end of transition xen-e:pe-input-47 not found in logs (transition not complete yet?)
+.INP: peinputs
+history-test/xen-e/pengine/pe-input-47.bz2
+history-test/xen-e/pengine/pe-input-48.bz2
+history-test/xen-e/pengine/pe-warn-272.bz2
+history-test/xen-e/pengine/pe-input-49.bz2
+history-test/xen-e/pengine/pe-input-50.bz2
+.INP: resource d1
+Dec 14 20:07:56 xen-e lrmd: [24225]: info: rsc:d1 start[9] (pid 24568)
+Dec 14 20:07:56 xen-e crmd: [24228]: info: process_lrm_event: LRM operation d1_start_0 (call=9, rc=0, cib-update=96, confirmed=true) ok
+Dec 14 20:08:26 xen-d lrmd: [1945]: info: rsc:d1 start[4] (pid 2405)
+Dec 14 20:08:26 xen-d crmd: [1948]: info: process_lrm_event: LRM operation d1_start_0 (call=4, rc=0, cib-update=9, confirmed=true) ok
+Dec 14 20:08:43 xen-e lrmd: [24225]: info: rsc:d1 stop[11] (pid 24774)
+Dec 14 20:08:43 xen-e crmd: [24228]: info: process_lrm_event: LRM operation d1_stop_0 (call=11, rc=0, cib-update=125, confirmed=true) ok
+.INP: exclude corosync|crmd|pengine|stonith-ng|cib|attrd|mgmtd|sshd
+.INP: transition log
+Dec 14 20:08:43 xen-e lrmd: [24225]: info: cancel_op: operation monitor[10] on d1 for client 24228, its parameters: CRM_meta_name=[monitor] crm_feature_set=[3.0.6] CRM_meta_timeout=[30000] CRM_meta_interval=[5000]  cancelled
+Dec 14 20:08:43 xen-e lrmd: [24225]: info: rsc:d1 stop[11] (pid 24774)
+Dec 14 20:08:43 xen-e lrmd: [24225]: info: operation stop[11] on d1 for client 24228: pid 24774 exited with return code 0
+Dec 14 20:08:43 xen-e external/libvirt(s-libvirt)[24748]: [24786]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+.INP: transition nograph
+INFO: 24: running ptest with history-test/xen-e/pengine/pe-input-50.bz2
+.EXT >/dev/null 2>&1 crm_simulate -x - -S -VV
+Transition xen-e:pe-input-50 (20:08:43 - 20:08:43):
+	total 8 actions: 8 Complete
+Dec 14 20:08:43 xen-e lrmd: [24225]: info: rsc:d1 stop[11] (pid 24774)
+Dec 14 20:08:43 xen-e external/libvirt(s-libvirt)[24748]: [24786]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+.INP: transition -1 nograph
+INFO: 25: running ptest with history-test/xen-e/pengine/pe-input-49.bz2
+.EXT >/dev/null 2>&1 crm_simulate -x - -S -VV
+Transition xen-e:pe-input-49 (20:07:56 - 20:07:57):
+	total 2 actions: 2 Complete
+Dec 14 20:07:56 xen-e lrmd: [24225]: info: rsc:d1 start[9] (pid 24568)
+Dec 14 20:07:57 xen-e external/libvirt(s-libvirt)[24555]: [24588]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+.INP: transition save 0 _crmsh_regtest
+INFO: 26: transition history-test/xen-e/pengine/pe-input-50.bz2 saved to shadow _crmsh_regtest
+.INP: transition log 49
+Dec 14 20:07:56 xen-e lrmd: [24225]: info: rsc:d1 start[9] (pid 24568)
+Dec 14 20:07:56 xen-e lrmd: [24225]: info: operation start[9] on d1 for client 24228: pid 24568 exited with return code 0
+Dec 14 20:07:57 xen-e lrmd: [24225]: info: rsc:d1 monitor[10] (pid 24577)
+Dec 14 20:07:57 xen-e lrmd: [24225]: info: operation monitor[10] on d1 for client 24228: pid 24577 exited with return code 0
+Dec 14 20:07:57 xen-e external/libvirt(s-libvirt)[24555]: [24588]: notice: xen+ssh://hex-12.suse.de/?keyfile=/root/.ssh/xen: Running hypervisor: Xen 4.1.0
+Dec 14 20:07:57 xen-d logd: [6194]: info: Waiting for pid=1787 to exit
+Dec 14 20:07:57 xen-d logd: [1787]: debug: logd_term_action: received SIGTERM
+Dec 14 20:07:57 xen-d logd: [1787]: debug: logd_term_action: waiting for 0 messages to be read for process lrmd
+Dec 14 20:07:57 xen-d logd: [1787]: debug: logd_term_action: waiting for 0 messages to be read by write process
+Dec 14 20:07:57 xen-d logd: [1787]: debug: logd_term_action: sending SIGTERM to write process
+Dec 14 20:07:57 xen-d logd: [1790]: info: logd_term_write_action: received SIGTERM
+Dec 14 20:07:57 xen-d logd: [1790]: debug: Writing out 0 messages then quitting
+Dec 14 20:07:57 xen-d logd: [1790]: info: Exiting write process
+Dec 14 20:07:57 xen-d haveged: haveged stopping due to signal 15
+.INP: # reset timeframe
+.INP: timeframe
+WARNING: 29: end of transition xen-e:pe-input-47 not found in logs (transition not complete yet?)
+.INP: session save _crmsh_regtest
+.EXT mkdir -p /var/cache/crm/history/session/_crmsh_regtest
+.INP: session load _crmsh_regtest
+.INP: session
+current session: _crmsh_regtest
+.INP: session pack
+.EXT tar -C '/var/cache/crm/history/session/_crmsh_regtest/..' -cj -f '/root/_crmsh_regtest.tar.bz2' _crmsh_regtest
+Report saved in '/root/_crmsh_regtest.tar.bz2'
+.TRY History 2
+.INP: history
+.INP: session load _crmsh_regtest
+.EXT tar -xj < history-test.tar.bz2
+WARNING: 2: end of transition xen-e:pe-input-47 not found in logs (transition not complete yet?)
+.INP: exclude
+corosync|crmd|pengine|stonith-ng|cib|attrd|mgmtd|sshd
diff --git a/test/testcases/history.post b/test/testcases/history.post
new file mode 100755
index 0000000..b5bb7fc
--- /dev/null
+++ b/test/testcases/history.post
@@ -0,0 +1,3 @@
+#!/bin/sh
+crm history session delete _crmsh_regtest
+rm -r history-test
diff --git a/test/testcases/history.pre b/test/testcases/history.pre
new file mode 100755
index 0000000..4905f13
--- /dev/null
+++ b/test/testcases/history.pre
@@ -0,0 +1,3 @@
+#!/bin/sh
+crm history session delete _crmsh_regtest
+rm -rf history-test
diff --git a/test/testcases/newfeatures b/test/testcases/newfeatures
new file mode 100644
index 0000000..2262a37
--- /dev/null
+++ b/test/testcases/newfeatures
@@ -0,0 +1,25 @@
+session New features
+configure
+# erase to start from scratch
+erase
+erase nodes
+node node1
+# create one stonith so that verify does not complain
+primitive st stonith:ssh \
+	params hostlist='node1' \
+	meta target-role="Started" \
+	op start requires=nothing timeout=60s \
+	op monitor interval=60m timeout=60s
+primitive p0 Dummy params $p0-state:state=1
+primitive p1 Dummy params \
+    rule role=Started date in start=2009-05-26 end=2010-05-26 or date gt 2014-01-01 \
+    state=2
+primitive p2 Dummy params @p0-state
+property rule #uname eq node1 stonith-enabled=no
+tag tag1: p0 p1 p2
+location l1 { p0 p1 p2 } inf: node1
+show
+_test
+verify
+commit
+.
diff --git a/test/testcases/newfeatures.exp b/test/testcases/newfeatures.exp
new file mode 100644
index 0000000..8e84129
--- /dev/null
+++ b/test/testcases/newfeatures.exp
@@ -0,0 +1,44 @@
+.TRY New features
+.INP: configure
+.INP: # erase to start from scratch
+.INP: erase
+.INP: erase nodes
+.INP: node node1
+.INP: # create one stonith so that verify does not complain
+.INP: primitive st stonith:ssh 	params hostlist='node1' 	meta target-role="Started" 	op start requires=nothing timeout=60s 	op monitor interval=60m timeout=60s
+.INP: primitive p0 Dummy params $p0-state:state=1
+.INP: primitive p1 Dummy params     rule role=Started date in start=2009-05-26 end=2010-05-26 or date gt 2014-01-01     state=2
+.INP: primitive p2 Dummy params @p0-state
+nvpair_ref: 'p0-state' None
+nvpair_ref: 'p0-state' None
+.INP: property rule #uname eq node1 stonith-enabled=no
+.INP: tag tag1: p0 p1 p2
+.INP: location l1 { p0 p1 p2 } inf: node1
+.INP: show
+node node1
+primitive p0 Dummy \
+	params state=1
+primitive p1 Dummy \
+	params rule $role=Started date in start=2009-05-26 end=2010-05-26 or date gt 2014-01-01 state=2
+primitive p2 Dummy \
+	params @p0-state
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	meta target-role=Started \
+	op start requires=nothing timeout=60s interval=0 \
+	op monitor interval=60m timeout=60s
+location l1 { p0 p1 p2 } inf: node1
+property cib-bootstrap-options: \
+	rule #uname eq node1 \
+	stonith-enabled=no
+tag tag1: p0 p1 p2
+.INP: _test
+.INP: verify
+.EXT crm_resource --show-metadata stonith:ssh
+.EXT stonithd metadata
+.EXT crm_resource --show-metadata ocf:heartbeat:Dummy
+.EXT pengine metadata
+.EXT crmd metadata
+.EXT cib metadata
+.INP: commit
+nvpair_ref: 'p0-state' None
diff --git a/test/testcases/node b/test/testcases/node
new file mode 100644
index 0000000..a94dad8
--- /dev/null
+++ b/test/testcases/node
@@ -0,0 +1,10 @@
+node show
+node show node1
+%setenv showobj=node1
+node standby node1
+node online node1
+node maintenance node1
+node ready node1
+node attribute node1 set a1 "1 2 3"
+node attribute node1 show a1
+node attribute node1 delete a1
diff --git a/test/testcases/node.exp b/test/testcases/node.exp
new file mode 100644
index 0000000..f3be5e8
--- /dev/null
+++ b/test/testcases/node.exp
@@ -0,0 +1,162 @@
+.TRY node show
+node1: normal
+.TRY node show node1
+node1: normal
+.SETENV showobj=node1
+.TRY node standby node1
+.EXT crm_attribute -t nodes -N 'node1' -n standby -v 'on' --lifetime='forever'
+.INP: configure
+.INP: _regtest on
+.INP: show xml node1
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes>
+      <node uname="node1" id="node1">
+        <instance_attributes id="nodes-node1">
+          <nvpair id="nodes-node1-standby" name="standby" value="on"/>
+        </instance_attributes>
+      </node>
+    </nodes>
+    <resources/>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY node online node1
+.EXT crm_attribute -t nodes -N 'node1' -n standby -v 'off' --lifetime='forever'
+.INP: configure
+.INP: _regtest on
+.INP: show xml node1
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes>
+      <node uname="node1" id="node1">
+        <instance_attributes id="nodes-node1">
+          <nvpair id="nodes-node1-standby" name="standby" value="off"/>
+        </instance_attributes>
+      </node>
+    </nodes>
+    <resources/>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY node maintenance node1
+.EXT crm_attribute -t nodes -N 'node1' -n maintenance -v 'on'
+.INP: configure
+.INP: _regtest on
+.INP: show xml node1
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes>
+      <node uname="node1" id="node1">
+        <instance_attributes id="nodes-node1">
+          <nvpair id="nodes-node1-standby" name="standby" value="off"/>
+          <nvpair id="nodes-node1-maintenance" name="maintenance" value="on"/>
+        </instance_attributes>
+      </node>
+    </nodes>
+    <resources/>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY node ready node1
+.EXT crm_attribute -t nodes -N 'node1' -n maintenance -v 'off'
+.INP: configure
+.INP: _regtest on
+.INP: show xml node1
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes>
+      <node uname="node1" id="node1">
+        <instance_attributes id="nodes-node1">
+          <nvpair id="nodes-node1-standby" name="standby" value="off"/>
+          <nvpair id="nodes-node1-maintenance" name="maintenance" value="off"/>
+        </instance_attributes>
+      </node>
+    </nodes>
+    <resources/>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY node attribute node1 set a1 "1 2 3"
+.EXT crm_attribute -t nodes -U 'node1' -n 'a1' -v '1 2 3'
+.INP: configure
+.INP: _regtest on
+.INP: show xml node1
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes>
+      <node uname="node1" id="node1">
+        <instance_attributes id="nodes-node1">
+          <nvpair id="nodes-node1-standby" name="standby" value="off"/>
+          <nvpair id="nodes-node1-maintenance" name="maintenance" value="off"/>
+          <nvpair id="nodes-node1-a1" name="a1" value="1 2 3"/>
+        </instance_attributes>
+      </node>
+    </nodes>
+    <resources/>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY node attribute node1 show a1
+.EXT crm_attribute -G -t nodes -U 'node1' -n 'a1'
+scope=nodes  name=a1 value=1 2 3
+.INP: configure
+.INP: _regtest on
+.INP: show xml node1
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes>
+      <node uname="node1" id="node1">
+        <instance_attributes id="nodes-node1">
+          <nvpair id="nodes-node1-standby" name="standby" value="off"/>
+          <nvpair id="nodes-node1-maintenance" name="maintenance" value="off"/>
+          <nvpair id="nodes-node1-a1" name="a1" value="1 2 3"/>
+        </instance_attributes>
+      </node>
+    </nodes>
+    <resources/>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY node attribute node1 delete a1
+.EXT crm_attribute -D -t nodes -U 'node1' -n 'a1'
+Deleted nodes attribute: id=nodes-node1-a1 name=a1
+
+.INP: configure
+.INP: _regtest on
+.INP: show xml node1
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes>
+      <node uname="node1" id="node1">
+        <instance_attributes id="nodes-node1">
+          <nvpair id="nodes-node1-standby" name="standby" value="off"/>
+          <nvpair id="nodes-node1-maintenance" name="maintenance" value="off"/>
+        </instance_attributes>
+      </node>
+    </nodes>
+    <resources/>
+    <constraints/>
+  </configuration>
+</cib>
+
diff --git a/test/testcases/options b/test/testcases/options
new file mode 100644
index 0000000..44f331b
--- /dev/null
+++ b/test/testcases/options
@@ -0,0 +1,23 @@
+session Options
+options
+reset
+pager cat
+editor vi
+show
+check-frequency never
+check-mode nosuchever
+colorscheme normal,yellow,cyan,red,green,magenta
+colorscheme normal,yellow,cyan,red
+pager nosuchprogram
+skill-level operator
+skill-level joe
+skill-level expert
+output plain
+output misplain
+wait true
+wait off
+wait happy
+show
+save
+.
+options show
diff --git a/test/testcases/options.exp b/test/testcases/options.exp
new file mode 100644
index 0000000..f13d308
--- /dev/null
+++ b/test/testcases/options.exp
@@ -0,0 +1,64 @@
+.TRY Options
+.INP: options
+.INP: reset
+.INP: pager cat
+.INP: editor vi
+.INP: show
+editor "vi"
+pager "cat"
+user ""
+skill-level "expert"
+output "color"
+colorscheme "yellow,normal,cyan,red,green,magenta"
+sort-elements "yes"
+check-frequency "always"
+check-mode "strict"
+wait "no"
+add-quotes "yes"
+manage-children "ask"
+.INP: check-frequency never
+.INP: check-mode nosuchever
+ERROR: nosuchever not valid (choose one from strict,relaxed)
+.INP: colorscheme normal,yellow,cyan,red,green,magenta
+.INP: colorscheme normal,yellow,cyan,red
+ERROR: bad color scheme: normal,yellow,cyan,red
+.INP: pager nosuchprogram
+ERROR: nosuchprogram does not exist or is not a program
+.INP: skill-level operator
+.INP: skill-level joe
+ERROR: joe not valid (choose one from operator,administrator,expert)
+.INP: skill-level expert
+.INP: output plain
+.INP: output misplain
+ERROR: misplain not valid (choose one from plain,color,uppercase)
+.INP: wait true
+.INP: wait off
+.INP: wait happy
+ERROR: happy not valid (yes or no are valid)
+.INP: show
+editor "vi"
+pager "cat"
+user ""
+skill-level "expert"
+output "plain"
+colorscheme "normal,yellow,cyan,red,green,magenta"
+sort-elements "yes"
+check-frequency "never"
+check-mode "strict"
+wait "off"
+add-quotes "yes"
+manage-children "ask"
+.INP: save
+.TRY options show
+editor "vi"
+pager "cat"
+user ""
+skill-level "expert"
+output "plain"
+colorscheme "normal,yellow,cyan,red,green,magenta"
+sort-elements "yes"
+check-frequency "never"
+check-mode "strict"
+wait "off"
+add-quotes "yes"
+manage-children "ask"
diff --git a/test/testcases/ra b/test/testcases/ra
new file mode 100644
index 0000000..bd44a3a
--- /dev/null
+++ b/test/testcases/ra
@@ -0,0 +1,7 @@
+session RA interface
+ra
+providers IPaddr
+providers Dummy
+info ocf:pacemaker:Dummy
+info stonith:external/ssh
+.
diff --git a/test/testcases/ra.exp b/test/testcases/ra.exp
new file mode 100644
index 0000000..c6999d8
--- /dev/null
+++ b/test/testcases/ra.exp
@@ -0,0 +1,140 @@
+.TRY RA interface
+.INP: ra
+.INP: providers IPaddr
+
+heartbeat
+.INP: providers Dummy
+heartbeat pacemaker
+.INP: info ocf:pacemaker:Dummy
+.EXT crm_resource --show-metadata ocf:pacemaker:Dummy
+Example stateless resource agent (ocf:pacemaker:Dummy)
+
+This is a Dummy Resource Agent. It does absolutely nothing except 
+keep track of whether its running or not.
+Its purpose in life is for testing and to serve as a template for RA writers.
+
+NB: Please pay attention to the timeouts specified in the actions
+section below. They should be meaningful for the kind of resource
+the agent manages. They should be the minimum advised timeouts,
+but they shouldn't/cannot cover _all_ possible resource
+instances. So, try to be neither overly generous nor too stingy,
+but moderate. The minimum timeouts should never be below 10 seconds.
+
+Parameters (*: required, []: default):
+
+state (string, [/var/run//Dummy-{OCF_RESOURCE_INSTANCE}.state]): State file
+    Location to store the resource state in.
+
+fake (string, [dummy]): 
+    Fake attribute that can be changed to cause a reload
+
+op_sleep (string, [0]): Operation sleep duration in seconds.
+    Number of seconds to sleep during operations.  This can be used to test how
+    the cluster reacts to operation timeouts.
+
+Operations' defaults (advisory minimum):
+
+    start         timeout=20
+    stop          timeout=20
+    monitor       timeout=20 interval=10
+    reload        timeout=20
+    migrate_to    timeout=20
+    migrate_from  timeout=20
+.INP: info stonith:external/ssh
+.EXT crm_resource --show-metadata stonith:external/ssh
+.EXT stonithd metadata
+ssh STONITH device (stonith:external/ssh)
+
+ssh-based host reset
+Fine for testing, but not suitable for production!
+Only reboot action supported, no poweroff, and, surprisingly enough, no poweron.
+
+Parameters (*: required, []: default):
+
+hostlist* (string): Hostlist
+    The list of hosts that the STONITH device controls
+
+livedangerously (enum): Live Dangerously!!
+    Set to "yes" if you want to risk your system's integrity.
+    Of course, since this plugin isn't for production, using it
+    in production at all is a bad idea.  On the other hand,
+    setting this parameter to yes makes it an even worse idea.
+    Viva la Vida Loca!
+
+stonith-timeout (time, [60s]): How long to wait for the STONITH action to complete per a stonith device.
+    Overrides the stonith-timeout cluster property
+
+priority (integer, [0]): The priority of the stonith resource. Devices are tried in order of highest priority to lowest.
+pcmk_host_argument (string, [port]): Advanced use only: An alternate parameter to supply instead of 'port'
+    Some devices do not support the standard 'port' parameter or may provide additional ones.
+    Use this to specify an alternate, device-specific, parameter that should indicate the machine to be fenced.
+    A value of 'none' can be used to tell the cluster not to supply any additional parameters.
+
+pcmk_host_map (string): A mapping of host names to ports numbers for devices that do not support host names.
+    Eg. node1:1;node2:2,3 would tell the cluster to use port 1 for node1 and ports 2 and 3 for node2
+
+pcmk_host_list (string): A list of machines controlled by this device (Optional unless pcmk_host_check=static-list).
+pcmk_host_check (string, [dynamic-list]): How to determine which machines are controlled by the device.
+    Allowed values: dynamic-list (query the device), static-list (check the pcmk_host_list attribute), none (assume every device can fence every machine)
+
+pcmk_reboot_action (string, [reboot]): Advanced use only: An alternate command to run instead of 'reboot'
+    Some devices do not support the standard commands or may provide additional ones.
+    Use this to specify an alternate, device-specific, command that implements the 'reboot' action.
+
+pcmk_reboot_timeout (time, [60s]): Advanced use only: Specify an alternate timeout to use for reboot actions instead of stonith-timeout
+    Some devices need much more/less time to complete than normal.
+    Use this to specify an alternate, device-specific, timeout for 'reboot' actions.
+
+pcmk_reboot_retries (integer, [2]): Advanced use only: The maximum number of times to retry the 'reboot' command within the timeout period
+    Some devices do not support multiple connections. Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation, if there is time remaining. Use this option to alter the number of times Pacemaker retries 'reboot' actions before giving up.
+
+pcmk_off_action (string, [off]): Advanced use only: An alternate command to run instead of 'off'
+    Some devices do not support the standard commands or may provide additional ones.
+    Use this to specify an alternate, device-specific, command that implements the 'off' action.
+
+pcmk_off_timeout (time, [60s]): Advanced use only: Specify an alternate timeout to use for off actions instead of stonith-timeout
+    Some devices need much more/less time to complete than normal.
+    Use this to specify an alternate, device-specific, timeout for 'off' actions.
+
+pcmk_off_retries (integer, [2]): Advanced use only: The maximum number of times to retry the 'off' command within the timeout period
+    Some devices do not support multiple connections. Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation, if there is time remaining. Use this option to alter the number of times Pacemaker retries 'off' actions before giving up.
+
+pcmk_list_action (string, [list]): Advanced use only: An alternate command to run instead of 'list'
+    Some devices do not support the standard commands or may provide additional ones.
+    Use this to specify an alternate, device-specific, command that implements the 'list' action.
+
+pcmk_list_timeout (time, [60s]): Advanced use only: Specify an alternate timeout to use for list actions instead of stonith-timeout
+    Some devices need much more/less time to complete than normal.
+    Use this to specify an alternate, device-specific, timeout for 'list' actions.
+
+pcmk_list_retries (integer, [2]): Advanced use only: The maximum number of times to retry the 'list' command within the timeout period
+    Some devices do not support multiple connections. Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation, if there is time remaining. Use this option to alter the number of times Pacemaker retries 'list' actions before giving up.
+
+pcmk_monitor_action (string, [monitor]): Advanced use only: An alternate command to run instead of 'monitor'
+    Some devices do not support the standard commands or may provide additional ones.
+    Use this to specify an alternate, device-specific, command that implements the 'monitor' action.
+
+pcmk_monitor_timeout (time, [60s]): Advanced use only: Specify an alternate timeout to use for monitor actions instead of stonith-timeout
+    Some devices need much more/less time to complete than normal.
+    Use this to specify an alternate, device-specific, timeout for 'monitor' actions.
+
+pcmk_monitor_retries (integer, [2]): Advanced use only: The maximum number of times to retry the 'monitor' command within the timeout period
+    Some devices do not support multiple connections. Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation, if there is time remaining. Use this option to alter the number of times Pacemaker retries 'monitor' actions before giving up.
+
+pcmk_status_action (string, [status]): Advanced use only: An alternate command to run instead of 'status'
+    Some devices do not support the standard commands or may provide additional ones.
+    Use this to specify an alternate, device-specific, command that implements the 'status' action.
+
+pcmk_status_timeout (time, [60s]): Advanced use only: Specify an alternate timeout to use for status actions instead of stonith-timeout
+    Some devices need much more/less time to complete than normal.
+    Use this to specify an alternate, device-specific, timeout for 'status' actions.
+
+pcmk_status_retries (integer, [2]): Advanced use only: The maximum number of times to retry the 'status' command within the timeout period
+    Some devices do not support multiple connections. Operations may 'fail' if the device is busy with another task so Pacemaker will automatically retry the operation, if there is time remaining. Use this option to alter the number of times Pacemaker retries 'status' actions before giving up.
+
+Operations' defaults (advisory minimum):
+
+    start         timeout=20
+    stop          timeout=15
+    status        timeout=20
+    monitor       timeout=20 interval=3600
diff --git a/test/testcases/ra.filter b/test/testcases/ra.filter
new file mode 100755
index 0000000..43ffbab
--- /dev/null
+++ b/test/testcases/ra.filter
@@ -0,0 +1,16 @@
+#!/usr/bin/awk -f
+# reduce the providers list to heartbeat and pacemaker
+# (prevents other providers creeping in)
+function reduce(a) {
+	a["heartbeat"]=1; a["pacemaker"]=1;
+	s="";
+	for( i=1; i<=NF; i++ )
+		if( $i in a )
+			s=s" "$i;
+	return substr(s,2);
+}
+n==1 { n=0; print reduce(a); next; }
+/providers IPaddr/ { n=1; }
+/providers Dummy/ { n=1; }
+/^ssh STONITH/ { sub(" external",""); }
+{ print }
diff --git a/test/testcases/resource b/test/testcases/resource
new file mode 100644
index 0000000..e094a5a
--- /dev/null
+++ b/test/testcases/resource
@@ -0,0 +1,39 @@
+resource status p0
+%setenv showobj=p3
+resource start p3
+resource stop p3
+%setenv showobj=c1
+resource manage c1
+resource unmanage c1
+%setenv showobj=cli-prefer-p3
+resource migrate p3 node1
+%setenv showobj=
+resource unmigrate p3
+%setenv showobj=cli-prefer-p3
+resource migrate p3 node1 force
+%setenv showobj=
+resource unmigrate p3
+%setenv showobj=p0
+resource param p0 set a0 "1 2 3"
+resource param p0 show a0
+resource param p0 delete a0
+resource meta p0 set m0 123
+resource meta p0 show m0
+resource meta p0 delete m0
+configure group g p0 p3
+options manage-children never
+resource start g
+resource start p0
+resource stop g
+configure clone cg g
+options manage-children always
+resource start g
+resource stop g
+resource start cg
+resource stop p0
+resource start cg
+resource stop cg
+resource stop p3
+configure rename p3 p4
+configure primitive p3 Dummy
+resource stop p3
diff --git a/test/testcases/resource.exp b/test/testcases/resource.exp
new file mode 100644
index 0000000..e052132
--- /dev/null
+++ b/test/testcases/resource.exp
@@ -0,0 +1,734 @@
+.TRY resource status p0
+.EXT crm_resource -W -r 'p0'
+resource p0 is NOT running
+.SETENV showobj=p3
+.TRY resource start p3
+.INP: configure
+.INP: _regtest on
+.INP: show xml p3
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+        <meta_attributes id="p3-meta_attributes">
+          <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Started"/>
+        </meta_attributes>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource stop p3
+.INP: configure
+.INP: _regtest on
+.INP: show xml p3
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+        <meta_attributes id="p3-meta_attributes">
+          <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.SETENV showobj=c1
+.TRY resource manage c1
+.INP: configure
+.INP: _regtest on
+.INP: show xml c1
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="c1">
+        <meta_attributes id="c1-meta_attributes">
+          <nvpair id="c1-meta_attributes-is-managed" name="is-managed" value="true"/>
+        </meta_attributes>
+        <primitive id="p1" class="ocf" provider="pacemaker" type="Dummy"/>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource unmanage c1
+.INP: configure
+.INP: _regtest on
+.INP: show xml c1
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="c1">
+        <meta_attributes id="c1-meta_attributes">
+          <nvpair id="c1-meta_attributes-is-managed" name="is-managed" value="false"/>
+        </meta_attributes>
+        <primitive id="p1" class="ocf" provider="pacemaker" type="Dummy"/>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.SETENV showobj=cli-prefer-p3
+.TRY resource migrate p3 node1
+.EXT crm_resource -M -r 'p3' --node='node1'
+.INP: configure
+.INP: _regtest on
+.INP: show xml cli-prefer-p3
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources/>
+    <constraints>
+      <rsc_location id="cli-prefer-p3" rsc="p3" role="Started" node="node1" score="INFINITY"/>
+    </constraints>
+  </configuration>
+</cib>
+
+.SETENV showobj=
+.TRY resource unmigrate p3
+.EXT crm_resource -U -r 'p3'
+.SETENV showobj=cli-prefer-p3
+.TRY resource migrate p3 node1 force
+.EXT crm_resource -M -r 'p3' --node='node1' --force
+.INP: configure
+.INP: _regtest on
+.INP: show xml cli-prefer-p3
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources/>
+    <constraints>
+      <rsc_location id="cli-prefer-p3" rsc="p3" role="Started" node="node1" score="INFINITY"/>
+    </constraints>
+  </configuration>
+</cib>
+
+.SETENV showobj=
+.TRY resource unmigrate p3
+.EXT crm_resource -U -r 'p3'
+.SETENV showobj=p0
+.TRY resource param p0 set a0 "1 2 3"
+.EXT crm_resource -r 'p0' -p 'a0' -v '1 2 3'
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+        <instance_attributes id="p0-instance_attributes">
+          <nvpair id="p0-instance_attributes-a0" name="a0" value="1 2 3"/>
+        </instance_attributes>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource param p0 show a0
+.EXT crm_resource -r 'p0' -g 'a0'
+1 2 3
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+        <instance_attributes id="p0-instance_attributes">
+          <nvpair id="p0-instance_attributes-a0" name="a0" value="1 2 3"/>
+        </instance_attributes>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource param p0 delete a0
+.EXT crm_resource -r 'p0' -d 'a0'
+Deleted p0 option: id=p0-instance_attributes-a0 name=a0
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+        <instance_attributes id="p0-instance_attributes"/>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource meta p0 set m0 123
+.EXT crm_resource --meta -r 'p0' -p 'm0' -v '123'
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+        <instance_attributes id="p0-instance_attributes"/>
+        <meta_attributes id="p0-meta_attributes">
+          <nvpair id="p0-meta_attributes-m0" name="m0" value="123"/>
+        </meta_attributes>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource meta p0 show m0
+.EXT crm_resource --meta -r 'p0' -g 'm0'
+123
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+        <instance_attributes id="p0-instance_attributes"/>
+        <meta_attributes id="p0-meta_attributes">
+          <nvpair id="p0-meta_attributes-m0" name="m0" value="123"/>
+        </meta_attributes>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource meta p0 delete m0
+.EXT crm_resource --meta -r 'p0' -d 'm0'
+Deleted p0 option: id=p0-meta_attributes-m0 name=m0
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+        <instance_attributes id="p0-instance_attributes"/>
+        <meta_attributes id="p0-meta_attributes"/>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY configure group g p0 p3
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <group id="g">
+        <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+          <instance_attributes id="p0-instance_attributes"/>
+          <meta_attributes id="p0-meta_attributes"/>
+        </primitive>
+        <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+          <meta_attributes id="p3-meta_attributes">
+            <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+          </meta_attributes>
+        </primitive>
+      </group>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY options manage-children never
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <group id="g">
+        <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+          <instance_attributes id="p0-instance_attributes"/>
+          <meta_attributes id="p0-meta_attributes"/>
+        </primitive>
+        <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+          <meta_attributes id="p3-meta_attributes">
+            <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+          </meta_attributes>
+        </primitive>
+      </group>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource start g
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <group id="g">
+        <meta_attributes id="g-meta_attributes">
+          <nvpair id="g-meta_attributes-target-role" name="target-role" value="Started"/>
+        </meta_attributes>
+        <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy"/>
+        <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+          <meta_attributes id="p3-meta_attributes">
+            <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+          </meta_attributes>
+        </primitive>
+      </group>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource start p0
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <group id="g">
+        <meta_attributes id="g-meta_attributes">
+          <nvpair id="g-meta_attributes-target-role" name="target-role" value="Started"/>
+        </meta_attributes>
+        <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+          <meta_attributes id="p0-meta_attributes">
+            <nvpair id="p0-meta_attributes-target-role" name="target-role" value="Started"/>
+          </meta_attributes>
+        </primitive>
+        <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+          <meta_attributes id="p3-meta_attributes">
+            <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+          </meta_attributes>
+        </primitive>
+      </group>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource stop g
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <group id="g">
+        <meta_attributes id="g-meta_attributes">
+          <nvpair id="g-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+        <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+          <meta_attributes id="p0-meta_attributes">
+            <nvpair id="p0-meta_attributes-target-role" name="target-role" value="Started"/>
+          </meta_attributes>
+        </primitive>
+        <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+          <meta_attributes id="p3-meta_attributes">
+            <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+          </meta_attributes>
+        </primitive>
+      </group>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY configure clone cg g
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <group id="g">
+          <meta_attributes id="g-meta_attributes">
+            <nvpair id="g-meta_attributes-target-role" name="target-role" value="Stopped"/>
+          </meta_attributes>
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p0-meta_attributes">
+              <nvpair id="p0-meta_attributes-target-role" name="target-role" value="Started"/>
+            </meta_attributes>
+          </primitive>
+          <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p3-meta_attributes">
+              <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+            </meta_attributes>
+          </primitive>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY options manage-children always
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <group id="g">
+          <meta_attributes id="g-meta_attributes">
+            <nvpair id="g-meta_attributes-target-role" name="target-role" value="Stopped"/>
+          </meta_attributes>
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p0-meta_attributes">
+              <nvpair id="p0-meta_attributes-target-role" name="target-role" value="Started"/>
+            </meta_attributes>
+          </primitive>
+          <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p3-meta_attributes">
+              <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+            </meta_attributes>
+          </primitive>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource start g
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Started"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p0-meta_attributes">
+              <nvpair id="p0-meta_attributes-target-role" name="target-role" value="Started"/>
+            </meta_attributes>
+          </primitive>
+          <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy"/>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource stop g
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy"/>
+          <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy"/>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource start cg
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Started"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy"/>
+          <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy"/>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource stop p0
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Started"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p0-meta_attributes">
+              <nvpair id="p0-meta_attributes-target-role" name="target-role" value="Stopped"/>
+            </meta_attributes>
+          </primitive>
+          <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy"/>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource start cg
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Started"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy"/>
+          <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy"/>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource stop cg
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy"/>
+          <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy"/>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource stop p3
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy"/>
+          <primitive id="p3" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p3-meta_attributes">
+              <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+            </meta_attributes>
+          </primitive>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY configure rename p3 p4
+.EXT crm_resource --show-metadata ocf:pacemaker:Dummy
+.EXT crm_resource --show-metadata ocf:heartbeat:Delay
+.EXT crm_resource --show-metadata stonith:null
+.EXT stonithd metadata
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy"/>
+          <primitive id="p4" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p3-meta_attributes">
+              <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+            </meta_attributes>
+          </primitive>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY configure primitive p3 Dummy
+.EXT crm_resource --show-metadata ocf:pacemaker:Dummy
+.EXT crm_resource --show-metadata ocf:heartbeat:Delay
+.EXT crm_resource --show-metadata stonith:null
+.EXT stonithd metadata
+.EXT crm_resource --show-metadata ocf:heartbeat:Dummy
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy"/>
+          <primitive id="p4" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p3-meta_attributes">
+              <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+            </meta_attributes>
+          </primitive>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource stop p3
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes/>
+    <resources>
+      <clone id="cg">
+        <meta_attributes id="cg-meta_attributes">
+          <nvpair id="cg-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+        <group id="g">
+          <primitive id="p0" class="ocf" provider="pacemaker" type="Dummy"/>
+          <primitive id="p4" class="ocf" provider="pacemaker" type="Dummy">
+            <meta_attributes id="p3-meta_attributes">
+              <nvpair id="p3-meta_attributes-target-role" name="target-role" value="Stopped"/>
+            </meta_attributes>
+          </primitive>
+        </group>
+      </clone>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
diff --git a/test/testcases/rset b/test/testcases/rset
new file mode 100644
index 0000000..798e392
--- /dev/null
+++ b/test/testcases/rset
@@ -0,0 +1,21 @@
+show Resource sets
+node node1
+primitive st stonith:ssh \
+	params hostlist='node1' \
+	op start timeout=60s
+primitive d1 ocf:pacemaker:Dummy
+primitive d2 ocf:heartbeat:Dummy
+primitive d3 ocf:heartbeat:Dummy
+primitive d4 ocf:heartbeat:Dummy
+primitive d5 ocf:heartbeat:Dummy
+order o1 Serialize: d1 d2 ( d3 d4 )
+colocation c1 inf: d4 ( d1 d2 d3 )
+colocation c2 inf: d1 d2 d3 d4
+colocation c3 inf: ( d3 d4 ) ( d1 d2 )
+delete d2
+show o1 c1 c2 c3
+delete d4
+show o1 c1 c2 c3
+_test
+verify
+.
diff --git a/test/testcases/rset-xml b/test/testcases/rset-xml
new file mode 100644
index 0000000..842d4df
--- /dev/null
+++ b/test/testcases/rset-xml
@@ -0,0 +1,19 @@
+showxml Resource sets
+node node1
+primitive st stonith:ssh \
+	params hostlist='node1' \
+	op start timeout=60s
+primitive d1 ocf:pacemaker:Dummy
+primitive d2 ocf:heartbeat:Dummy
+primitive d3 ocf:heartbeat:Dummy
+primitive d4 ocf:heartbeat:Dummy
+primitive d5 ocf:heartbeat:Dummy
+order o1 Serialize: d1 d2 ( d3 d4 )
+colocation c1 inf: d4 ( d1 d2 d3 )
+colocation c2 inf: d1 d2 d3 d4
+colocation c3 inf: ( d3 d4 ) ( d1 d2 )
+delete d2
+delete d4
+_test
+verify
+.
diff --git a/test/testcases/rset-xml.exp b/test/testcases/rset-xml.exp
new file mode 100644
index 0000000..740fc6c
--- /dev/null
+++ b/test/testcases/rset-xml.exp
@@ -0,0 +1,33 @@
+<?xml version="1.0" ?>
+<cib num_updates="0" crm_feature_set="3.0.9" validate-with="pacemaker-2.0" epoch="1" admin_epoch="0" cib-last-written="Sun Apr 12 21:37:48 2009">
+  <configuration>
+    <crm_config/>
+    <nodes>
+      <node uname="node1" id="node1"/>
+    </nodes>
+    <resources>
+      <primitive id="d5" class="ocf" provider="heartbeat" type="Dummy"/>
+      <primitive id="st" class="stonith" type="ssh">
+        <instance_attributes id="st-instance_attributes">
+          <nvpair name="hostlist" value="node1" id="st-instance_attributes-hostlist"/>
+        </instance_attributes>
+        <operations>
+          <op name="start" timeout="60s" interval="0" id="st-start-0"/>
+        </operations>
+      </primitive>
+      <primitive id="d3" class="ocf" provider="heartbeat" type="Dummy"/>
+      <primitive id="d1" class="ocf" provider="pacemaker" type="Dummy"/>
+    </resources>
+    <constraints>
+      <rsc_colocation id="c2" score="INFINITY" rsc="d3" with-rsc="d1"/>
+      <rsc_colocation id="c1" score="INFINITY">
+        <resource_set sequential="false" id="c1-1">
+          <resource_ref id="d1"/>
+          <resource_ref id="d3"/>
+        </resource_set>
+      </rsc_colocation>
+      <rsc_order id="o1" kind="Serialize" first="d1" then="d3"/>
+      <rsc_colocation id="c3" score="INFINITY" rsc="d3" with-rsc="d1"/>
+    </constraints>
+  </configuration>
+</cib>
diff --git a/test/testcases/rset.exp b/test/testcases/rset.exp
new file mode 100644
index 0000000..45da4a3
--- /dev/null
+++ b/test/testcases/rset.exp
@@ -0,0 +1,56 @@
+.TRY Resource sets
+.INP: configure
+.INP: _regtest on
+.INP: erase
+.INP: erase nodes
+.INP: node node1
+.INP: primitive st stonith:ssh 	params hostlist='node1' 	op start timeout=60s
+.INP: primitive d1 ocf:pacemaker:Dummy
+.INP: primitive d2 ocf:heartbeat:Dummy
+.INP: primitive d3 ocf:heartbeat:Dummy
+.INP: primitive d4 ocf:heartbeat:Dummy
+.INP: primitive d5 ocf:heartbeat:Dummy
+.INP: order o1 Serialize: d1 d2 ( d3 d4 )
+.INP: colocation c1 inf: d4 ( d1 d2 d3 )
+.INP: colocation c2 inf: d1 d2 d3 d4
+.INP: colocation c3 inf: ( d3 d4 ) ( d1 d2 )
+.INP: delete d2
+INFO: 16: constraint order:o1 updated
+INFO: 16: constraint colocation:c1 updated
+INFO: 16: constraint colocation:c2 updated
+INFO: 16: constraint colocation:c3 updated
+.INP: show o1 c1 c2 c3
+colocation c1 inf: d4 ( d1 d3 )
+colocation c2 inf: d1 d3 d4
+colocation c3 inf: ( d3 d4 ) ( d1 )
+order o1 Serialize: d1 ( d3 d4 )
+.INP: delete d4
+INFO: 18: constraint order:o1 updated
+INFO: 18: constraint colocation:c1 updated
+INFO: 18: constraint colocation:c2 updated
+INFO: 18: constraint colocation:c3 updated
+.INP: show o1 c1 c2 c3
+colocation c1 inf: ( d1 d3 )
+colocation c2 inf: d3 d1
+colocation c3 inf: d3 d1
+order o1 Serialize: d1 d3
+.INP: _test
+.INP: verify
+.EXT crm_resource --show-metadata stonith:ssh
+.EXT stonithd metadata
+.EXT crm_resource --show-metadata ocf:pacemaker:Dummy
+.EXT crm_resource --show-metadata ocf:heartbeat:Dummy
+.EXT pengine metadata
+.INP: show
+node node1
+primitive d1 ocf:pacemaker:Dummy
+primitive d3 Dummy
+primitive d5 Dummy
+primitive st stonith:ssh \
+	params hostlist=node1 \
+	op start timeout=60s interval=0
+colocation c1 inf: ( d1 d3 )
+colocation c2 inf: d3 d1
+colocation c3 inf: d3 d1
+order o1 Serialize: d1 d3
+.INP: commit
diff --git a/test/testcases/shadow b/test/testcases/shadow
new file mode 100644
index 0000000..3bfd389
--- /dev/null
+++ b/test/testcases/shadow
@@ -0,0 +1,10 @@
+filesession Shadow CIB management
+cib
+new regtest force
+reset regtest
+use regtest
+commit regtest
+delete regtest
+use
+delete regtest
+.
diff --git a/test/testcases/shadow.exp b/test/testcases/shadow.exp
new file mode 100644
index 0000000..759d6a0
--- /dev/null
+++ b/test/testcases/shadow.exp
@@ -0,0 +1,18 @@
+.TRY Shadow CIB management
+.INP: cib
+.INP: new regtest force
+.EXT >/dev/null </dev/null crm_shadow -c 'regtest' --force
+INFO: 2: cib.new: regtest shadow CIB created
+.INP: reset regtest
+.EXT >/dev/null </dev/null crm_shadow -r 'regtest'
+INFO: 3: cib.reset: copied live CIB to regtest
+.INP: use regtest
+.INP: commit regtest
+.EXT >/dev/null </dev/null crm_shadow -C 'regtest' --force
+INFO: 5: cib.commit: committed 'regtest' shadow CIB to the cluster
+.INP: delete regtest
+ERROR: 6: cib.delete: regtest shadow CIB is in use
+.INP: use
+.INP: delete regtest
+.EXT >/dev/null </dev/null crm_shadow -D 'regtest' --force
+INFO: 8: cib.delete: regtest shadow CIB deleted
diff --git a/test/testcases/xmlonly.sh b/test/testcases/xmlonly.sh
new file mode 100755
index 0000000..15e6427
--- /dev/null
+++ b/test/testcases/xmlonly.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+#
+# extract the xml cib
+#
+sed -n '/^<?xml/,/^<\/cib>/p'
diff --git a/test/unit-tests.sh b/test/unit-tests.sh
new file mode 100755
index 0000000..635c6aa
--- /dev/null
+++ b/test/unit-tests.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+case `pwd` in
+	*/test/unittests)
+		PYTHONPATH=../../modules nosetests -w . "$@"
+		;;
+	*/test)
+		PYTHONPATH=../modules nosetests -w unittests "$@"
+		;;
+	*)
+		PYTHONPATH=modules nosetests -w test/unittests "$@"
+		;;
+esac
+
diff --git a/test/unittests/__init__.py b/test/unittests/__init__.py
new file mode 100644
index 0000000..e8be176
--- /dev/null
+++ b/test/unittests/__init__.py
@@ -0,0 +1,54 @@
+import os
+import msg
+import config
+msg.ERR_STREAM = None
+config.core.debug = True
+_here = os.path.dirname(__file__)
+config.path.sharedir = os.path.join(_here, "../../doc")
+config.path.crm_dtd_dir = os.path.join(_here, "schemas")
+
+
+# install a basic CIB
+import cibconfig
+
+_CIB = """
+<cib epoch="0" num_updates="0" admin_epoch="0" validate-with="pacemaker-1.2" cib-last-written="Mon Mar  3 23:58:36 2014" update-origin="ha-one" update-client="crmd" update-user="hacluster" crm_feature_set="3.0.9" have-quorum="1" dc-uuid="1">
+  <configuration>
+    <crm_config>
+      <cluster_property_set id="cib-bootstrap-options">
+        <nvpair name="stonith-enabled" value="false" id="cib-bootstrap-options-stonith-enabled"/>
+        <nvpair name="no-quorum-policy" value="ignore" id="cib-bootstrap-options-no-quorum-policy"/>
+        <nvpair name="placement-strategy" value="balanced" id="cib-bootstrap-options-placement-strategy"/>
+        <nvpair name="dc-version" value="1.1.11+git20140221.0b7d85a-115.1-1.1.11+git20140221.0b7d85a" id="cib-bootstrap-options-dc-version"/>
+        <nvpair name="cluster-infrastructure" value="corosync" id="cib-bootstrap-options-cluster-infrastructure"/>
+        <nvpair name="symmetric-cluster" value="true" id="cib-bootstrap-options-symmetric-cluster"/>
+      </cluster_property_set>
+    </crm_config>
+    <nodes>
+      <node id="1" uname="ha-one"/>
+      <node id="2" uname="ha-two"/>
+      <node id="3" uname="ha-three"/>
+    </nodes>
+    <resources>
+    </resources>
+    <constraints>
+    </constraints>
+    <rsc_defaults>
+      <meta_attributes id="rsc-options">
+        <nvpair name="resource-stickiness" value="1" id="rsc-options-resource-stickiness"/>
+        <nvpair name="migration-threshold" value="0" id="rsc-options-migration-threshold"/>
+      </meta_attributes>
+    </rsc_defaults>
+    <op_defaults>
+      <meta_attributes id="op-options">
+        <nvpair name="timeout" value="200" id="op-options-timeout"/>
+        <nvpair name="record-pending" value="true" id="op-options-record-pending"/>
+      </meta_attributes>
+    </op_defaults>
+  </configuration>
+  <status>
+  </status>
+</cib>
+"""
+
+cibconfig.cib_factory.initialize(cib=_CIB)
diff --git a/test/unittests/bug-862577_corosync.conf b/test/unittests/bug-862577_corosync.conf
new file mode 100644
index 0000000..09b1225
--- /dev/null
+++ b/test/unittests/bug-862577_corosync.conf
@@ -0,0 +1,51 @@
+# Please read the corosync.conf.5 manual page
+
+service {
+	ver:		1
+	name:		pacemaker
+}
+totem {
+	version:	2
+	secauth:	off
+	cluster_name:	hacluster
+	clear_node_high_bit: yes
+
+# Following are old corosync 1.4.x defaults from SLES
+#	token:		5000
+#	token_retransmits_before_loss_const: 10
+#	join:		60
+#	consensus:	6000
+#	vsftype:	none
+#	max_messages:	20
+#	threads:	0
+
+	crypto_cipher:	none
+	crypto_hash:	none
+
+	interface {
+		ringnumber:	0
+		bindnetaddr:	10.122.2.13
+		mcastaddr:	239.91.185.71
+		mcastport:	5405
+		ttl:		1
+	}
+}
+logging {
+	fileline:	off
+	to_stderr:	no
+	to_logfile:	yes
+	logfile:	/var/log/cluster/corosync.log
+	to_syslog:	yes
+	debug:		off
+	timestamp:	on
+	logger_subsys {
+		subsys:	QUORUM
+		debug:	off
+	}
+}
+quorum {
+	# Enable and configure quorum subsystem (default: off)
+	# see also corosync.conf.5 and votequorum.5
+	provider: corosync_votequorum
+	expected_votes: 2
+}
diff --git a/test/unittests/corosync.conf.1 b/test/unittests/corosync.conf.1
new file mode 100644
index 0000000..7b3abed
--- /dev/null
+++ b/test/unittests/corosync.conf.1
@@ -0,0 +1,81 @@
+# Please read the corosync.conf.5 manual page
+totem {
+	version: 2
+
+	# crypto_cipher and crypto_hash: Used for mutual node authentication.
+	# If you choose to enable this, then do remember to create a shared
+	# secret with "corosync-keygen".
+	# enabling crypto_cipher, requires also enabling of crypto_hash.
+	crypto_cipher: none
+	crypto_hash: none
+
+	# interface: define at least one interface to communicate
+	# over. If you define more than one interface stanza, you must
+	# also set rrp_mode.
+	interface {
+                # Rings must be consecutively numbered, starting at 0.
+		ringnumber: 0
+		# This is normally the *network* address of the
+		# interface to bind to. This ensures that you can use
+		# identical instances of this configuration file
+		# across all your cluster nodes, without having to
+		# modify this option.
+		bindnetaddr: 192.168.1.0
+		# However, if you have multiple physical network
+		# interfaces configured for the same subnet, then the
+		# network address alone is not sufficient to identify
+		# the interface Corosync should bind to. In that case,
+		# configure the *host* address of the interface
+		# instead:
+		# bindnetaddr: 192.168.1.1
+		# When selecting a multicast address, consider RFC
+		# 2365 (which, among other things, specifies that
+		# 239.255.x.x addresses are left to the discretion of
+		# the network administrator). Do not reuse multicast
+		# addresses across multiple Corosync clusters sharing
+		# the same network.
+		mcastaddr: 239.255.1.1
+		# Corosync uses the port you specify here for UDP
+		# messaging, and also the immediately preceding
+		# port. Thus if you set this to 5405, Corosync sends
+		# messages over UDP ports 5405 and 5404.
+		mcastport: 5405
+		# Time-to-live for cluster communication packets. The
+		# number of hops (routers) that this ring will allow
+		# itself to pass. Note that multicast routing must be
+		# specifically enabled on most network routers.
+		ttl: 1
+	}
+}
+
+logging {
+	# Log the source file and line where messages are being
+	# generated. When in doubt, leave off. Potentially useful for
+	# debugging.
+	fileline: off
+	# Log to standard error. When in doubt, set to no. Useful when
+	# running in the foreground (when invoking "corosync -f")
+	to_stderr: no
+	# Log to a log file. When set to "no", the "logfile" option
+	# must not be set.
+	to_logfile: yes
+	logfile: /var/log/cluster/corosync.log
+	# Log to the system log daemon. When in doubt, set to yes.
+	to_syslog: yes
+	# Log debug messages (very verbose). When in doubt, leave off.
+	debug: off
+	# Log messages with time stamps. When in doubt, set to on
+	# (unless you are only logging to syslog, where double
+	# timestamps can be annoying).
+	timestamp: on
+	logger_subsys {
+		subsys: QUORUM
+		debug: off
+	}
+}
+
+quorum {
+	# Enable and configure quorum subsystem (default: off)
+	# see also corosync.conf.5 and votequorum.5
+	#provider: corosync_votequorum
+}
diff --git a/test/unittests/corosync.conf.2 b/test/unittests/corosync.conf.2
new file mode 100644
index 0000000..0438451
--- /dev/null
+++ b/test/unittests/corosync.conf.2
@@ -0,0 +1,58 @@
+# Please read the corosync.conf.5 manual page
+totem {
+	version: 2
+
+	crypto_cipher: none
+	crypto_hash: none
+
+	interface {
+		ringnumber: 0
+		bindnetaddr: 10.16.35.0
+		mcastport: 5405
+		ttl: 1
+	}
+	transport: udpu
+}
+
+logging {
+	fileline: off
+	to_logfile: yes
+	to_syslog: yes
+	logfile: /var/log/cluster/corosync.log
+	debug: off
+	timestamp: on
+	logger_subsys {
+		subsys: QUORUM
+		debug: off
+	}
+}
+
+nodelist {
+	node {
+		ring0_addr: 10.16.35.101
+		nodeid: 1
+	}
+
+	node {
+		ring0_addr: 10.16.35.102
+		nodeid: 2
+	}
+
+	node {
+		ring0_addr: 10.16.35.103
+	}
+
+	node {
+		ring0_addr: 10.16.35.104
+	}
+
+	node {
+		ring0_addr: 10.16.35.105
+	}
+}
+
+quorum {
+	# Enable and configure quorum subsystem (default: off)
+	# see also corosync.conf.5 and votequorum.5
+	provider: corosync_votequorum
+}
diff --git a/test/unittests/schemas/acls-1.1.rng b/test/unittests/schemas/acls-1.1.rng
new file mode 100644
index 0000000..22cc631
--- /dev/null
+++ b/test/unittests/schemas/acls-1.1.rng
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-acls"/>
+  </start>
+
+  <define name="element-acls">
+    <element name="acls">
+      <zeroOrMore>
+	<choice>
+	  <element name="acl_user">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <choice>
+	      <zeroOrMore>
+ 		<element name="role_ref">
+		  <attribute name="id"><data type="IDREF"/></attribute>
+		</element>
+	      </zeroOrMore>
+	      <zeroOrMore>
+		<ref name="element-acl"/>
+	      </zeroOrMore>
+	    </choice>
+	  </element>
+	  <element name="acl_role">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <zeroOrMore>
+	      <ref name="element-acl"/>
+	    </zeroOrMore>
+	  </element>
+	</choice>
+      </zeroOrMore>
+    </element>
+  </define>
+
+  <define name="element-acl">
+    <choice>
+      <element name="read">
+	<ref name="attribute-acl"/>
+      </element>
+      <element name="write">
+	<ref name="attribute-acl"/>
+      </element>
+      <element name="deny">
+	<ref name="attribute-acl"/>
+      </element>
+    </choice>
+  </define>
+
+  <define name="attribute-acl">
+    <attribute name="id"><data type="ID"/></attribute>
+      <choice>
+	<attribute name="tag"><text/></attribute>
+	<attribute name="ref"><data type="IDREF"/></attribute>
+	<group>
+	  <attribute name="tag"><text/></attribute>
+	  <attribute name="ref"><data type="IDREF"/></attribute>
+	</group>
+	<attribute name="xpath"><text/></attribute>
+      </choice>
+      <optional>
+	<attribute name="attribute"><text/></attribute>
+      </optional>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/acls-1.2.rng b/test/unittests/schemas/acls-1.2.rng
new file mode 100644
index 0000000..22cc631
--- /dev/null
+++ b/test/unittests/schemas/acls-1.2.rng
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-acls"/>
+  </start>
+
+  <define name="element-acls">
+    <element name="acls">
+      <zeroOrMore>
+	<choice>
+	  <element name="acl_user">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <choice>
+	      <zeroOrMore>
+ 		<element name="role_ref">
+		  <attribute name="id"><data type="IDREF"/></attribute>
+		</element>
+	      </zeroOrMore>
+	      <zeroOrMore>
+		<ref name="element-acl"/>
+	      </zeroOrMore>
+	    </choice>
+	  </element>
+	  <element name="acl_role">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <zeroOrMore>
+	      <ref name="element-acl"/>
+	    </zeroOrMore>
+	  </element>
+	</choice>
+      </zeroOrMore>
+    </element>
+  </define>
+
+  <define name="element-acl">
+    <choice>
+      <element name="read">
+	<ref name="attribute-acl"/>
+      </element>
+      <element name="write">
+	<ref name="attribute-acl"/>
+      </element>
+      <element name="deny">
+	<ref name="attribute-acl"/>
+      </element>
+    </choice>
+  </define>
+
+  <define name="attribute-acl">
+    <attribute name="id"><data type="ID"/></attribute>
+      <choice>
+	<attribute name="tag"><text/></attribute>
+	<attribute name="ref"><data type="IDREF"/></attribute>
+	<group>
+	  <attribute name="tag"><text/></attribute>
+	  <attribute name="ref"><data type="IDREF"/></attribute>
+	</group>
+	<attribute name="xpath"><text/></attribute>
+      </choice>
+      <optional>
+	<attribute name="attribute"><text/></attribute>
+      </optional>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/constraints-1.0.rng b/test/unittests/schemas/constraints-1.0.rng
new file mode 100644
index 0000000..5a4474a
--- /dev/null
+++ b/test/unittests/schemas/constraints-1.0.rng
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-constraints"/>
+  </start>
+
+  <define name="element-constraints">
+      <zeroOrMore>
+	<choice>
+	  <ref name="element-location"/>
+	  <ref name="element-colocation"/>
+	  <ref name="element-order"/>
+	</choice>
+      </zeroOrMore>
+  </define>
+
+  <define name="element-location">
+    <element name="rsc_location">
+      <attribute name="id"><data type="ID"/></attribute>
+      <attribute name="rsc"><data type="IDREF"/></attribute>
+      <choice>
+	<group>
+	  <externalRef href="score.rng"/>
+	  <attribute name="node"><text/></attribute>
+	</group>
+	<oneOrMore>
+	  <externalRef href="rule.rng"/>
+	</oneOrMore>
+      </choice>
+      <optional>
+	<ref name="element-lifetime"/>
+      </optional>
+    </element>
+  </define>
+
+  <define name="element-resource-set">
+    <element name="resource_set">
+      <choice>
+	<attribute name="id-ref"><data type="IDREF"/></attribute>
+	<group>
+	  <attribute name="id"><data type="ID"/></attribute>
+	  <optional>
+	    <attribute name="sequential"><data type="boolean"/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="action">
+	      <ref name="attribute-actions"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <externalRef href="score.rng"/>
+	  </optional>
+	  <oneOrMore>
+	    <element name="resource_ref">
+	      <attribute name="id"><data type="IDREF"/></attribute>
+	    </element>
+	  </oneOrMore>
+	</group>
+      </choice>
+    </element>
+  </define>
+
+  <define name="element-colocation">
+    <element name="rsc_colocation">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<choice>
+	  <externalRef href="score.rng"/>
+	  <attribute name="score-attribute"><text/></attribute>
+	  <attribute name="score-attribute-mangle"><text/></attribute>
+	</choice>
+      </optional>
+      <optional>
+	<ref name="element-lifetime"/>
+      </optional>
+      <choice>
+	<oneOrMore>
+	  <ref name="element-resource-set"/>
+	</oneOrMore>
+	<group>
+	  <attribute name="rsc"><data type="IDREF"/></attribute>
+	  <attribute name="with-rsc"><data type="IDREF"/></attribute>
+	  <optional>
+	    <attribute name="node-attribute"><text/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="rsc-role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="with-rsc-role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	</group>
+      </choice>
+    </element>
+  </define>
+
+  <define name="element-order">
+    <element name="rsc_order">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<ref name="element-lifetime"/>
+      </optional>
+      <optional>
+	<attribute name="symmetrical"><data type="boolean"/></attribute>
+      </optional>
+      <optional>
+	<choice>
+	  <externalRef href="score.rng"/>
+	  <attribute name="kind">
+	    <ref name="order-types"/>
+	  </attribute>
+	</choice>
+      </optional>
+      <choice>
+	<oneOrMore>
+	  <ref name="element-resource-set"/>
+	</oneOrMore>
+	<group>
+	  <attribute name="first"><data type="IDREF"/></attribute>
+	  <attribute name="then"><data type="IDREF"/></attribute>
+	  <optional>
+	    <attribute name="first-action">
+	      <ref name="attribute-actions"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="then-action">
+	      <ref name="attribute-actions"/>
+	    </attribute>
+	  </optional>
+	</group>
+      </choice>
+    </element>
+  </define>
+ 
+  <define name="attribute-actions">
+    <choice>
+      <value>start</value>
+      <value>promote</value>
+      <value>demote</value>
+      <value>stop</value>
+    </choice>
+  </define>
+      
+  <define name="attribute-roles">
+    <choice>
+      <value>Stopped</value>
+      <value>Started</value>
+      <value>Master</value>
+      <value>Slave</value>
+    </choice>
+  </define>
+
+  <define name="order-types">
+    <choice>
+      <value>Optional</value>
+      <value>Mandatory</value>
+      <value>Serialize</value>
+    </choice>
+  </define>
+
+  <define name="element-lifetime">
+    <element name="lifetime">
+      <oneOrMore>
+	<externalRef href="rule.rng"/>
+      </oneOrMore>
+    </element>
+  </define>
+  
+</grammar>
diff --git a/test/unittests/schemas/constraints-1.1.rng b/test/unittests/schemas/constraints-1.1.rng
new file mode 100644
index 0000000..fff0fb7
--- /dev/null
+++ b/test/unittests/schemas/constraints-1.1.rng
@@ -0,0 +1,246 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-constraints"/>
+  </start>
+
+  <define name="element-constraints">
+      <zeroOrMore>
+	<choice>
+	  <ref name="element-location"/>
+	  <ref name="element-colocation"/>
+	  <ref name="element-order"/>
+	  <ref name="element-rsc_ticket"/>
+	</choice>
+      </zeroOrMore>
+  </define>
+
+  <define name="element-location">
+    <element name="rsc_location">
+      <attribute name="id"><data type="ID"/></attribute>
+      <choice>
+	<group>
+	  <choice>
+	    <attribute name="rsc"><data type="IDREF"/></attribute>
+	    <attribute name="rsc-pattern"><text/></attribute>
+	  </choice>
+	  <optional>
+	    <attribute name="role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	</group>
+	<oneOrMore>
+	  <ref name="element-resource-set"/>
+	</oneOrMore>
+      </choice>
+      <choice>
+	<group>
+	  <choice>
+	    <attribute name="domain"><data type="IDREF"/></attribute>
+	    <group>
+	      <attribute name="node"><text/></attribute>
+	      <externalRef href="score.rng"/>
+	    </group>
+	  </choice>
+	</group>
+	<oneOrMore>
+	  <externalRef href="rule.rng"/>
+	</oneOrMore>
+      </choice>
+      <optional>
+	<ref name="element-lifetime"/>
+      </optional>
+    </element>
+  </define>
+
+  <define name="element-resource-set">
+    <element name="resource_set">
+      <choice>
+	<attribute name="id-ref"><data type="IDREF"/></attribute>
+	<group>
+	  <attribute name="id"><data type="ID"/></attribute>
+	  <optional>
+	    <attribute name="sequential"><data type="boolean"/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="require-all"><data type="boolean"/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="action">
+	      <ref name="attribute-actions"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <externalRef href="score.rng"/>
+	  </optional>
+	  <oneOrMore>
+	    <element name="resource_ref">
+	      <attribute name="id"><data type="IDREF"/></attribute>
+	    </element>
+	  </oneOrMore>
+	</group>
+      </choice>
+    </element>
+  </define>
+
+  <define name="element-colocation">
+    <element name="rsc_colocation">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<choice>
+	  <externalRef href="score.rng"/>
+	  <attribute name="score-attribute"><text/></attribute>
+	  <attribute name="score-attribute-mangle"><text/></attribute>
+	</choice>
+      </optional>
+      <optional>
+	<ref name="element-lifetime"/>
+      </optional>
+      <choice>
+	<oneOrMore>
+	  <ref name="element-resource-set"/>
+	</oneOrMore>
+	<group>
+	  <attribute name="rsc"><data type="IDREF"/></attribute>
+	  <attribute name="with-rsc"><data type="IDREF"/></attribute>
+	  <optional>
+	    <attribute name="node-attribute"><text/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="rsc-role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="with-rsc-role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="rsc-instance"><data type="integer"/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="with-rsc-instance"><data type="integer"/></attribute>
+	  </optional>
+	</group>
+      </choice>
+    </element>
+  </define>
+
+  <define name="element-order">
+    <element name="rsc_order">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<ref name="element-lifetime"/>
+      </optional>
+      <optional>
+	<attribute name="symmetrical"><data type="boolean"/></attribute>
+      </optional>
+      <optional>
+	<choice>
+	  <externalRef href="score.rng"/>
+	  <attribute name="kind">
+	    <ref name="order-types"/>
+	  </attribute>
+	</choice>
+      </optional>
+      <choice>
+	<oneOrMore>
+	  <ref name="element-resource-set"/>
+	</oneOrMore>
+	<group>
+	  <attribute name="first"><data type="IDREF"/></attribute>
+	  <attribute name="then"><data type="IDREF"/></attribute>
+	  <optional>
+	    <attribute name="first-action">
+	      <ref name="attribute-actions"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="then-action">
+	      <ref name="attribute-actions"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="first-instance"><data type="integer"/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="then-instance"><data type="integer"/></attribute>
+	  </optional>
+	</group>
+      </choice>
+    </element>
+  </define>
+ 
+  <define name="element-rsc_ticket">
+    <element name="rsc_ticket">
+      <attribute name="id"><data type="ID"/></attribute>
+      <choice>
+	<oneOrMore>
+	  <ref name="element-resource-set"/>
+	</oneOrMore>
+	<group>
+	  <attribute name="rsc"><data type="IDREF"/></attribute>
+	  <optional>
+	    <attribute name="rsc-role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	</group>
+      </choice>
+      <attribute name="ticket"><text/></attribute>
+      <optional>
+	<attribute name="loss-policy">
+	  <choice>
+	    <value>stop</value>
+	    <value>demote</value>
+	    <value>fence</value>
+	    <value>freeze</value>
+	  </choice>
+	</attribute>
+      </optional>
+    </element>
+  </define>
+
+  <define name="attribute-actions">
+    <choice>
+      <value>start</value>
+      <value>promote</value>
+      <value>demote</value>
+      <value>stop</value>
+    </choice>
+  </define>
+      
+  <define name="attribute-roles">
+    <choice>
+      <value>Stopped</value>
+      <value>Started</value>
+      <value>Master</value>
+      <value>Slave</value>
+    </choice>
+  </define>
+
+  <define name="order-types">
+    <choice>
+      <value>Optional</value>
+      <value>Mandatory</value>
+      <value>Serialize</value>
+    </choice>
+  </define>
+
+  <define name="element-lifetime">
+    <element name="lifetime">
+      <oneOrMore>
+	<externalRef href="rule.rng"/>
+      </oneOrMore>
+    </element>
+  </define>
+  
+</grammar>
diff --git a/test/unittests/schemas/constraints-1.2.rng b/test/unittests/schemas/constraints-1.2.rng
new file mode 100644
index 0000000..221140c
--- /dev/null
+++ b/test/unittests/schemas/constraints-1.2.rng
@@ -0,0 +1,219 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-constraints"/>
+  </start>
+
+  <define name="element-constraints">
+      <zeroOrMore>
+	<choice>
+	  <ref name="element-location"/>
+	  <ref name="element-colocation"/>
+	  <ref name="element-order"/>
+	  <ref name="element-rsc_ticket"/>
+	</choice>
+      </zeroOrMore>
+  </define>
+
+  <define name="element-location">
+    <element name="rsc_location">
+      <attribute name="id"><data type="ID"/></attribute>
+      <attribute name="rsc"><data type="IDREF"/></attribute>
+      <optional>
+	<attribute name="role">
+	  <ref name="attribute-roles"/>
+	</attribute>
+      </optional>
+      <choice>
+	<group>
+	  <externalRef href="score.rng"/>
+	  <attribute name="node"><text/></attribute>
+	</group>
+	<oneOrMore>
+	  <externalRef href="rule.rng"/>
+	</oneOrMore>
+      </choice>
+      <optional>
+	<ref name="element-lifetime"/>
+      </optional>
+    </element>
+  </define>
+
+  <define name="element-resource-set">
+    <element name="resource_set">
+      <choice>
+	<attribute name="id-ref"><data type="IDREF"/></attribute>
+	<group>
+	  <attribute name="id"><data type="ID"/></attribute>
+	  <optional>
+	    <attribute name="sequential"><data type="boolean"/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="require-all"><data type="boolean"/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="action">
+	      <ref name="attribute-actions"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <externalRef href="score.rng"/>
+	  </optional>
+	  <oneOrMore>
+	    <element name="resource_ref">
+	      <attribute name="id"><data type="IDREF"/></attribute>
+	    </element>
+	  </oneOrMore>
+	</group>
+      </choice>
+    </element>
+  </define>
+
+  <define name="element-colocation">
+    <element name="rsc_colocation">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<choice>
+	  <externalRef href="score.rng"/>
+	  <attribute name="score-attribute"><text/></attribute>
+	  <attribute name="score-attribute-mangle"><text/></attribute>
+	</choice>
+      </optional>
+      <optional>
+	<ref name="element-lifetime"/>
+      </optional>
+      <choice>
+	<oneOrMore>
+	  <ref name="element-resource-set"/>
+	</oneOrMore>
+	<group>
+	  <attribute name="rsc"><data type="IDREF"/></attribute>
+	  <attribute name="with-rsc"><data type="IDREF"/></attribute>
+	  <optional>
+	    <attribute name="node-attribute"><text/></attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="rsc-role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="with-rsc-role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	</group>
+      </choice>
+    </element>
+  </define>
+
+  <define name="element-order">
+    <element name="rsc_order">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<ref name="element-lifetime"/>
+      </optional>
+      <optional>
+	<attribute name="symmetrical"><data type="boolean"/></attribute>
+      </optional>
+      <optional>
+	<choice>
+	  <externalRef href="score.rng"/>
+	  <attribute name="kind">
+	    <ref name="order-types"/>
+	  </attribute>
+	</choice>
+      </optional>
+      <choice>
+	<oneOrMore>
+	  <ref name="element-resource-set"/>
+	</oneOrMore>
+	<group>
+	  <attribute name="first"><data type="IDREF"/></attribute>
+	  <attribute name="then"><data type="IDREF"/></attribute>
+	  <optional>
+	    <attribute name="first-action">
+	      <ref name="attribute-actions"/>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="then-action">
+	      <ref name="attribute-actions"/>
+	    </attribute>
+	  </optional>
+	</group>
+      </choice>
+    </element>
+  </define>
+ 
+  <define name="element-rsc_ticket">
+    <element name="rsc_ticket">
+      <attribute name="id"><data type="ID"/></attribute>
+      <choice>
+	<oneOrMore>
+	  <ref name="element-resource-set"/>
+	</oneOrMore>
+	<group>
+	  <attribute name="rsc"><data type="IDREF"/></attribute>
+	  <optional>
+	    <attribute name="rsc-role">
+	      <ref name="attribute-roles"/>
+	    </attribute>
+	  </optional>
+	</group>
+      </choice>
+      <attribute name="ticket"><text/></attribute>
+      <optional>
+	<attribute name="loss-policy">
+	  <choice>
+	    <value>stop</value>
+	    <value>demote</value>
+	    <value>fence</value>
+	    <value>freeze</value>
+	  </choice>
+	</attribute>
+      </optional>
+    </element>
+  </define>
+
+  <define name="attribute-actions">
+    <choice>
+      <value>start</value>
+      <value>promote</value>
+      <value>demote</value>
+      <value>stop</value>
+    </choice>
+  </define>
+      
+  <define name="attribute-roles">
+    <choice>
+      <value>Stopped</value>
+      <value>Started</value>
+      <value>Master</value>
+      <value>Slave</value>
+    </choice>
+  </define>
+
+  <define name="order-types">
+    <choice>
+      <value>Optional</value>
+      <value>Mandatory</value>
+      <value>Serialize</value>
+    </choice>
+  </define>
+
+  <define name="element-lifetime">
+    <element name="lifetime">
+      <oneOrMore>
+	<externalRef href="rule.rng"/>
+      </oneOrMore>
+    </element>
+  </define>
+  
+</grammar>
diff --git a/test/unittests/schemas/fencing.rng b/test/unittests/schemas/fencing.rng
new file mode 100644
index 0000000..87de5a8
--- /dev/null
+++ b/test/unittests/schemas/fencing.rng
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-stonith"/>
+  </start>
+
+  <define name="element-stonith">
+    <element name="fencing-topology">
+      <zeroOrMore>
+	<ref name="element-level"/>
+      </zeroOrMore>
+    </element>
+  </define>
+  
+  <define name="element-level">
+    <element name="fencing-level">
+      <attribute name="id"><data type="ID"/></attribute>
+      <attribute name="target"><text/></attribute>
+      <attribute name="index"><data type="positiveInteger"/></attribute>
+      <attribute name="devices">
+	<data type="string">
+          <param name="pattern">([a-zA-Z0-9_\.\-]+)(,[a-zA-Z0-9_\.\-]+)*</param>
+	</data>
+      </attribute>
+    </element>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/nvset.rng b/test/unittests/schemas/nvset.rng
new file mode 100644
index 0000000..0d7e72c
--- /dev/null
+++ b/test/unittests/schemas/nvset.rng
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- types: http://www.w3.org/TR/xmlschema-2/#dateTime -->
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+    <ref name="element-nvset"/>
+  </start>
+
+  <define name="element-nvset">
+   <choice>
+   <attribute name="id-ref"><data type="IDREF"/></attribute>
+   <group>
+    <attribute name="id"><data type="ID"/></attribute>
+    <interleave>
+      <optional>
+	<externalRef href="rule.rng"/>
+      </optional>
+	<zeroOrMore>
+	  <element name="nvpair">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <attribute name="name"><text/></attribute>
+	    <optional>
+	      <attribute name="value"><text/></attribute>
+	    </optional>
+	  </element>
+	</zeroOrMore>
+      <optional>
+	<externalRef href="score.rng"/>
+      </optional>
+    </interleave>
+   </group>
+   </choice>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/pacemaker-1.0.rng b/test/unittests/schemas/pacemaker-1.0.rng
new file mode 100644
index 0000000..7100393
--- /dev/null
+++ b/test/unittests/schemas/pacemaker-1.0.rng
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- types: http://www.w3.org/TR/xmlschema-2/#dateTime -->
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+    <element name="cib">
+      <ref name="element-cib"/>
+    </element>
+  </start>
+
+  <define name="element-cib">
+    <ref name="attribute-options"/>
+    <element name="configuration">
+      <interleave>
+	<element name="crm_config">
+	  <zeroOrMore>
+	    <element name="cluster_property_set">
+	      <externalRef href="nvset.rng"/>
+	    </element>
+	  </zeroOrMore>
+	</element>
+	<optional>
+	  <element name="rsc_defaults">
+	    <zeroOrMore>
+	      <element name="meta_attributes">
+		<externalRef href="nvset.rng"/>
+	      </element>
+	    </zeroOrMore>
+	  </element>
+	</optional>
+	<optional>
+	  <element name="op_defaults">
+	    <zeroOrMore>
+	      <element name="meta_attributes">
+		<externalRef href="nvset.rng"/>
+	      </element>
+	    </zeroOrMore>
+	  </element>
+	</optional>
+	<ref name="element-nodes"/>
+	<element name="resources">
+	  <externalRef href="resources-1.0.rng"/>
+	</element>
+	<element name="constraints">
+	  <externalRef href="constraints-1.0.rng"/>
+	</element>
+      </interleave>
+    </element>
+    <element name="status">
+      <ref name="element-status"/>
+    </element>
+  </define>
+
+  <define name="attribute-options">
+    <externalRef href="versions.rng"/>
+    <optional>
+      <attribute name="crm_feature_set"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="remote-tls-port"><data type="nonNegativeInteger"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="remote-clear-port"><data type="nonNegativeInteger"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="have-quorum"><data type="boolean"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="dc-uuid"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="cib-last-written"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="no-quorum-panic"><data type="boolean"/></attribute>
+    </optional>
+  </define>
+
+  <define name="element-nodes">
+    <element name="nodes">
+      <zeroOrMore>
+	<element name="node">
+	  <attribute name="id"><text/></attribute>
+	  <attribute name="uname"><text/></attribute>
+	  <attribute name="type">
+	    <choice>
+	      <value>normal</value>
+	      <value>member</value>
+	      <value>ping</value>
+	    </choice>
+	  </attribute>
+	  <optional>
+	    <attribute name="description"><text/></attribute>
+	  </optional>
+	  <zeroOrMore>
+	    <element name="instance_attributes">
+	      <externalRef href="nvset.rng"/>
+	    </element>
+	  </zeroOrMore>
+	</element>
+      </zeroOrMore>
+    </element>
+  </define>
+
+  <define name="element-status">
+    <zeroOrMore>
+      <choice>
+        <attribute>
+          <anyName/>
+          <text/>
+        </attribute>
+        <element>
+          <anyName/>
+          <ref name="element-status"/>
+        </element>
+        <text/>
+      </choice>
+    </zeroOrMore>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/pacemaker-1.1.rng b/test/unittests/schemas/pacemaker-1.1.rng
new file mode 100644
index 0000000..50e9458
--- /dev/null
+++ b/test/unittests/schemas/pacemaker-1.1.rng
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- types: http://www.w3.org/TR/xmlschema-2/#dateTime -->
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+    <element name="cib">
+      <ref name="element-cib"/>
+    </element>
+  </start>
+
+  <define name="element-cib">
+    <ref name="attribute-options"/>
+    <element name="configuration">
+      <interleave>
+	<element name="crm_config">
+	  <zeroOrMore>
+	    <element name="cluster_property_set">
+	      <externalRef href="nvset.rng"/>
+	    </element>
+	  </zeroOrMore>
+	</element>
+	<optional>
+	  <element name="rsc_defaults">
+	    <zeroOrMore>
+	      <element name="meta_attributes">
+		<externalRef href="nvset.rng"/>
+	      </element>
+	    </zeroOrMore>
+	  </element>
+	</optional>
+	<optional>
+	  <element name="op_defaults">
+	    <zeroOrMore>
+	      <element name="meta_attributes">
+		<externalRef href="nvset.rng"/>
+	      </element>
+	    </zeroOrMore>
+	  </element>
+	</optional>
+	<ref name="element-nodes"/>
+	<element name="resources">
+	  <externalRef href="resources-1.1.rng"/>
+	</element>
+	<optional>
+	  <element name="domains">
+	    <zeroOrMore>
+	      <element name="domain">
+		<attribute name="id"><data type="ID"/></attribute>
+		<zeroOrMore>
+		  <element name="node">
+		    <attribute name="name"><text/></attribute>
+		    <externalRef href="score.rng"/>
+		  </element>
+		</zeroOrMore>
+	      </element>
+	    </zeroOrMore>
+	  </element>
+	</optional>
+	<element name="constraints">
+	  <externalRef href="constraints-1.1.rng"/>
+	</element>
+	<optional>
+	  <externalRef href="acls-1.1.rng"/>
+	</optional>
+	<optional>
+	  <externalRef href="fencing.rng"/>
+	</optional>
+      </interleave>
+    </element>
+    <element name="status">
+      <ref name="element-status"/>
+    </element>
+  </define>
+
+  <define name="attribute-options">
+    <externalRef href="versions.rng"/>
+    <optional>
+      <attribute name="crm_feature_set"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="remote-tls-port"><data type="nonNegativeInteger"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="remote-clear-port"><data type="nonNegativeInteger"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="have-quorum"><data type="boolean"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="dc-uuid"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="cib-last-written"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="no-quorum-panic"><data type="boolean"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="update-origin"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="update-client"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="update-user"><text/></attribute>
+    </optional>
+  </define>
+
+  <define name="element-nodes">
+    <element name="nodes">
+      <zeroOrMore>
+	<element name="node">
+	  <attribute name="id"><text/></attribute>
+	  <attribute name="uname"><text/></attribute>
+	  <optional>
+	    <attribute name="type">
+	      <choice>
+		<value>normal</value>
+		<value>member</value>
+		<value>ping</value>
+	      </choice>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="description"><text/></attribute>
+	  </optional>
+	  <optional>
+	    <externalRef href="score.rng"/>
+	  </optional>
+	  <zeroOrMore>
+	    <choice>
+	      <element name="instance_attributes">
+	        <externalRef href="nvset.rng"/>
+	      </element>
+	      <element name="utilization">
+	        <externalRef href="nvset.rng"/>
+	      </element>
+	    </choice>
+	  </zeroOrMore>
+	</element>
+      </zeroOrMore>
+    </element>
+  </define>
+
+  <define name="element-status">
+    <zeroOrMore>
+      <choice>
+        <attribute>
+          <anyName/>
+          <text/>
+        </attribute>
+        <element>
+          <anyName/>
+          <ref name="element-status"/>
+        </element>
+        <text/>
+      </choice>
+    </zeroOrMore>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/pacemaker-1.2.rng b/test/unittests/schemas/pacemaker-1.2.rng
new file mode 100644
index 0000000..33a7d2d
--- /dev/null
+++ b/test/unittests/schemas/pacemaker-1.2.rng
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- types: http://www.w3.org/TR/xmlschema-2/#dateTime -->
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+    <element name="cib">
+      <ref name="element-cib"/>
+    </element>
+  </start>
+
+  <define name="element-cib">
+    <ref name="attribute-options"/>
+    <element name="configuration">
+      <interleave>
+	<element name="crm_config">
+	  <zeroOrMore>
+	    <element name="cluster_property_set">
+	      <externalRef href="nvset.rng"/>
+	    </element>
+	  </zeroOrMore>
+	</element>
+	<optional>
+	  <element name="rsc_defaults">
+	    <zeroOrMore>
+	      <element name="meta_attributes">
+		<externalRef href="nvset.rng"/>
+	      </element>
+	    </zeroOrMore>
+	  </element>
+	</optional>
+	<optional>
+	  <element name="op_defaults">
+	    <zeroOrMore>
+	      <element name="meta_attributes">
+		<externalRef href="nvset.rng"/>
+	      </element>
+	    </zeroOrMore>
+	  </element>
+	</optional>
+	<ref name="element-nodes"/>
+	<element name="resources">
+	  <externalRef href="resources-1.2.rng"/>
+	</element>
+	<element name="constraints">
+	  <externalRef href="constraints-1.2.rng"/>
+	</element>
+	<optional>
+	  <externalRef href="acls-1.2.rng"/>
+	</optional>
+	<optional>
+	  <externalRef href="fencing.rng"/>
+	</optional>
+      </interleave>
+    </element>
+    <element name="status">
+      <ref name="element-status"/>
+    </element>
+  </define>
+
+  <define name="attribute-options">
+    <externalRef href="versions.rng"/>
+    <optional>
+      <attribute name="crm_feature_set"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="remote-tls-port"><data type="nonNegativeInteger"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="remote-clear-port"><data type="nonNegativeInteger"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="have-quorum"><data type="boolean"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="dc-uuid"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="cib-last-written"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="no-quorum-panic"><data type="boolean"/></attribute>
+    </optional>
+    <optional>
+      <attribute name="update-origin"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="update-client"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="update-user"><text/></attribute>
+    </optional>
+  </define>
+
+  <define name="element-nodes">
+    <element name="nodes">
+      <zeroOrMore>
+	<element name="node">
+	  <attribute name="id"><text/></attribute>
+	  <attribute name="uname"><text/></attribute>
+	  <optional>
+	    <attribute name="type">
+	      <choice>
+		<value>normal</value>
+		<value>member</value>
+		<value>ping</value>
+	      </choice>
+	    </attribute>
+	  </optional>
+	  <optional>
+	    <attribute name="description"><text/></attribute>
+	  </optional>
+	  <optional>
+	    <externalRef href="score.rng"/>
+	  </optional>
+	  <zeroOrMore>
+	    <choice>
+	      <element name="instance_attributes">
+	        <externalRef href="nvset.rng"/>
+	      </element>
+	      <element name="utilization">
+	        <externalRef href="nvset.rng"/>
+	      </element>
+	    </choice>
+	  </zeroOrMore>
+	</element>
+      </zeroOrMore>
+    </element>
+  </define>
+
+  <define name="element-status">
+    <zeroOrMore>
+      <choice>
+        <attribute>
+          <anyName/>
+          <text/>
+        </attribute>
+        <element>
+          <anyName/>
+          <ref name="element-status"/>
+        </element>
+        <text/>
+      </choice>
+    </zeroOrMore>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/resources-1.0.rng b/test/unittests/schemas/resources-1.0.rng
new file mode 100644
index 0000000..7ea2228
--- /dev/null
+++ b/test/unittests/schemas/resources-1.0.rng
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-resources"/>
+  </start>
+
+  <define name="element-resources">
+      <zeroOrMore>
+	<choice>
+	  <ref name="element-primitive"/>
+	  <ref name="element-group"/>
+	  <ref name="element-clone"/>
+	  <ref name="element-master"/>
+	</choice>
+      </zeroOrMore>
+  </define>
+
+  <define name="element-primitive">
+    <element name="primitive">
+      <interleave>
+	<attribute name="id"><data type="ID"/></attribute>
+	<choice>
+	  <group>
+	    <attribute name="class"><value>ocf</value></attribute>
+	    <attribute name="provider"><text/></attribute>
+	  </group>
+	  <attribute name="class">
+	    <choice>
+	      <value>lsb</value>
+	      <value>heartbeat</value>
+	      <value>stonith</value>
+	      <value>upstart</value>
+	    </choice>
+	  </attribute>
+	</choice>
+	<attribute name="type"><text/></attribute>
+	<optional>
+	  <attribute name="description"><text/></attribute>
+	</optional>
+	<ref name="element-resource-extra"/>
+	<ref name="element-operations"/>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-group">
+    <element name="group">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<attribute name="description"><text/></attribute>
+      </optional>
+      <interleave>
+	<ref name="element-resource-extra"/>
+	<oneOrMore>
+	  <ref name="element-primitive"/>
+	</oneOrMore>
+      </interleave>
+    </element>
+  </define>
+ 
+  <define name="element-clone">
+    <element name="clone">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<attribute name="description"><text/></attribute>
+      </optional>
+      <interleave>
+	<ref name="element-resource-extra"/>
+	<choice>
+	  <ref name="element-primitive"/>
+	  <ref name="element-group"/>
+	</choice>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-master">
+    <element name="master">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<attribute name="description"><text/></attribute>
+      </optional>
+      <interleave>
+	<ref name="element-resource-extra"/>
+	<choice>
+	  <ref name="element-primitive"/>
+	  <ref name="element-group"/>
+	</choice>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-resource-extra">
+      <zeroOrMore>
+	<choice>
+	  <element name="meta_attributes">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+	  <element name="instance_attributes">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+	</choice>
+      </zeroOrMore>
+  </define>
+ 
+  <define name="element-operations">
+    <optional>
+      <element name="operations">
+	<optional>
+	  <attribute name="id"><data type="ID"/></attribute>
+	</optional>
+	<optional>
+	  <attribute name="id-ref"><data type="IDREF"/></attribute>
+	</optional>
+	<zeroOrMore>
+	  <element name="op">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <attribute name="name"><text/></attribute>
+	    <attribute name="interval"><text/></attribute>
+	    <optional>
+	      <attribute name="description"><text/></attribute>
+	    </optional>
+	    <optional>
+	      <choice>
+		<attribute name="start-delay"><text/></attribute>
+		<attribute name="interval-origin"><text/></attribute>
+	      </choice>
+	    </optional>
+	    <optional>
+	      <attribute name="timeout"><text/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="enabled"><data type="boolean"/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="record-pending"><data type="boolean"/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="role">
+		<choice>
+		  <value>Stopped</value>
+		  <value>Started</value>
+		  <value>Slave</value>
+		  <value>Master</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="requires">
+		<choice>
+		  <value>nothing</value>
+		  <value>quorum</value>
+		  <value>fencing</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="on-fail">
+		<choice>
+		  <value>ignore</value>
+		  <value>block</value>
+		  <value>stop</value>
+		  <value>restart</value>
+		  <value>standby</value>
+		  <value>fence</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	    <ref name="element-resource-extra"/>
+	  </element>
+	</zeroOrMore>
+      </element>
+    </optional>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/resources-1.1.rng b/test/unittests/schemas/resources-1.1.rng
new file mode 100644
index 0000000..81a8f82
--- /dev/null
+++ b/test/unittests/schemas/resources-1.1.rng
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-resources"/>
+  </start>
+
+  <define name="element-resources">
+      <zeroOrMore>
+	<choice>
+	  <ref name="element-primitive"/>
+	  <ref name="element-template"/>
+	  <ref name="element-group"/>
+	  <ref name="element-clone"/>
+	  <ref name="element-master"/>
+	</choice>
+      </zeroOrMore>
+  </define>
+
+  <define name="element-primitive">
+    <element name="primitive">
+      <interleave>
+	<attribute name="id"><data type="ID"/></attribute>
+	<choice>
+	  <group>
+	    <choice>
+	      <group>
+		<attribute name="class"><value>ocf</value></attribute>
+		<attribute name="provider"><text/></attribute>
+	      </group>
+	      <attribute name="class">
+		<choice>
+		  <value>lsb</value>
+		  <value>heartbeat</value>
+		  <value>stonith</value>
+		  <value>upstart</value>
+		  <value>service</value>
+		  <value>systemd</value>
+		  <value>nagios</value>
+		</choice>
+	      </attribute>
+	    </choice>
+	    <attribute name="type"><text/></attribute>
+	  </group>
+	  <attribute name="template"><data type="IDREF"/></attribute>
+	</choice>
+	<optional>
+	  <attribute name="description"><text/></attribute>
+	</optional>
+	<ref name="element-resource-extra"/>
+	<ref name="element-operations"/>
+      	<zeroOrMore>
+	  <element name="utilization">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+      	</zeroOrMore>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-template">
+    <element name="template">
+      <interleave>
+	<attribute name="id"><data type="ID"/></attribute>
+	<choice>
+	  <group>
+	    <attribute name="class"><value>ocf</value></attribute>
+	    <attribute name="provider"><text/></attribute>
+	  </group>
+	  <attribute name="class">
+	    <choice>
+	      <value>lsb</value>
+	      <value>heartbeat</value>
+	      <value>stonith</value>
+	      <value>upstart</value>
+	    </choice>
+	  </attribute>
+	</choice>
+	<attribute name="type"><text/></attribute>
+	<optional>
+	  <attribute name="description"><text/></attribute>
+	</optional>
+	<ref name="element-resource-extra"/>
+	<ref name="element-operations"/>
+      	<zeroOrMore>
+	  <element name="utilization">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+      	</zeroOrMore>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-group">
+    <element name="group">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<attribute name="description"><text/></attribute>
+      </optional>
+      <interleave>
+	<ref name="element-resource-extra"/>
+	<oneOrMore>
+	  <ref name="element-primitive"/>
+	</oneOrMore>
+      </interleave>
+    </element>
+  </define>
+ 
+  <define name="element-clone">
+    <element name="clone">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<attribute name="description"><text/></attribute>
+      </optional>
+      <interleave>
+	<ref name="element-resource-extra"/>
+	<choice>
+	  <ref name="element-primitive"/>
+	  <ref name="element-group"/>
+	</choice>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-master">
+    <element name="master">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<attribute name="description"><text/></attribute>
+      </optional>
+      <interleave>
+	<ref name="element-resource-extra"/>
+	<choice>
+	  <ref name="element-primitive"/>
+	  <ref name="element-group"/>
+	</choice>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-resource-extra">
+      <zeroOrMore>
+	<choice>
+	  <element name="meta_attributes">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+	  <element name="instance_attributes">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+	</choice>
+      </zeroOrMore>
+  </define>
+ 
+  <define name="element-operations">
+    <optional>
+      <element name="operations">
+	<optional>
+	  <attribute name="id"><data type="ID"/></attribute>
+	</optional>
+	<optional>
+	  <attribute name="id-ref"><data type="IDREF"/></attribute>
+	</optional>
+	<zeroOrMore>
+	  <element name="op">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <attribute name="name"><text/></attribute>
+	    <attribute name="interval"><text/></attribute>
+	    <optional>
+	      <attribute name="description"><text/></attribute>
+	    </optional>
+	    <optional>
+	      <choice>
+		<attribute name="start-delay"><text/></attribute>
+		<attribute name="interval-origin"><text/></attribute>
+	      </choice>
+	    </optional>
+	    <optional>
+	      <attribute name="timeout"><text/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="enabled"><data type="boolean"/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="record-pending"><data type="boolean"/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="role">
+		<choice>
+		  <value>Stopped</value>
+		  <value>Started</value>
+		  <value>Slave</value>
+		  <value>Master</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="requires">
+		<choice>
+		  <value>nothing</value>
+		  <value>quorum</value>
+		  <value>fencing</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="on-fail">
+		<choice>
+		  <value>ignore</value>
+		  <value>block</value>
+		  <value>stop</value>
+		  <value>restart</value>
+		  <value>standby</value>
+		  <value>fence</value>
+		  <value>restart-container</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	    <ref name="element-resource-extra"/>
+	  </element>
+	</zeroOrMore>
+      </element>
+    </optional>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/resources-1.2.rng b/test/unittests/schemas/resources-1.2.rng
new file mode 100644
index 0000000..81a8f82
--- /dev/null
+++ b/test/unittests/schemas/resources-1.2.rng
@@ -0,0 +1,225 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-resources"/>
+  </start>
+
+  <define name="element-resources">
+      <zeroOrMore>
+	<choice>
+	  <ref name="element-primitive"/>
+	  <ref name="element-template"/>
+	  <ref name="element-group"/>
+	  <ref name="element-clone"/>
+	  <ref name="element-master"/>
+	</choice>
+      </zeroOrMore>
+  </define>
+
+  <define name="element-primitive">
+    <element name="primitive">
+      <interleave>
+	<attribute name="id"><data type="ID"/></attribute>
+	<choice>
+	  <group>
+	    <choice>
+	      <group>
+		<attribute name="class"><value>ocf</value></attribute>
+		<attribute name="provider"><text/></attribute>
+	      </group>
+	      <attribute name="class">
+		<choice>
+		  <value>lsb</value>
+		  <value>heartbeat</value>
+		  <value>stonith</value>
+		  <value>upstart</value>
+		  <value>service</value>
+		  <value>systemd</value>
+		  <value>nagios</value>
+		</choice>
+	      </attribute>
+	    </choice>
+	    <attribute name="type"><text/></attribute>
+	  </group>
+	  <attribute name="template"><data type="IDREF"/></attribute>
+	</choice>
+	<optional>
+	  <attribute name="description"><text/></attribute>
+	</optional>
+	<ref name="element-resource-extra"/>
+	<ref name="element-operations"/>
+      	<zeroOrMore>
+	  <element name="utilization">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+      	</zeroOrMore>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-template">
+    <element name="template">
+      <interleave>
+	<attribute name="id"><data type="ID"/></attribute>
+	<choice>
+	  <group>
+	    <attribute name="class"><value>ocf</value></attribute>
+	    <attribute name="provider"><text/></attribute>
+	  </group>
+	  <attribute name="class">
+	    <choice>
+	      <value>lsb</value>
+	      <value>heartbeat</value>
+	      <value>stonith</value>
+	      <value>upstart</value>
+	    </choice>
+	  </attribute>
+	</choice>
+	<attribute name="type"><text/></attribute>
+	<optional>
+	  <attribute name="description"><text/></attribute>
+	</optional>
+	<ref name="element-resource-extra"/>
+	<ref name="element-operations"/>
+      	<zeroOrMore>
+	  <element name="utilization">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+      	</zeroOrMore>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-group">
+    <element name="group">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<attribute name="description"><text/></attribute>
+      </optional>
+      <interleave>
+	<ref name="element-resource-extra"/>
+	<oneOrMore>
+	  <ref name="element-primitive"/>
+	</oneOrMore>
+      </interleave>
+    </element>
+  </define>
+ 
+  <define name="element-clone">
+    <element name="clone">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<attribute name="description"><text/></attribute>
+      </optional>
+      <interleave>
+	<ref name="element-resource-extra"/>
+	<choice>
+	  <ref name="element-primitive"/>
+	  <ref name="element-group"/>
+	</choice>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-master">
+    <element name="master">
+      <attribute name="id"><data type="ID"/></attribute>
+      <optional>
+	<attribute name="description"><text/></attribute>
+      </optional>
+      <interleave>
+	<ref name="element-resource-extra"/>
+	<choice>
+	  <ref name="element-primitive"/>
+	  <ref name="element-group"/>
+	</choice>
+      </interleave>
+    </element>
+  </define>
+
+  <define name="element-resource-extra">
+      <zeroOrMore>
+	<choice>
+	  <element name="meta_attributes">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+	  <element name="instance_attributes">
+	    <externalRef href="nvset.rng"/>
+	  </element>
+	</choice>
+      </zeroOrMore>
+  </define>
+ 
+  <define name="element-operations">
+    <optional>
+      <element name="operations">
+	<optional>
+	  <attribute name="id"><data type="ID"/></attribute>
+	</optional>
+	<optional>
+	  <attribute name="id-ref"><data type="IDREF"/></attribute>
+	</optional>
+	<zeroOrMore>
+	  <element name="op">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <attribute name="name"><text/></attribute>
+	    <attribute name="interval"><text/></attribute>
+	    <optional>
+	      <attribute name="description"><text/></attribute>
+	    </optional>
+	    <optional>
+	      <choice>
+		<attribute name="start-delay"><text/></attribute>
+		<attribute name="interval-origin"><text/></attribute>
+	      </choice>
+	    </optional>
+	    <optional>
+	      <attribute name="timeout"><text/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="enabled"><data type="boolean"/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="record-pending"><data type="boolean"/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="role">
+		<choice>
+		  <value>Stopped</value>
+		  <value>Started</value>
+		  <value>Slave</value>
+		  <value>Master</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="requires">
+		<choice>
+		  <value>nothing</value>
+		  <value>quorum</value>
+		  <value>fencing</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="on-fail">
+		<choice>
+		  <value>ignore</value>
+		  <value>block</value>
+		  <value>stop</value>
+		  <value>restart</value>
+		  <value>standby</value>
+		  <value>fence</value>
+		  <value>restart-container</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	    <ref name="element-resource-extra"/>
+	  </element>
+	</zeroOrMore>
+      </element>
+    </optional>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/rule.rng b/test/unittests/schemas/rule.rng
new file mode 100644
index 0000000..242eff8
--- /dev/null
+++ b/test/unittests/schemas/rule.rng
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+	 xmlns:ann="http://relaxng.org/ns/compatibility/annotations/1.0"
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="element-rule"/>
+  </start>
+
+  <define name="element-rule">
+    <element name="rule">
+     <choice>
+     <attribute name="id-ref"><data type="IDREF"/></attribute>
+     <group>
+      <attribute name="id"><data type="ID"/></attribute>
+      <choice>
+	<externalRef href="score.rng"/>
+	<attribute name="score-attribute"><text/></attribute>
+      </choice>
+      <optional>
+	<attribute name="boolean-op">
+	  <choice>
+	    <value>or</value>
+	    <value>and</value>
+	  </choice>
+	</attribute>
+      </optional>
+      <optional>
+	<attribute name="role"><text/></attribute>
+      </optional>
+      <oneOrMore>
+	<choice>
+          <element name="expression">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <attribute name="attribute"><text/></attribute>
+	    <attribute name="operation">
+	      <choice>
+		<value>lt</value>
+		<value>gt</value>
+		<value>lte</value>
+		<value>gte</value>
+		<value>eq</value>
+		<value>ne</value>
+		<value>defined</value>
+		<value>not_defined</value>
+	      </choice>
+	    </attribute>
+	    <optional>
+	      <attribute name="value"><text/></attribute>
+	    </optional>
+	    <optional>
+	      <attribute name="type" ann:defaultValue="string">
+		<choice>
+		  <value>string</value>
+		  <value>number</value>
+		  <value>version</value>
+		</choice>
+	      </attribute>
+	    </optional>
+	  </element>
+          <element name="date_expression">
+	    <attribute name="id"><data type="ID"/></attribute>
+	    <choice>
+	      <group>
+		<attribute name="operation"><value>in_range</value></attribute>
+		<choice>
+		  <group>
+		    <optional>
+		      <attribute name="start"><text/></attribute>
+		    </optional>
+		    <attribute name="end"><text/></attribute>
+		  </group>
+		  <group>
+		    <attribute name="start"><text/></attribute>
+		    <element name="duration">
+		      <ref name="date-common"/>
+		    </element>
+		  </group>
+		</choice>
+	      </group>
+	      <group>
+		<attribute name="operation"><value>gt</value></attribute>
+		<attribute name="start"><text/></attribute>
+	      </group>
+	      <group>
+		<attribute name="operation"><value>lt</value></attribute>
+		<choice>
+		  <attribute name="end"><text/></attribute>
+		</choice>
+	      </group>
+	      <group>
+		<attribute name="operation"><value>date_spec</value></attribute>
+		<element name="date_spec">
+		  <ref name="date-common"/>
+		</element> 
+	      </group>
+	    </choice>
+	  </element> 
+	  <ref name="element-rule"/>
+	</choice>
+      </oneOrMore>
+     </group>
+     </choice>
+    </element>
+  </define>
+
+  <define name="date-common">
+    <attribute name="id"><data type="ID"/></attribute>
+    <optional>
+      <attribute name="hours"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="monthdays"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="weekdays"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="yearsdays"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="months"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="weeks"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="years"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="weekyears"><text/></attribute>
+    </optional>
+    <optional>
+      <attribute name="moon"><text/></attribute>
+    </optional>
+  </define>
+
+</grammar>
diff --git a/test/unittests/schemas/score.rng b/test/unittests/schemas/score.rng
new file mode 100644
index 0000000..57b10f2
--- /dev/null
+++ b/test/unittests/schemas/score.rng
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="attribute-score"/>
+  </start>
+
+  <define name="attribute-score">
+    <attribute name="score">
+      <choice>
+	<data type="integer"/>
+	<value>INFINITY</value>
+	<value>+INFINITY</value>
+	<value>-INFINITY</value>
+      </choice>
+    </attribute>
+  </define>
+</grammar>
diff --git a/test/unittests/schemas/versions.rng b/test/unittests/schemas/versions.rng
new file mode 100644
index 0000000..ab4e4ea
--- /dev/null
+++ b/test/unittests/schemas/versions.rng
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0" 
+         datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
+  <start>
+      <ref name="attribute-version"/>
+  </start>
+
+  <define name="attribute-version">
+    <attribute name="validate-with">
+      <choice>
+	<value>none</value>
+	<value>pacemaker-0.6</value>
+	<value>transitional-0.6</value>
+	<value>pacemaker-0.7</value>
+	<value>pacemaker-1.0</value>
+	<value>pacemaker-1.1</value>
+	<value>pacemaker-1.2</value>
+      </choice>
+    </attribute>
+    <attribute name="admin_epoch"><data type="nonNegativeInteger"/></attribute>
+    <attribute name="epoch"><data type="nonNegativeInteger"/></attribute>
+    <attribute name="num_updates"><data type="nonNegativeInteger"/></attribute>
+  </define>
+</grammar>
diff --git a/test/unittests/test.conf b/test/unittests/test.conf
new file mode 100644
index 0000000..fe75686
--- /dev/null
+++ b/test/unittests/test.conf
@@ -0,0 +1,12 @@
+[path]
+sharedir = ../../doc
+cache = ../../doc
+crm_config = .
+crm_daemon_dir = .
+crm_daemon_user = hacluster
+ocf_root = .
+crm_dtd_dir = .
+pe_state_dir = .
+heartbeat_dir = .
+hb_delnode = ./hb_delnode
+nagios_plugins = .
diff --git a/test/unittests/test_bugs.py b/test/unittests/test_bugs.py
new file mode 100644
index 0000000..6408ec5
--- /dev/null
+++ b/test/unittests/test_bugs.py
@@ -0,0 +1,446 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+
+import cibconfig
+from lxml import etree
+from nose.tools import eq_
+import xmlutil
+
+factory = cibconfig.cib_factory
+
+
+def setup_func():
+    "set up test fixtures"
+    import idmgmt
+    idmgmt.clear()
+
+
+def test_bug41660_1():
+    xml = """<primitive id="bug41660" class="ocf" provider="pacemaker" type="Dummy"> \
+    <meta_attributes id="bug41660-meta"> \
+    <nvpair id="bug41660-meta-target-role" name="target-role" value="Stopped"/> \
+    </meta_attributes> \
+    </primitive>
+"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    print etree.tostring(obj.node)
+    data = obj.repr_cli(format=-1)
+    print data
+    exp = 'primitive bug41660 ocf:pacemaker:Dummy meta target-role=Stopped'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+    commit_holder = factory.commit
+    try:
+        factory.commit = lambda *args: True
+        from ui_resource import set_deep_meta_attr
+        set_deep_meta_attr("bug41660", "target-role", "Started")
+        eq_(['Started'],
+            obj.node.xpath('.//nvpair[@name="target-role"]/@value'))
+    finally:
+        factory.commit = commit_holder
+
+
+def test_bug41660_2():
+    xml = """
+<clone id="libvirtd-clone">
+ <primitive class="lsb" id="libvirtd" type="libvirtd">
+  <operations>
+   <op id="libvirtd-monitor-interval-15" interval="15" name="monitor" start-delay="15" timeout="15"/>
+   <op id="libvirtd-start-interval-0" interval="0" name="start" on-fail="restart" timeout="15"/>
+   <op id="libvirtd-stop-interval-0" interval="0" name="stop" on-fail="ignore" timeout="15"/>
+  </operations>
+  <meta_attributes id="libvirtd-meta_attributes"/>
+ </primitive>
+ <meta_attributes id="libvirtd-clone-meta">
+  <nvpair id="libvirtd-interleave" name="interleave" value="true"/>
+  <nvpair id="libvirtd-ordered" name="ordered" value="true"/>
+  <nvpair id="libvirtd-clone-meta-target-role" name="target-role" value="Stopped"/>
+ </meta_attributes>
+</clone>
+"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    #data = obj.repr_cli(format=-1)
+    #print data
+    #exp = 'clone libvirtd-clone libvirtd meta interleave=true ordered=true target-role=Stopped'
+    #assert data == exp
+    #assert obj.cli_use_validate()
+
+    print etree.tostring(obj.node)
+
+    commit_holder = factory.commit
+    try:
+        factory.commit = lambda *args: True
+        from ui_resource import set_deep_meta_attr
+        print "PRE", etree.tostring(obj.node)
+        set_deep_meta_attr("libvirtd-clone", "target-role", "Started")
+        print "POST", etree.tostring(obj.node)
+        eq_(['Started'],
+            obj.node.xpath('.//nvpair[@name="target-role"]/@value'))
+    finally:
+        factory.commit = commit_holder
+
+
+def test_bug41660_3():
+    xml = """
+<clone id="libvirtd-clone">
+ <primitive class="lsb" id="libvirtd" type="libvirtd">
+  <operations>
+   <op id="libvirtd-monitor-interval-15" interval="15" name="monitor" start-delay="15" timeout="15"/>
+   <op id="libvirtd-start-interval-0" interval="0" name="start" on-fail="restart" timeout="15"/>
+   <op id="libvirtd-stop-interval-0" interval="0" name="stop" on-fail="ignore" timeout="15"/>
+  </operations>
+  <meta_attributes id="libvirtd-meta_attributes"/>
+ </primitive>
+ <meta_attributes id="libvirtd-clone-meta_attributes">
+ <nvpair id="libvirtd-clone-meta_attributes-target-role" name="target-role" value="Stopped"/>
+ </meta_attributes>
+</clone>
+"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    data = obj.repr_cli(format=-1)
+    print data
+    exp = 'clone libvirtd-clone libvirtd meta target-role=Stopped'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+    commit_holder = factory.commit
+    try:
+        factory.commit = lambda *args: True
+        from ui_resource import set_deep_meta_attr
+        set_deep_meta_attr("libvirtd-clone", "target-role", "Started")
+        eq_(['Started'],
+            obj.node.xpath('.//nvpair[@name="target-role"]/@value'))
+    finally:
+        factory.commit = commit_holder
+
+
+def test_comments():
+    xml = """<cib epoch="25" num_updates="1" admin_epoch="0" validate-with="pacemaker-1.2" cib-last-written="Thu Mar  6 15:53:49 2014" update-origin="beta1" update-client="cibadmin" update-user="root" crm_feature_set="3.0.8" have-quorum="1" dc-uuid="1">
+  <configuration>
+    <crm_config>
+      <cluster_property_set id="cib-bootstrap-options">
+        <nvpair id="cib-bootstrap-options-dc-version" name="dc-version" value="1.1.11-3.3-3ca8c3b"/>
+        <nvpair id="cib-bootstrap-options-cluster-infrastructure" name="cluster-infrastructure" value="corosync"/>
+        <!--# COMMENT TEXT 1 -->
+      </cluster_property_set>
+    </crm_config>
+    <nodes>
+      <node uname="beta1" id="1">
+        <!--# COMMENT TEXT 2 -->
+      </node>
+    </nodes>
+    <resources/>
+    <constraints/>
+    <rsc_defaults>
+      <meta_attributes id="rsc-options">
+        <nvpair name="resource-stickiness" value="1" id="rsc-options-resource-stickiness"/>
+        <!--# COMMENT TEXT 3 -->
+      </meta_attributes>
+    </rsc_defaults>
+  </configuration>
+  <status>
+    <node_state id="1" uname="beta1" in_ccm="true" crmd="online" crm-debug-origin="do_state_transition" join="member" expected="member">
+      <lrm id="1">
+        <lrm_resources/>
+      </lrm>
+      <transient_attributes id="1">
+        <instance_attributes id="status-1">
+          <nvpair id="status-1-shutdown" name="shutdown" value="0"/>
+          <nvpair id="status-1-probe_complete" name="probe_complete" value="true"/>
+        </instance_attributes>
+      </transient_attributes>
+    </node_state>
+  </status>
+</cib>"""
+    elems = etree.fromstring(xml)
+    xmlutil.sanitize_cib(elems)
+    assert etree.tostring(elems).count("COMMENT TEXT") == 3
+
+
+def test_eq1():
+    xml1 = """<cluster_property_set id="cib-bootstrap-options">
+    <nvpair id="cib-bootstrap-options-stonith-enabled" name="stonith-enabled" value="true"></nvpair>
+    <nvpair id="cib-bootstrap-options-stonith-timeout" name="stonith-timeout" value="180"></nvpair>
+    <nvpair id="cib-bootstrap-options-symmetric-cluster" name="symmetric-cluster" value="false"></nvpair>
+    <nvpair id="cib-bootstrap-options-no-quorum-policy" name="no-quorum-policy" value="freeze"></nvpair>
+    <nvpair id="cib-bootstrap-options-batch-limit" name="batch-limit" value="20"></nvpair>
+    <nvpair id="cib-bootstrap-options-dc-version" name="dc-version" value="1.1.10-c1a326d"></nvpair>
+    <nvpair id="cib-bootstrap-options-cluster-infrastructure" name="cluster-infrastructure" value="corosync"></nvpair>
+    <nvpair id="cib-bootstrap-options-last-lrm-refresh" name="last-lrm-refresh" value="1391433789"></nvpair>
+    <nvpair id="cib-bootstrap-options-is-managed-default" name="is-managed-default" value="true"></nvpair>
+  </cluster_property_set>
+  """
+    xml2 = """<cluster_property_set id="cib-bootstrap-options">
+    <nvpair id="cib-bootstrap-options-stonith-enabled" name="stonith-enabled" value="true"></nvpair>
+    <nvpair id="cib-bootstrap-options-stonith-timeout" name="stonith-timeout" value="180"></nvpair>
+    <nvpair id="cib-bootstrap-options-symmetric-cluster" name="symmetric-cluster" value="false"></nvpair>
+    <nvpair id="cib-bootstrap-options-no-quorum-policy" name="no-quorum-policy" value="freeze"></nvpair>
+    <nvpair id="cib-bootstrap-options-batch-limit" name="batch-limit" value="20"></nvpair>
+    <nvpair id="cib-bootstrap-options-dc-version" name="dc-version" value="1.1.10-c1a326d"></nvpair>
+    <nvpair id="cib-bootstrap-options-cluster-infrastructure" name="cluster-infrastructure" value="corosync"></nvpair>
+    <nvpair id="cib-bootstrap-options-last-lrm-refresh" name="last-lrm-refresh" value="1391433789"></nvpair>
+    <nvpair id="cib-bootstrap-options-is-managed-default" name="is-managed-default" value="true"></nvpair>
+  </cluster_property_set>
+    """
+    e1 = etree.fromstring(xml1)
+    e2 = etree.fromstring(xml2)
+    assert xmlutil.xml_equals(e1, e2, show=True)
+
+
+def test_pcs_interop_1():
+    """
+    pcs<>crmsh interop bug
+    """
+
+    xml = """<clone id="dummies">
+        <meta_attributes id="dummies-meta">
+          <nvpair name="globally-unique" value="false" id="dummies-meta-globally-unique"/>
+        </meta_attributes>
+        <meta_attributes id="dummies-meta_attributes">
+          <nvpair id="dummies-meta_attributes-target-role" name="target-role" value="Stopped"/>
+        </meta_attributes>
+        <primitive id="dummy-1" class="ocf" provider="heartbeat" type="Dummy"/>
+      </clone>"""
+    elem = etree.fromstring(xml)
+    from ui_resource import set_deep_meta_attr_node
+
+    assert len(elem.xpath(".//meta_attributes/nvpair[@name='target-role']")) == 1
+
+    print "BEFORE:", etree.tostring(elem)
+
+    set_deep_meta_attr_node(elem, 'target-role', 'Stopped')
+
+    print "AFTER:", etree.tostring(elem)
+
+    assert len(elem.xpath(".//meta_attributes/nvpair[@name='target-role']")) == 1
+
+
+def test_bnc878128():
+    """
+    L3: "crm configure show" displays XML information instead of typical crm output.
+    """
+    xml = """<rsc_location id="cli-prefer-dummy-resource" rsc="dummy-resource"
+role="Started">
+  <rule id="cli-prefer-rule-dummy-resource" score="INFINITY">
+    <expression id="cli-prefer-expr-dummy-resource" attribute="#uname"
+operation="eq" value="x64-4"/>
+    <date_expression id="cli-prefer-lifetime-end-dummy-resource" operation="lt"
+end="2014-05-17 17:56:11Z"/>
+  </rule>
+</rsc_location>"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    data = obj.repr_cli(format=-1)
+    print "OUTPUT:", data
+    exp = 'location cli-prefer-dummy-resource dummy-resource role=Started rule #uname eq x64-4 and date lt "2014-05-17 17:56:11Z"'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+
+def test_order_without_score_kind():
+    """
+    Spec says order doesn't require score or kind to be set
+    """
+    xml = '<rsc_order first="a" first-action="promote" id="order-a-b" then="b" then-action="start"/>'
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    data = obj.repr_cli(format=-1)
+    print "OUTPUT:", data
+    exp = 'order order-a-b a:promote b:start'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+
+
+def test_bnc878112():
+    """
+    crm configure group can hijack a cloned primitive (and then crash)
+    """
+    obj1 = factory.create_object('primitive', 'p1', 'Dummy')
+    assert obj1 is not None
+    obj2 = factory.create_object('group', 'g1', 'p1')
+    assert obj2 is not None
+    obj3 = factory.create_object('group', 'g2', 'p1')
+    print obj3
+    assert obj3 is False
+
+
+def test_copy_nvpairs():
+    from cibconfig import copy_nvpairs
+
+    to = etree.fromstring('''
+    <node>
+    <nvpair name="stonith-enabled" value="true"/>
+    </node>
+    ''')
+    copy_nvpairs(to, etree.fromstring('''
+    <node>
+    <nvpair name="stonith-enabled" value="false"/>
+    </node>
+    '''))
+
+    eq_(['stonith-enabled'], to.xpath('./nvpair/@name'))
+    eq_(['false'], to.xpath('./nvpair/@value'))
+
+    copy_nvpairs(to, etree.fromstring('''
+    <node>
+    <nvpair name="stonith-enabled" value="true"/>
+    </node>
+    '''))
+
+    eq_(['stonith-enabled'], to.xpath('./nvpair/@name'))
+    eq_(['true'], to.xpath('./nvpair/@value'))
+
+
+def test_pengine_test():
+    xml = '''<primitive class="ocf" id="rsc1" provider="pacemaker" type="Dummy">
+        <instance_attributes id="rsc1-instance_attributes-1">
+          <nvpair id="rsc1-instance_attributes-1-state" name="state" value="/var/run/Dummy-rsc1-clusterA"/>
+          <rule id="rsc1-instance_attributes-1-rule-1" score="0">
+            <expression id="rsc1-instance_attributes-1-rule-1-expr-1" attribute="#cluster-name" operation="eq" value="clusterA"/>
+          </rule>
+        </instance_attributes>
+        <instance_attributes id="rsc1-instance_attributes-2">
+          <nvpair id="rsc1-instance_attributes-2-state" name="state" value="/var/run/Dummy-rsc1-clusterB"/>
+          <rule id="rsc1-instance_attributes-2-rule-1" score="0">
+            <expression id="rsc1-instance_attributes-2-rule-1-expr-1" attribute="#cluster-name" operation="eq" value="clusterB"/>
+          </rule>
+        </instance_attributes>
+        <operations>
+          <op id="rsc1-monitor-10" interval="10" name="monitor"/>
+        </operations>
+      </primitive>'''
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    data = obj.repr_cli(format=-1)
+    print "OUTPUT:", data
+    exp = 'primitive rsc1 ocf:pacemaker:Dummy params rule 0: #cluster-name eq clusterA state="/var/run/Dummy-rsc1-clusterA" params rule 0: #cluster-name eq clusterB state="/var/run/Dummy-rsc1-clusterB" op monitor interval=10'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+
+def test_tagset():
+    xml = '''<primitive class="ocf" id="%s" provider="pacemaker" type="Dummy"/>'''
+    tag = '''<tag id="t0"><obj_ref id="r1"/><obj_ref id="r2"/></tag>'''
+    factory.create_from_node(etree.fromstring(xml % ('r1')))
+    factory.create_from_node(etree.fromstring(xml % ('r2')))
+    factory.create_from_node(etree.fromstring(xml % ('r3')))
+    factory.create_from_node(etree.fromstring(tag))
+    elems = factory.get_elems_on_tag("tag:t0")
+    assert set(x.obj_id for x in elems) == set(['r1', 'r2'])
+
+def test_ratrace():
+    xml = '''<primitive class="ocf" id="%s" provider="pacemaker" type="Dummy"/>'''
+    factory.create_from_node(etree.fromstring(xml % ('r1')))
+    factory.create_from_node(etree.fromstring(xml % ('r2')))
+    factory.create_from_node(etree.fromstring(xml % ('r3')))
+
+    context = object()
+
+    from ui_resource import RscMgmt
+    obj = factory.find_object('r1')
+    RscMgmt()._trace_resource(context, 'r1', obj)
+
+    obj = factory.find_object('r1')
+    ops = obj.node.xpath('./operations/op')
+    for op in ops:
+        assert op.xpath('./instance_attributes/nvpair[@name="trace_ra"]/@value') == ["1"]
+    assert set(obj.node.xpath('./operations/op/@name')) == set(['start', 'stop'])
+
+
+def test_op_role():
+    xml = '''<primitive class="ocf" id="rsc2" provider="pacemaker" type="Dummy">
+        <operations>
+          <op id="rsc2-monitor-10" interval="10" name="monitor" role="Stopped"/>
+        </operations>
+      </primitive>'''
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    data = obj.repr_cli(format=-1)
+    print "OUTPUT:", data
+    exp = 'primitive rsc2 ocf:pacemaker:Dummy op monitor interval=10 role=Stopped'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+
+def test_nvpair_no_value():
+    xml = '''<primitive class="ocf" id="rsc3" provider="heartbeat" type="Dummy">
+        <instance_attributes id="rsc3-instance_attributes-1">
+          <nvpair id="rsc3-instance_attributes-1-verbose" name="verbose"/>
+          <nvpair id="rsc3-instance_attributes-1-verbase" name="verbase" value=""/>
+          <nvpair id="rsc3-instance_attributes-1-verbese" name="verbese" value=" "/>
+        </instance_attributes>
+      </primitive>'''
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    data = obj.repr_cli(format=-1)
+    print "OUTPUT:", data
+    exp = 'primitive rsc3 Dummy params verbose verbase="" verbese=" "'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+
+def test_delete_ticket():
+    xml0 = '<primitive id="daa0" class="ocf" provider="heartbeat" type="Dummy"/>'
+    xml1 = '<primitive id="daa1" class="ocf" provider="heartbeat" type="Dummy"/>'
+    xml2 = '''<rsc_ticket id="taa0" ticket="taaA">
+        <resource_set id="taa0-0">
+          <resource_ref id="daa0"/>
+          <resource_ref id="daa1"/>
+        </resource_set>
+      </rsc_ticket>'''
+    for x in (xml0, xml1, xml2):
+        data = etree.fromstring(x)
+        obj = factory.create_from_node(data)
+        assert obj is not None
+        data = obj.repr_cli(format=-1)
+
+    factory.delete('daa0')
+    assert factory.find_object('daa0') is None
+    assert factory.find_object('taa0') is not None
+
+
+def test_quotes():
+    """
+    Parsing escaped quotes
+    """
+    xml = '''<primitive class="ocf" id="q1" provider="pacemaker" type="Dummy">
+        <instance_attributes id="q1-instance_attributes-1">
+          <nvpair id="q1-instance_attributes-1-state" name="state" value="foo"foo""/>
+        </instance_attributes>
+    </primitive>
+    '''
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    data = obj.repr_cli(format=-1)
+    print "OUTPUT:", data
+    exp = 'primitive q1 ocf:pacemaker:Dummy params state="foo\\"foo\\""'
+    assert data == exp
+    assert obj.cli_use_validate()
diff --git a/test/unittests/test_cib.py b/test/unittests/test_cib.py
new file mode 100644
index 0000000..7bdefcc
--- /dev/null
+++ b/test/unittests/test_cib.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2015 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+import cibconfig
+from lxml import etree
+from nose.tools import eq_
+import copy
+
+factory = cibconfig.cib_factory
+
+
+def setup_func():
+    "set up test fixtures"
+    import idmgmt
+    idmgmt.clear()
+
+
+def test_cib_schema_change():
+    "Changing the validate-with CIB attribute"
+    copy_of_cib = copy.copy(factory.cib_orig)
+    print etree.tostring(copy_of_cib, pretty_print=True)
+    tmp_cib_objects = factory.cib_objects
+    factory.cib_objects = []
+    factory.change_schema("pacemaker-1.1")
+    factory.cib_objects = tmp_cib_objects
+    factory._copy_cib_attributes(copy_of_cib, factory.cib_orig)
+    eq_(factory.cib_attrs["validate-with"], "pacemaker-1.1")
+    eq_(factory.cib_elem.get("validate-with"), "pacemaker-1.1")
diff --git a/test/unittests/test_cliformat.py b/test/unittests/test_cliformat.py
new file mode 100644
index 0000000..e9a40bf
--- /dev/null
+++ b/test/unittests/test_cliformat.py
@@ -0,0 +1,227 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+# unit tests for cliformat.py
+
+import cibconfig
+from lxml import etree
+from test_parse import MockValidation
+from nose.tools import eq_
+
+factory = cibconfig.cib_factory
+
+
+def assert_is_not_none(thing):
+    assert thing is not None, "Expected non-None value"
+
+
+def roundtrip(cli, debug=False, expected=None):
+    node, _, _ = cibconfig.parse_cli_to_xml(cli, validation=MockValidation())
+    assert_is_not_none(node)
+    obj = factory.create_from_node(node)
+    assert_is_not_none(obj)
+    obj.nocli = True
+    xml = obj.repr_cli(format=-1)
+    print xml
+    obj.nocli = False
+    s = obj.repr_cli(format=-1)
+    if (s != cli) or debug:
+        print "GOT:", s
+        print "EXP:", cli
+    assert obj.cli_use_validate()
+    if expected is not None:
+        eq_(expected, s)
+    else:
+        eq_(cli, s)
+    assert not debug
+
+
+def setup_func():
+    "set up test fixtures"
+    import idmgmt
+    idmgmt.clear()
+
+
+def teardown_func():
+    "tear down test fixtures"
+
+
+def test_rscset():
+    roundtrip('colocation foo inf: a b')
+    roundtrip('order order_2 Mandatory: [ A B ] C')
+    roundtrip('rsc_template public_vm Xen')
+
+
+def test_group():
+    factory.create_from_cli('primitive p1 Dummy')
+    roundtrip('group g1 p1 params target-role=Stopped')
+
+
+def test_bnc863736():
+    roundtrip('order order_3 Mandatory: [ A B ] C symmetrical=true')
+
+
+def test_sequential():
+    roundtrip('colocation rsc_colocation-master inf: [ vip-master vip-rep sequential=true ] [ msPostgresql:Master sequential=true ]')
+
+def test_broken_colo():
+    xml = """<rsc_colocation id="colo-2" score="INFINITY">
+  <resource_set id="colo-2-0" require-all="false">
+    <resource_ref id="vip1"/>
+    <resource_ref id="vip2"/>
+  </resource_set>
+  <resource_set id="colo-2-1" require-all="false" role="Master">
+    <resource_ref id="apache"/>
+  </resource_set>
+</rsc_colocation>"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert_is_not_none(obj)
+    data = obj.repr_cli(format=-1)
+    eq_('colocation colo-2 inf: [ vip1 vip2 sequential=true ] [ apache:Master sequential=true ]', data)
+    assert obj.cli_use_validate()
+
+
+def test_comment():
+    roundtrip("# comment 1\nprimitive d0 ocf:pacemaker:Dummy")
+
+
+def test_comment2():
+    roundtrip("# comment 1\n# comment 2\n# comment 3\nprimitive d0 ocf:pacemaker:Dummy")
+
+
+def test_nvpair_ref1():
+    factory.create_from_cli("primitive dummy-0 Dummy params $fiz:buz=bin")
+    roundtrip('primitive dummy-1 Dummy params @fiz:boz')
+
+
+def test_idresolve():
+    factory.create_from_cli("primitive dummy-5 Dummy params buz=bin")
+    roundtrip('primitive dummy-1 Dummy params @dummy-5-instance_attributes-buz')
+
+
+def test_ordering():
+    xml = """<primitive id="dummy" class="ocf" provider="pacemaker" type="Dummy"> \
+  <operations> \
+    <op name="start" timeout="60" interval="0" id="dummy-start-0"/> \
+    <op name="stop" timeout="60" interval="0" id="dummy-stop-0"/> \
+    <op name="monitor" interval="60" timeout="30" id="dummy-monitor-60"/> \
+  </operations> \
+  <meta_attributes id="dummy-meta_attributes"> \
+    <nvpair id="dummy-meta_attributes-target-role" name="target-role"
+value="Stopped"/> \
+  </meta_attributes> \
+</primitive>"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert_is_not_none(obj)
+    data = obj.repr_cli(format=-1)
+    print data
+    exp = 'primitive dummy ocf:pacemaker:Dummy op start timeout=60 interval=0 op stop timeout=60 interval=0 op monitor interval=60 timeout=30 meta target-role=Stopped'
+    eq_(exp, data)
+    assert obj.cli_use_validate()
+
+
+def test_ordering2():
+    xml = """<primitive id="dummy2" class="ocf" provider="pacemaker" type="Dummy"> \
+  <meta_attributes id="dummy2-meta_attributes"> \
+    <nvpair id="dummy2-meta_attributes-target-role" name="target-role"
+value="Stopped"/> \
+  </meta_attributes> \
+  <operations> \
+    <op name="start" timeout="60" interval="0" id="dummy2-start-0"/> \
+    <op name="stop" timeout="60" interval="0" id="dummy2-stop-0"/> \
+    <op name="monitor" interval="60" timeout="30" id="dummy2-monitor-60"/> \
+  </operations> \
+</primitive>"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert_is_not_none(obj)
+    data = obj.repr_cli(format=-1)
+    print data
+    exp = 'primitive dummy2 ocf:pacemaker:Dummy meta target-role=Stopped ' \
+          'op start timeout=60 interval=0 op stop timeout=60 interval=0 ' \
+          'op monitor interval=60 timeout=30'
+    eq_(exp, data)
+    assert obj.cli_use_validate()
+
+def test_fencing():
+    xml = """<fencing-topology>
+    <fencing-level devices="st1" id="fencing" index="1"
+target="ha-three"></fencing-level>
+    <fencing-level devices="st1" id="fencing-0" index="1"
+target="ha-two"></fencing-level>
+    <fencing-level devices="st1" id="fencing-1" index="1"
+target="ha-one"></fencing-level>
+  </fencing-topology>"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert_is_not_none(obj)
+    data = obj.repr_cli(format=-1)
+    print data
+    exp = 'fencing_topology st1'
+    eq_(exp, data)
+    assert obj.cli_use_validate()
+
+
+def test_master():
+    xml = """<master id="ms-1">
+    <crmsh-ref id="dummy3" />
+    </master>
+    """
+    data = etree.fromstring(xml)
+    factory.create_from_cli("primitive dummy3 ocf:pacemaker:Dummy")
+    data, _, _ = cibconfig.postprocess_cli(data)
+    print "after postprocess:", etree.tostring(data)
+    obj = factory.create_from_node(data)
+    assert_is_not_none(obj)
+    assert obj.cli_use_validate()
+
+
+def test_param_rules():
+    roundtrip('primitive foo Dummy ' +
+              'params rule #uname eq wizbang laser=yes ' +
+              'params rule #uname eq gandalf staff=yes')
+
+    roundtrip('primitive mySpecialRsc me:Special ' +
+              'params 3: rule #uname eq node1 interface=eth1 ' +
+              'params 2: rule #uname eq node2 interface=eth2 port=8888 ' +
+              'params 1: interface=eth0 port=9999')
+
+
+def test_new_acls():
+    roundtrip('role fum description=test read description=test2 xpath:"*[@name=karl]"')
+
+
+def test_acls_reftype():
+    roundtrip('role boo deny ref:d0 type:nvpair',
+              expected='role boo deny ref:d0 deny type:nvpair')
+
+
+def test_acls_oldsyntax():
+    roundtrip('role boo deny ref:d0 tag:nvpair',
+              expected='role boo deny ref:d0 deny type:nvpair')
+
+def test_rules():
+    roundtrip('primitive p1 Dummy params ' +
+              'rule $role=Started date in start=2009-05-26 end=2010-05-26 ' +
+              'or date gt 2014-01-01 state=2')
+
+
+def test_new_role():
+    roundtrip('role silly-role-2 read xpath:"//nodes//attributes" ' +
+              'deny type:nvpair deny ref:d0 deny type:nvpair')
+
diff --git a/test/unittests/test_corosync.py b/test/unittests/test_corosync.py
new file mode 100644
index 0000000..af2bb16
--- /dev/null
+++ b/test/unittests/test_corosync.py
@@ -0,0 +1,134 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+# unit tests for parse.py
+
+import unittest
+import corosync
+from corosync import Parser, make_section, make_value
+
+
+F1 = open('corosync.conf.1').read()
+F2 = open('corosync.conf.2').read()
+F3 = open('bug-862577_corosync.conf').read()
+
+
+def _valid(parser):
+    depth = 0
+    for t in parser._tokens:
+        if t.token not in (corosync._tCOMMENT,
+                           corosync._tBEGIN,
+                           corosync._tEND,
+                           corosync._tVALUE):
+            raise AssertionError("illegal token " + str(t))
+        if t.token == corosync._tBEGIN:
+            depth += 1
+        if t.token == corosync._tEND:
+            depth -= 1
+    if depth != 0:
+        raise AssertionError("Unbalanced sections")
+
+
+def _print(parser):
+    print parser.to_string()
+
+
+class TestCorosyncParser(unittest.TestCase):
+    def test_parse(self):
+        p = Parser(F1)
+        _valid(p)
+        self.assertEqual(p.get('logging.logfile'), '/var/log/cluster/corosync.log')
+        self.assertEqual(p.get('totem.interface.ttl'), '1')
+        p.set('totem.interface.ttl', '2')
+        _valid(p)
+        self.assertEqual(p.get('totem.interface.ttl'), '2')
+        p.remove('quorum')
+        _valid(p)
+        self.assertEqual(p.count('quorum'), 0)
+        p.add('', make_section('quorum', []))
+        _valid(p)
+        self.assertEqual(p.count('quorum'), 1)
+        p.set('quorum.votequorum', '2')
+        _valid(p)
+        self.assertEqual(p.get('quorum.votequorum'), '2')
+        p.set('bananas', '5')
+        _valid(p)
+        self.assertEqual(p.get('bananas'), '5')
+
+    def test_logfile(self):
+        self.assertEqual(corosync.logfile(F1), '/var/log/cluster/corosync.log')
+        self.assertEqual(corosync.logfile('# nothing\n'), None)
+
+    def test_udpu(self):
+        p = Parser(F2)
+        _valid(p)
+        self.assertEqual(p.count('nodelist.node'), 5)
+        p.add('nodelist',
+              make_section('nodelist.node',
+                           make_value('nodelist.node.ring0_addr', '10.10.10.10') +
+                           make_value('nodelist.node.nodeid', str(corosync.next_nodeid(p)))))
+        _valid(p)
+        self.assertEqual(p.count('nodelist.node'), 6)
+        self.assertEqual(p.get_all('nodelist.node.nodeid'),
+                         ['1', '2', '3'])
+
+    def test_add_node_no_nodelist(self):
+        "test checks that if there is no nodelist, no node is added"
+        from corosync import make_section, make_value, next_nodeid
+
+        p = Parser(F1)
+        _valid(p)
+        nid = next_nodeid(p)
+        self.assertEqual(p.count('nodelist.node'), nid - 1)
+        p.add('nodelist',
+              make_section('nodelist.node',
+                           make_value('nodelist.node.ring0_addr', 'foo') +
+                           make_value('nodelist.node.nodeid', str(nid))))
+        _valid(p)
+        self.assertEqual(p.count('nodelist.node'), nid - 1)
+
+    def test_add_node_nodelist(self):
+        from corosync import make_section, make_value, next_nodeid
+
+        p = Parser(F2)
+        _valid(p)
+        nid = next_nodeid(p)
+        c = p.count('nodelist.node')
+        p.add('nodelist',
+              make_section('nodelist.node',
+                           make_value('nodelist.node.ring0_addr', 'foo') +
+                           make_value('nodelist.node.nodeid', str(nid))))
+        _valid(p)
+        self.assertEqual(p.count('nodelist.node'), c + 1)
+        self.assertEqual(next_nodeid(p), nid + 1)
+
+    def test_remove_node(self):
+        p = Parser(F2)
+        _valid(p)
+        self.assertEqual(p.count('nodelist.node'), 5)
+        p.remove_section_where('nodelist.node', 'nodeid', '2')
+        _valid(p)
+        self.assertEqual(p.count('nodelist.node'), 4)
+        self.assertEqual(p.get_all('nodelist.node.nodeid'),
+                         ['1'])
+
+    def test_bnc862577(self):
+        p = Parser(F3)
+        _valid(p)
+        self.assertEqual(p.count('service.ver'), 1)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test/unittests/test_objset.py b/test/unittests/test_objset.py
new file mode 100644
index 0000000..4029660
--- /dev/null
+++ b/test/unittests/test_objset.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+
+
+import cibconfig
+from nose.tools import eq_
+
+factory = cibconfig.cib_factory
+
+
+def assert_in(needle, haystack):
+    if needle not in haystack:
+        message = "%s not in %s" % (needle, haystack)
+        raise AssertionError(message)
+
+
+def setup_func():
+    "set up test fixtures"
+    import idmgmt
+    idmgmt.clear()
+
+
+def test_nodes_nocli():
+    for n in factory.node_id_list():
+        obj = factory.find_object(n)
+        if obj is not None:
+            assert obj.node is not None
+            eq_(True, obj.cli_use_validate())
+            eq_(False, obj.nocli)
+
+
+def test_show():
+    setobj = cibconfig.mkset_obj()
+    s = setobj.repr_nopretty()
+    sp = s.splitlines()
+    assert_in("node 1: ha-one", sp[0:3])
diff --git a/test/unittests/test_parse.py b/test/unittests/test_parse.py
new file mode 100644
index 0000000..896a5b1
--- /dev/null
+++ b/test/unittests/test_parse.py
@@ -0,0 +1,662 @@
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+# unit tests for parse.py
+
+import parse
+import unittest
+import shlex
+from utils import lines2cli
+from lxml import etree
+from nose.tools import ok_, eq_
+
+
+class MockValidation(parse.Validation):
+    def resource_roles(self):
+        return ['Master', 'Slave', 'Started']
+
+    def resource_actions(self):
+        return ['start', 'stop', 'promote', 'demote']
+
+    def date_ops(self):
+        return ['lt', 'gt', 'in_range', 'date_spec']
+
+    def expression_types(self):
+        return ['normal', 'string', 'number']
+
+    def rsc_order_kinds(self):
+        return ['Mandatory', 'Optional', 'Serialize']
+
+    def op_attributes(self):
+        return ['id', 'name', 'interval', 'timeout', 'description',
+                'start-delay', 'interval-origin', 'timeout', 'enabled',
+                'record-pending', 'role', 'requires', 'on-fail']
+
+    def acl_2_0(self):
+        return True
+
+
+class TestBaseParser(unittest.TestCase):
+    def setUp(self):
+        self.base = parse.BaseParser()
+
+    def _reset(self, cmd):
+        self.base._cmd = shlex.split(cmd)
+        self.base._currtok = 0
+
+    def test_err(self):
+        self._reset('a:b:c:d')
+
+        def runner():
+            self.base.match_split()
+        self.assertRaises(parse.ParseError, runner)
+
+    def test_idspec(self):
+        self._reset('$id=foo')
+        self.base.match_idspec()
+        self.assertEqual(self.base.matched(1), '$id')
+        self.assertEqual(self.base.matched(2), 'foo')
+
+        self._reset('$id-ref=foo')
+        self.base.match_idspec()
+        self.assertEqual(self.base.matched(1), '$id-ref')
+        self.assertEqual(self.base.matched(2), 'foo')
+
+        def runner():
+            self._reset('id=foo')
+            self.base.match_idspec()
+        self.assertRaises(parse.ParseError, runner)
+
+    def test_match_split(self):
+        self._reset('resource:role')
+        a, b = self.base.match_split()
+        self.assertEqual(a, 'resource')
+        self.assertEqual(b, 'role')
+
+        self._reset('role')
+        a, b = self.base.match_split()
+        self.assertEqual(a, 'role')
+        self.assertEqual(b, None)
+
+    def test_description(self):
+        self._reset('description="this is a description"')
+        self.assertEqual(self.base.try_match_description(), 'this is a description')
+
+    def test_nvpairs(self):
+        self._reset('foo=bar wiz="fizz buzz" bug= bug2=')
+        ret = self.base.match_nvpairs()
+        self.assertEqual(len(ret), 4)
+        retdict = {r.get('name'): r.get('value') for r in ret}
+        self.assertEqual(retdict['foo'], 'bar')
+        self.assertEqual(retdict['bug'], '')
+        self.assertEqual(retdict['wiz'], 'fizz buzz')
+
+
+class TestCliParser(unittest.TestCase):
+    def setUp(self):
+        self.parser = parse.CliParser()
+        mockv = MockValidation()
+        for n, p in self.parser.parsers.iteritems():
+            p.validation = mockv
+
+    def test_node(self):
+        out = self.parser.parse('node node-1')
+        self.assertEqual(out.get('uname'), 'node-1')
+
+        out = self.parser.parse('node $id=testid node-1')
+        self.assertEqual(out.get('id'), 'testid')
+        self.assertEqual(out.get('uname'), 'node-1')
+
+        out = self.parser.parse('node 1: node-1')
+        self.assertEqual(out.get('id'), '1')
+        self.assertEqual(out.get('uname'), 'node-1')
+
+        out = self.parser.parse('node testid: node-1')
+        self.assertEqual(out.get('id'), 'testid')
+        self.assertEqual(out.get('uname'), 'node-1')
+
+        out = self.parser.parse('node $id=testid node-1:ping')
+        self.assertEqual(out.get('id'), 'testid')
+        self.assertEqual(out.get('uname'), 'node-1')
+        self.assertEqual(out.get('type'), 'ping')
+
+        out = self.parser.parse('node node-1:unknown')
+        self.assertFalse(out)
+
+        out = self.parser.parse('node node-1 description="foo bar" attributes foo=bar')
+        self.assertEqual(out.get('description'), 'foo bar')
+        self.assertEqual(['bar'], out.xpath('instance_attributes/nvpair[@name="foo"]/@value'))
+
+        out = self.parser.parse('node node-1 attributes foo=bar utilization wiz=bang')
+        self.assertEqual(['bar'], out.xpath('instance_attributes/nvpair[@name="foo"]/@value'))
+        self.assertEqual(['bang'], out.xpath('utilization/nvpair[@name="wiz"]/@value'))
+
+    def test_resources(self):
+        out = self.parser.parse('primitive www ocf:heartbeat:apache op monitor timeout=10s')
+        self.assertEqual(out.get('id'), 'www')
+        self.assertEqual(out.get('class'), 'ocf')
+        self.assertEqual(['monitor'], out.xpath('//op/@name'))
+
+        out = self.parser.parse('rsc_template public_vm ocf:heartbeat:Xen op start timeout=300s op stop timeout=300s op monitor interval=30s timeout=60s op migrate_from timeout=600s op migrate_to timeout=600s')
+        self.assertEqual(out.get('id'), 'public_vm')
+        self.assertEqual(out.get('class'), 'ocf')
+        #print out
+
+        out = self.parser.parse('primitive st stonith:ssh params hostlist=node1 meta target-role=Started op start requires=nothing timeout=60s op monitor interval=60m timeout=60s')
+        self.assertEqual(out.get('id'), 'st')
+
+        out = self.parser.parse('primitive st stonith:ssh params hostlist= meta')
+        self.assertEqual(out.get('id'), 'st')
+
+        out = self.parser.parse('primitive st stonith:null params hostlist=node1 meta description="some description here" op start requires=nothing op monitor interval=60m')
+        self.assertEqual(out.get('id'), 'st')
+
+        out = self.parser.parse('ms m0 resource params a=b')
+        self.assertEqual(out.get('id'), 'm0')
+        print etree.tostring(out)
+        self.assertEqual(['resource'], out.xpath('./crmsh-ref/@id'))
+        self.assertEqual(['b'], out.xpath('instance_attributes/nvpair[@name="a"]/@value'))
+
+        out = self.parser.parse('master ma resource meta a=b')
+        self.assertEqual(out.get('id'), 'ma')
+        self.assertEqual(['resource'], out.xpath('./crmsh-ref/@id'))
+        self.assertEqual(['b'], out.xpath('meta_attributes/nvpair[@name="a"]/@value'))
+
+        out = self.parser.parse('clone clone-1 resource meta a=b')
+        self.assertEqual(out.get('id'), 'clone-1')
+        self.assertEqual(['resource'], out.xpath('./crmsh-ref/@id'))
+        self.assertEqual(['b'], out.xpath('meta_attributes/nvpair[@name="a"]/@value'))
+
+        out = self.parser.parse('group group-1 a')
+        self.assertEqual(out.get('id'), 'group-1')
+        self.assertEqual(len(out), 1)
+
+        out = self.parser.parse('group group-1 a b c')
+        self.assertEqual(len(out), 3)
+
+        out = self.parser.parse('group group-1')
+        self.assertFalse(out)
+
+        out = self.parser.parse('group group-1 params a=b')
+        self.assertEqual(len(out), 1)
+        self.assertEqual(['b'], out.xpath('/group/instance_attributes/nvpair[@name="a"]/@value'))
+
+    def test_heartbeat_class(self):
+        out = self.parser.parse('primitive p_node-activate heartbeat:node-activate')
+        self.assertEqual(out.get('id'), 'p_node-activate')
+        self.assertEqual(out.get('class'), 'heartbeat')
+        self.assertEqual(out.get('provider'), None)
+        self.assertEqual(out.get('type'), 'node-activate')
+
+
+    def test_nvpair_ref(self):
+        out = self.parser.parse('primitive dummy-0 Dummy params @foo')
+        self.assertEqual(out.get('id'), 'dummy-0')
+        self.assertEqual(out.get('class'), 'ocf')
+        self.assertEqual(['foo'], out.xpath('.//nvpair/@id-ref'))
+
+        out = self.parser.parse('primitive dummy-0 Dummy params @fiz:buz')
+        self.assertEqual(out.get('id'), 'dummy-0')
+        self.assertEqual(out.get('class'), 'ocf')
+        self.assertEqual(['fiz'], out.xpath('.//nvpair/@id-ref'))
+        self.assertEqual(['buz'], out.xpath('.//nvpair/@name'))
+
+    def test_location(self):
+        out = self.parser.parse('location loc-1 resource inf: foo')
+        self.assertEqual(out.get('id'), 'loc-1')
+        self.assertEqual(out.get('rsc'), 'resource')
+        self.assertEqual(out.get('score'), 'INFINITY')
+        self.assertEqual(out.get('node'), 'foo')
+
+        out = self.parser.parse('location loc-1 /foo.*/ inf: bar')
+        self.assertEqual(out.get('id'), 'loc-1')
+        self.assertEqual(out.get('rsc-pattern'), 'foo.*')
+        self.assertEqual(out.get('score'), 'INFINITY')
+        self.assertEqual(out.get('node'), 'bar')
+        #print out
+
+        out = self.parser.parse('location loc-1 // inf: bar')
+        self.assertFalse(out)
+
+        out = self.parser.parse('location loc-1 { one ( two three ) four } inf: bar')
+        self.assertEqual(out.get('id'), 'loc-1')
+        self.assertEqual(['one', 'two', 'three', 'four'], out.xpath('//resource_ref/@id'))
+        self.assertEqual(out.get('score'), 'INFINITY')
+        self.assertEqual(out.get('node'), 'bar')
+        #print out
+
+        out = self.parser.parse('location loc-1 thing rule role=slave -inf: #uname eq madrid')
+        self.assertEqual(out.get('id'), 'loc-1')
+        self.assertEqual(out.get('rsc'), 'thing')
+        self.assertEqual(out.get('score'), None)
+
+        out = self.parser.parse('location l { a:foo b:bar }')
+        self.assertFalse(out)
+
+    def test_colocation(self):
+        out = self.parser.parse('colocation col-1 inf: foo:master ( bar wiz sequential=yes )')
+        self.assertEqual(out.get('id'), 'col-1')
+        self.assertEqual(['foo', 'bar', 'wiz'], out.xpath('//resource_ref/@id'))
+        self.assertEqual([], out.xpath('//resource_set[@name="sequential"]/@value'))
+
+        out = self.parser.parse(
+            'colocation col-1 -20: foo:Master ( bar wiz ) ( zip zoo ) node-attribute="fiz"')
+        self.assertEqual(out.get('id'), 'col-1')
+        self.assertEqual(out.get('score'), '-20')
+        self.assertEqual(['foo', 'bar', 'wiz', 'zip', 'zoo'], out.xpath('//resource_ref/@id'))
+        self.assertEqual(['fiz'], out.xpath('//@node-attribute'))
+
+        out = self.parser.parse('colocation col-1 0: a:master b')
+        self.assertEqual(out.get('id'), 'col-1')
+
+        out = self.parser.parse('colocation col-1 10: ) bar wiz')
+        self.assertFalse(out)
+
+        out = self.parser.parse('colocation col-1 10: ( bar wiz')
+        self.assertFalse(out)
+
+        out = self.parser.parse('colocation col-1 10: ( bar wiz ]')
+        self.assertFalse(out)
+
+    def test_order(self):
+        out = self.parser.parse('order o1 Mandatory: [ A B sequential=true ] C')
+        print etree.tostring(out)
+        self.assertEqual(['Mandatory'], out.xpath('/rsc_order/@kind'))
+        self.assertEqual(2, len(out.xpath('/rsc_order/resource_set')))
+        self.assertEqual(['false'], out.xpath('/rsc_order/resource_set/@require-all'))
+        self.assertEqual(['A', 'B', 'C'], out.xpath('//resource_ref/@id'))
+
+        out = self.parser.parse('order o1 Mandatory: [ A B sequential=false ] C')
+        self.assertEqual(2, len(out.xpath('/rsc_order/resource_set')))
+        #self.assertTrue(['require-all', 'false'] in out.resources[0][1])
+        #self.assertTrue(['sequential', 'false'] in out.resources[0][1])
+        self.assertEqual(out.get('id'), 'o1')
+
+        out = self.parser.parse('order o1 Mandatory: A B C sequential=false')
+        self.assertEqual(1, len(out.xpath('/rsc_order/resource_set')))
+        #self.assertTrue(['sequential', 'false'] in out.resources[0][1])
+        self.assertEqual(out.get('id'), 'o1')
+
+        out = self.parser.parse('order o1 Mandatory: A B C sequential=true')
+        self.assertEqual(1, len(out.xpath('/rsc_order/resource_set')))
+        #self.assertTrue(['sequential', 'true'] not in out.resources[0][1])
+        self.assertEqual(out.get('id'), 'o1')
+
+        out = self.parser.parse('order c_apache_1 Mandatory: apache:start ip_1')
+        self.assertEqual(out.get('id'), 'c_apache_1')
+
+        out = self.parser.parse('order c_apache_2 Mandatory: apache:start ip_1 ip_2 ip_3')
+        self.assertEqual(2, len(out.xpath('/rsc_order/resource_set')))
+        self.assertEqual(out.get('id'), 'c_apache_2')
+
+        out = self.parser.parse('order o1 Serialize: A ( B C )')
+        self.assertEqual(2, len(out.xpath('/rsc_order/resource_set')))
+        self.assertEqual(out.get('id'), 'o1')
+
+        out = self.parser.parse('order o1 Serialize: A ( B C ) symmetrical=false')
+        self.assertEqual(2, len(out.xpath('/rsc_order/resource_set')))
+        self.assertEqual(out.get('id'), 'o1')
+        self.assertEqual(['false'], out.xpath('//@symmetrical'))
+
+        out = self.parser.parse('order o1 Serialize: A ( B C ) symmetrical=true')
+        self.assertEqual(2, len(out.xpath('/rsc_order/resource_set')))
+        self.assertEqual(out.get('id'), 'o1')
+        self.assertEqual(['true'], out.xpath('//@symmetrical'))
+
+        inp = 'colocation rsc_colocation-master INFINITY: [ vip-master vip-rep sequential=true ] [ msPostgresql:Master sequential=true ]'
+        out = self.parser.parse(inp)
+        self.assertEqual(2, len(out.xpath('/rsc_colocation/resource_set')))
+        self.assertEqual(out.get('id'), 'rsc_colocation-master')
+
+        out = self.parser.parse('order order_2 Mandatory: [ A B ] C')
+        self.assertEqual(2, len(out.xpath('/rsc_order/resource_set')))
+        self.assertEqual(out.get('id'), 'order_2')
+        self.assertEqual(['Mandatory'], out.xpath('/rsc_order/@kind'))
+        self.assertEqual(['false'], out.xpath('//resource_set/@sequential'))
+
+        out = self.parser.parse('order order-1 Optional: group1:stop group2:start')
+        self.assertEqual(out.get('id'), 'order-1')
+        self.assertEqual(['Optional'], out.xpath('/rsc_order/@kind'))
+        self.assertEqual(['group1'], out.xpath('/rsc_order/@first'))
+        self.assertEqual(['stop'], out.xpath('/rsc_order/@first-action'))
+        self.assertEqual(['group2'], out.xpath('/rsc_order/@then'))
+        self.assertEqual(['start'], out.xpath('/rsc_order/@then-action'))
+
+    def test_ticket(self):
+        out = self.parser.parse('rsc_ticket ticket-A_public-ip ticket-A: public-ip')
+        self.assertEqual(out.get('id'), 'ticket-A_public-ip')
+
+        out = self.parser.parse('rsc_ticket ticket-A_bigdb ticket-A: bigdb loss-policy=fence')
+        self.assertEqual(out.get('id'), 'ticket-A_bigdb')
+
+        out = self.parser.parse(
+            'rsc_ticket ticket-B_storage ticket-B: drbd-a:Master drbd-b:Master')
+        self.assertEqual(out.get('id'), 'ticket-B_storage')
+
+    def test_op(self):
+        out = self.parser.parse('monitor apache:Master 10s:20s')
+        self.assertEqual(out.get('rsc'), 'apache')
+        self.assertEqual(out.get('role'), 'Master')
+        self.assertEqual(out.get('interval'), '10s')
+        self.assertEqual(out.get('timeout'), '20s')
+
+        out = self.parser.parse('monitor apache 60m')
+        self.assertEqual(out.get('rsc'), 'apache')
+        self.assertEqual(out.get('role'), None)
+        self.assertEqual(out.get('interval'), '60m')
+
+    def test_acl(self):
+        out = self.parser.parse('role user-1 error')
+        self.assertFalse(out)
+        out = self.parser.parse('user user-1 role:user-1')
+        self.assertNotEqual(out, False)
+
+        out = self.parser.parse("role bigdb_admin " +
+                                "write meta:bigdb:target-role " +
+                                "write meta:bigdb:is-managed " +
+                                "write location:bigdb " +
+                                "read ref:bigdb")
+        self.assertEqual(4, len(out))
+
+        # new type of acls
+
+        out = self.parser.parse("acl_target foo a")
+        self.assertEqual('acl_target', out.tag)
+        self.assertEqual('foo', out.get('id'))
+        self.assertEqual(['a'], out.xpath('./role/@id'))
+
+        out = self.parser.parse("acl_target foo a b")
+        self.assertEqual('acl_target', out.tag)
+        self.assertEqual('foo', out.get('id'))
+        self.assertEqual(['a', 'b'], out.xpath('./role/@id'))
+
+        out = self.parser.parse("acl_target foo a b c")
+        self.assertEqual('acl_target', out.tag)
+        self.assertEqual('foo', out.get('id'))
+        self.assertEqual(['a', 'b', 'c'], out.xpath('./role/@id'))
+        out = self.parser.parse("acl_group fee a b c")
+        self.assertEqual('acl_group', out.tag)
+        self.assertEqual('fee', out.get('id'))
+        self.assertEqual(['a', 'b', 'c'], out.xpath('./role/@id'))
+        out = self.parser.parse('role fum description="test" read a: description="test2" xpath:*[@name=\\"karl\\"]')
+        self.assertEqual(['*[@name="karl"]'], out.xpath('/acl_role/acl_permission/@xpath'))
+
+    def test_xml(self):
+        out = self.parser.parse('xml <node uname="foo-1"/>')
+        self.assertEqual('node', out.tag)
+        self.assertEqual('foo-1', out.get('uname'))
+
+    def test_property(self):
+        out = self.parser.parse('property stonith-enabled=true')
+        self.assertEqual(['true'], out.xpath('//nvpair[@name="stonith-enabled"]/@value'))
+
+        # missing score
+        out = self.parser.parse('property rule #uname eq node1 stonith-enabled=no')
+        self.assertEqual(['INFINITY'], out.xpath('//@score'))
+
+        out = self.parser.parse('property rule 10: #uname eq node1 stonith-enabled=no')
+        self.assertEqual(['no'], out.xpath('//nvpair[@name="stonith-enabled"]/@value'))
+        self.assertEqual(['node1'], out.xpath('//expression[@attribute="#uname"]/@value'))
+
+        out = self.parser.parse('property rule +inf: date spec years=2014 stonith-enabled=no')
+        self.assertEqual(['no'], out.xpath('//nvpair[@name="stonith-enabled"]/@value'))
+        self.assertEqual(['2014'], out.xpath('//date_spec/@years'))
+
+        out = self.parser.parse('rsc_defaults failure-timeout=3m')
+        self.assertEqual(['3m'], out.xpath('//nvpair[@name="failure-timeout"]/@value'))
+
+        out = self.parser.parse('rsc_defaults foo: failure-timeout=3m')
+        self.assertEqual('foo', out[0].get('id'))
+        self.assertEqual(['3m'], out.xpath('//nvpair[@name="failure-timeout"]/@value'))
+
+        out = self.parser.parse('rsc_defaults failure-timeout=3m foo:')
+        self.assertEqual(False, out)
+
+    def test_empty_property_sets(self):
+        out = self.parser.parse('rsc_defaults defaults:')
+        self.assertEqual('<rsc_defaults><meta_attributes id="defaults"/></rsc_defaults>',
+                         etree.tostring(out))
+
+        out = self.parser.parse('op_defaults defaults:')
+        self.assertEqual('<op_defaults><meta_attributes id="defaults"/></op_defaults>',
+                         etree.tostring(out))
+
+    def test_fencing(self):
+        # num test nodes are 3
+
+        out = self.parser.parse('fencing_topology poison-pill power')
+        self.assertEqual(6, len(out))
+
+        out = self.parser.parse('fencing_topology node-a: poison-pill power node-b: ipmi serial')
+        self.assertEqual(4, len(out))
+
+        devs = ['stonith-vbox3-1-off', 'stonith-vbox3-2-off',
+                'stonith-vbox3-1-on', 'stonith-vbox3-2-on']
+        out = self.parser.parse('fencing_topology vbox4: %s' % ','.join(devs))
+        self.assertEqual(1, len(out))
+
+    def test_tag(self):
+        out = self.parser.parse('tag tag1: one two three')
+        self.assertEqual(out.get('id'), 'tag1')
+        self.assertEqual(['one', 'two', 'three'], out.xpath('/tag/obj_ref/@id'))
+
+        out = self.parser.parse('tag tag1:')
+        self.assertFalse(out)
+
+        out = self.parser.parse('tag tag1:: foo')
+        self.assertFalse(out)
+
+    def _parse_lines(self, lines):
+        out = []
+        for line in lines2cli(lines):
+            if line is not None:
+                tmp = self.parser.parse(line.strip())
+                self.assertNotEqual(tmp, False)
+                if tmp is not None:
+                    out.append(tmp)
+        return out
+
+    def test_comments(self):
+        outp = self._parse_lines('''
+        # comment
+        node n1
+        ''')
+        self.assertNotEqual(-1, etree.tostring(outp[0]).find('# comment'))
+
+    def test_uppercase(self):
+        outp = self._parse_lines('''
+        PRIMITIVE rsc_dummy ocf:heartbeat:Dummy
+        MONITOR rsc_dummy 30
+        ''')
+        #print outp
+        self.assertEqual('primitive', outp[0].tag)
+        self.assertEqual('op', outp[1].tag)
+
+        outp = self._parse_lines('''
+        PRIMITIVE testfs ocf:heartbeat:Filesystem \
+          PARAMS directory="/mnt" fstype="ocfs2" device="/dev/sda1"
+        CLONE testfs-clone testfs \
+          META ordered="true" interleave="true"
+        ''')
+        #print outp
+        self.assertEqual('primitive', outp[0].tag)
+        self.assertEqual('clone', outp[1].tag)
+
+        out = self.parser.parse('LOCATION loc-1 resource INF: foo')
+        self.assertEqual(out.get('id'), 'loc-1')
+        self.assertEqual(out.get('rsc'), 'resource')
+        self.assertEqual(out.get('score'), 'INFINITY')
+        self.assertEqual(out.get('node'), 'foo')
+
+        out = self.parser.parse('NODE node-1 ATTRIBUTES foo=bar UTILIZATION wiz=bang')
+        self.assertEqual('node-1', out.get('uname'))
+        self.assertEqual(['bar'], out.xpath('/node/instance_attributes/nvpair[@name="foo"]/@value'))
+        self.assertEqual(['bang'], out.xpath('/node/utilization/nvpair[@name="wiz"]/@value'))
+
+        out = self.parser.parse('PRIMITIVE virtual-ip ocf:heartbeat:IPaddr2 PARAMS ip=192.168.122.13 lvs_support=false OP start timeout=20 interval=0 OP stop timeout=20 interval=0 OP monitor interval=10 timeout=20')
+        self.assertEqual(['192.168.122.13'], out.xpath('//instance_attributes/nvpair[@name="ip"]/@value'))
+
+        out = self.parser.parse('GROUP web-server virtual-ip apache META target-role=Started')
+        self.assertEqual(out.get('id'), 'web-server')
+
+    def test_nvpair_novalue(self):
+        inp = """primitive stonith_ipmi-karl stonith:fence_ipmilan \
+        params pcmk_host_list=karl verbose action=reboot \
+        ipaddr=10.43.242.221 login=root passwd=dummy method=onoff \
+        op start interval=0 timeout=60 \
+        op stop interval=0 timeout=60 \
+        op monitor interval=600 timeout=60 \
+        meta target-role=Started"""
+
+        outp = self._parse_lines(inp)
+        eq_(len(outp), 1)
+        eq_('primitive', outp[0].tag)
+        # print etree.tostring(outp[0])
+        verbose = outp[0].xpath('//nvpair[@name="verbose"]')
+        eq_(len(verbose), 1)
+        ok_('value' not in verbose[0].attrib)
+
+
+    def test_configs(self):
+        outp = self._parse_lines('''
+        primitive rsc_dummy ocf:heartbeat:Dummy
+        monitor rsc_dummy 30
+        ''')
+        #print outp
+        self.assertEqual(2, len(outp))
+
+        outp = self._parse_lines('''
+        primitive testfs ocf:heartbeat:Filesystem \
+          params directory="/mnt" fstype="ocfs2" device="/dev/sda1"
+        clone testfs-clone testfs \
+          meta ordered="true" interleave="true"
+        ''')
+        #print outp
+        self.assertEqual(2, len(outp))
+
+        inp = [
+            """node node1 attributes mem=16G""",
+            """node node2 utilization cpu=4""",
+            """primitive st stonith:ssh \
+            params hostlist='node1 node2' \
+            meta target-role="Started" \
+            op start requires=nothing timeout=60s \
+            op monitor interval=60m timeout=60s""",
+            """primitive st2 stonith:ssh \
+            params hostlist='node1 node2'""",
+            """primitive d1 ocf:pacemaker:Dummy \
+            operations $id=d1-ops \
+            op monitor interval=60m \
+            op monitor interval=120m OCF_CHECK_LEVEL=10""",
+            """monitor d1 60s:30s""",
+            """primitive d2 ocf:heartbeat:Delay \
+            params mondelay=60 \
+            op start timeout=60s \
+            op stop timeout=60s""",
+            """monitor d2:Started 60s:30s""",
+            """group g1 d1 d2""",
+            """primitive d3 ocf:pacemaker:Dummy""",
+            """clone c d3 \
+            meta clone-max=1""",
+            """primitive d4 ocf:pacemaker:Dummy""",
+            """ms m d4""",
+            """primitive s5 ocf:pacemaker:Stateful \
+            operations $id-ref=d1-ops""",
+            """primitive s6 ocf:pacemaker:Stateful \
+            operations $id-ref=d1""",
+            """ms m5 s5""",
+            """ms m6 s6""",
+            """location l1 g1 100: node1""",
+            """location l2 c \
+            rule $id=l2-rule1 100: #uname eq node1""",
+            """location l3 m5 \
+            rule inf: #uname eq node1 and pingd gt 0""",
+            """location l4 m5 \
+            rule -inf: not_defined pingd or pingd lte 0""",
+            """location l5 m5 \
+            rule -inf: not_defined pingd or pingd lte 0 \
+            rule inf: #uname eq node1 and pingd gt 0 \
+            rule inf: date lt "2009-05-26" and \
+            date in start="2009-05-26" end="2009-07-26" and \
+            date in start="2009-05-26" years="2009" and \
+            date date_spec years="2009" hours=09-17""",
+            """location l6 m5 \
+            rule $id-ref=l2-rule1""",
+            """location l7 m5 \
+            rule $id-ref=l2""",
+            """collocation c1 inf: m6 m5""",
+            """collocation c2 inf: m5:Master d1:Started""",
+            """order o1 Mandatory: m5 m6""",
+            """order o2 Optional: d1:start m5:promote""",
+            """order o3 Serialize: m5 m6""",
+            """order o4 inf: m5 m6""",
+            """rsc_ticket ticket-A_m6 ticket-A: m6""",
+            """rsc_ticket ticket-B_m6_m5 ticket-B: m6 m5 loss-policy=fence""",
+            """rsc_ticket ticket-C_master ticket-C: m6 m5:Master loss-policy=fence""",
+            """fencing_topology st st2""",
+            """property stonith-enabled=true""",
+            """property $id=cpset2 maintenance-mode=true""",
+            """rsc_defaults failure-timeout=10m""",
+            """op_defaults $id=opsdef2 record-pending=true"""]
+
+        outp = self._parse_lines('\n'.join(inp))
+        a = [etree.tostring(x) for x in outp]
+        b = [
+            '<node uname="node1"><instance_attributes><nvpair name="mem" value="16G"/></instance_attributes></node>',
+            '<node uname="node2"><utilization><nvpair name="cpu" value="4"/></utilization></node>',
+            '<primitive id="st" class="stonith" type="ssh"><instance_attributes><nvpair name="hostlist" value="node1 node2"/></instance_attributes><meta_attributes><nvpair name="target-role" value="Started"/></meta_attributes><operations><op name="start" requires="nothing" timeout="60s" interval="0"/><op name="monitor" interval="60m" timeout="60s"/></operations></primitive>',
+            '<primitive id="st2" class="stonith" type="ssh"><instance_attributes><nvpair name="hostlist" value="node1 node2"/></instance_attributes></primitive>',
+            '<primitive id="d1" class="ocf" provider="pacemaker" type="Dummy"><operations id="d1-ops"><op name="monitor" interval="60m"/><op name="monitor" interval="120m"><instance_attributes><nvpair name="OCF_CHECK_LEVEL" value="10"/></instance_attributes></op></operations></primitive>',
+            '<op name="monitor" rsc="d1" interval="60s" timeout="30s"/>',
+            '<primitive id="d2" class="ocf" provider="heartbeat" type="Delay"><instance_attributes><nvpair name="mondelay" value="60"/></instance_attributes><operations><op name="start" timeout="60s" interval="0"/><op name="stop" timeout="60s" interval="0"/></operations></primitive>',
+            '<op name="monitor" role="Started" rsc="d2" interval="60s" timeout="30s"/>',
+            '<group id="g1"><crmsh-ref id="d1"/><crmsh-ref id="d2"/></group>',
+            '<primitive id="d3" class="ocf" provider="pacemaker" type="Dummy"/>',
+            '<clone id="c"><meta_attributes><nvpair name="clone-max" value="1"/></meta_attributes><crmsh-ref id="d3"/></clone>',
+            '<primitive id="d4" class="ocf" provider="pacemaker" type="Dummy"/>',
+            '<master id="m"><crmsh-ref id="d4"/></master>',
+            '<primitive id="s5" class="ocf" provider="pacemaker" type="Stateful"><operations id-ref="d1-ops"/></primitive>',
+            '<primitive id="s6" class="ocf" provider="pacemaker" type="Stateful"><operations id-ref="d1"/></primitive>',
+            '<master id="m5"><crmsh-ref id="s5"/></master>',
+            '<master id="m6"><crmsh-ref id="s6"/></master>',
+            '<rsc_location id="l1" rsc="g1" score="100" node="node1"/>',
+            '<rsc_location id="l2" rsc="c"><rule id="l2-rule1" score="100"><expression attribute="#uname" operation="eq" value="node1"/></rule></rsc_location>',
+            '<rsc_location id="l3" rsc="m5"><rule score="INFINITY"><expression attribute="#uname" operation="eq" value="node1"/><expression attribute="pingd" operation="gt" value="0"/></rule></rsc_location>',
+            '<rsc_location id="l4" rsc="m5"><rule score="-INFINITY" boolean-op="or"><expression attribute="pingd" operation="not_defined"/><expression attribute="pingd" operation="lte" value="0"/></rule></rsc_location>',
+            '<rsc_location id="l5" rsc="m5"><rule score="-INFINITY" boolean-op="or"><expression attribute="pingd" operation="not_defined"/><expression attribute="pingd" operation="lte" value="0"/></rule><rule score="INFINITY"><expression attribute="#uname" operation="eq" value="node1"/><expression attribute="pingd" operation="gt" value="0"/></rule><rule score="INFINITY"><date_expression operation="lt" end="2009-05-26"/><date_expression operation="in_range" start="2009-05-26" end="2009-07 [...]
+            '<rsc_location id="l6" rsc="m5"><rule id-ref="l2-rule1"/></rsc_location>',
+            '<rsc_location id="l7" rsc="m5"><rule id-ref="l2"/></rsc_location>',
+            '<rsc_colocation id="c1" score="INFINITY" rsc="m6" with-rsc="m5"/>',
+            '<rsc_colocation id="c2" score="INFINITY" rsc="m5" rsc-role="Master" with-rsc="d1" with-rsc-role="Started"/>',
+            '<rsc_order id="o1" kind="Mandatory" first="m5" then="m6"/>',
+            '<rsc_order id="o2" kind="Optional" first="d1" first-action="start" then="m5" then-action="promote"/>',
+            '<rsc_order id="o3" kind="Serialize" first="m5" then="m6"/>',
+            '<rsc_order id="o4" score="INFINITY" first="m5" then="m6"/>',
+            '<rsc_ticket id="ticket-A_m6" ticket="ticket-A" rsc="m6"/>',
+            '<rsc_ticket id="ticket-B_m6_m5" ticket="ticket-B" loss-policy="fence"><resource_set><resource_ref id="m6"/><resource_ref id="m5"/></resource_set></rsc_ticket>',
+            '<rsc_ticket id="ticket-C_master" ticket="ticket-C" loss-policy="fence"><resource_set><resource_ref id="m6"/></resource_set><resource_set role="Master"><resource_ref id="m5"/></resource_set></rsc_ticket>',
+            '<fencing-topology><fencing-level devices="st" index="1" target="ha-one"/><fencing-level devices="st2" index="2" target="ha-one"/><fencing-level devices="st" index="1" target="ha-two"/><fencing-level devices="st2" index="2" target="ha-two"/><fencing-level devices="st" index="1" target="ha-three"/><fencing-level devices="st2" index="2" target="ha-three"/></fencing-topology>',
+            '<cluster_property_set><nvpair name="stonith-enabled" value="true"/></cluster_property_set>',
+            '<cluster_property_set id="cpset2"><nvpair name="maintenance-mode" value="true"/></cluster_property_set>',
+            '<rsc_defaults><meta_attributes><nvpair name="failure-timeout" value="10m"/></meta_attributes></rsc_defaults>',
+            '<op_defaults><meta_attributes id="opsdef2"><nvpair name="record-pending" value="true"/></meta_attributes></op_defaults>',
+            ]
+
+        for result, expected in zip(a, b):
+            self.assertEqual(expected, result)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/test/unittests/test_resource.py b/test/unittests/test_resource.py
new file mode 100644
index 0000000..035bd5c
--- /dev/null
+++ b/test/unittests/test_resource.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+
+import ui_resource
+import utils
+
+
+def test_maintenance():
+    errors = []
+    commands = []
+
+    def mockcmd(*args):
+        commands.append(args)
+        return 0
+
+    class MockContext(object):
+        def fatal_error(*args):
+            errors.append(args)
+    mc = MockContext()
+
+    _pre_ext_cmd = utils.ext_cmd
+    try:
+        utils.ext_cmd = mockcmd
+        rscui = ui_resource.RscMgmt()
+        assert rscui.do_maintenance(mc, 'rsc1') is True
+        assert commands[-1] == ("crm_resource -r 'rsc1' --meta -p maintenance -v 'true'",)
+        assert rscui.do_maintenance(mc, 'rsc1', 'on') is True
+        assert commands[-1] == ("crm_resource -r 'rsc1' --meta -p maintenance -v 'true'",)
+        assert rscui.do_maintenance(mc, 'rsc1', 'off') is True
+        assert commands[-1] == ("crm_resource -r 'rsc1' --meta -p maintenance -v 'false'",)
+    finally:
+        utils.ext_cmd = _pre_ext_cmd
diff --git a/test/unittests/test_utils.py b/test/unittests/test_utils.py
new file mode 100644
index 0000000..3634658
--- /dev/null
+++ b/test/unittests/test_utils.py
@@ -0,0 +1,137 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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 software 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 library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+# unit tests for utils.py
+
+import os
+from itertools import chain
+import utils
+import config
+
+
+def test_systeminfo():
+    assert utils.getuser() is not None
+    assert utils.gethomedir() is not None
+    assert utils.get_tempdir() is not None
+
+
+def test_shadowcib():
+    assert utils.get_cib_in_use() == ""
+    utils.set_cib_in_use("foo")
+    assert utils.get_cib_in_use() == "foo"
+    utils.clear_cib_in_use()
+    assert utils.get_cib_in_use() == ""
+
+
+def test_booleans():
+    truthy = ['yes', 'Yes', 'True', 'true', 'TRUE',
+              'YES', 'on', 'On', 'ON']
+    falsy = ['no', 'false', 'off', 'OFF', 'FALSE', 'nO']
+    not_truthy = ['', 'not', 'ONN', 'TRUETH', 'yess']
+    for case in chain(truthy, falsy):
+        assert utils.verify_boolean(case) is True
+    for case in truthy:
+        assert utils.is_boolean_true(case) is True
+        assert utils.is_boolean_false(case) is False
+        assert utils.get_boolean(case) is True
+    for case in falsy:
+        assert utils.is_boolean_true(case) is False
+        assert utils.is_boolean_false(case) is True
+        assert utils.get_boolean(case, dflt=True) is False
+    for case in not_truthy:
+        assert utils.verify_boolean(case) is False
+        assert utils.is_boolean_true(case) is False
+        assert utils.is_boolean_false(case) is False
+        assert utils.get_boolean(case) is False
+
+
+def test_olist():
+    lst = utils.olist(['B', 'C', 'A'])
+    lst.append('f')
+    lst.append('aA')
+    lst.append('_')
+    assert 'aa' in lst
+    assert 'a' in lst
+    assert list(lst) == ['b', 'c', 'a', 'f', 'aa', '_']
+
+
+def test_add_sudo():
+    tmpuser = config.core.user
+    try:
+        config.core.user = 'root'
+        assert utils.add_sudo('ls').startswith('sudo')
+        config.core.user = ''
+        assert utils.add_sudo('ls') == 'ls'
+    finally:
+        config.core.user = tmpuser
+
+
+def test_str2tmp():
+    txt = "This is a test string"
+    filename = utils.str2tmp(txt)
+    assert os.path.isfile(filename)
+    assert open(filename).read() == txt + "\n"
+    assert utils.file2str(filename) == txt
+    # TODO: should this really return
+    # an empty line at the end?
+    assert utils.file2list(filename) == [txt, '']
+    os.unlink(filename)
+
+
+def test_sanity():
+    sane_paths = ['foo/bar', 'foo', '/foo/bar', 'foo0',
+                  'foo_bar', 'foo-bar', '0foo', '.foo',
+                  'foo.bar']
+    insane_paths = ['#foo', 'foo?', 'foo*', 'foo$', 'foo[bar]',
+                    'foo`', "foo'", 'foo/*']
+    for p in sane_paths:
+        assert utils.is_path_sane(p)
+    for p in insane_paths:
+        assert not utils.is_path_sane(p)
+    sane_filenames = ['foo', '0foo', '0', '.foo']
+    insane_filenames = ['foo/bar']
+    for p in sane_filenames:
+        assert utils.is_filename_sane(p)
+    for p in insane_filenames:
+        assert not utils.is_filename_sane(p)
+    sane_names = ['foo']
+    insane_names = ["f'o"]
+    for n in sane_names:
+        assert utils.is_name_sane(n)
+        assert utils.is_value_sane(n)
+    for n in insane_names:
+        assert not utils.is_name_sane(n)
+        assert not utils.is_value_sane(n)
+
+
+def test_nvpairs2dict():
+    assert utils.nvpairs2dict(['a=b', 'c=d']) == {'a': 'b', 'c': 'd'}
+    assert utils.nvpairs2dict(['a=b=c', 'c=d']) == {'a': 'b=c', 'c': 'd'}
+    assert utils.nvpairs2dict(['a']) == {'a': None}
+
+
+def test_validity():
+    assert utils.is_id_valid('foo0')
+    assert not utils.is_id_valid('0foo')
+
+
+def test_msec():
+    assert utils.crm_msec('1ms') == 1
+    assert utils.crm_msec('1s') == 1000
+    assert utils.crm_msec('1us') == 0
+    assert utils.crm_msec('1') == 1000
+    assert utils.crm_msec('1m') == 60*1000
+    assert utils.crm_msec('1h') == 60*60*1000
diff --git a/utils/Makefile.am b/utils/Makefile.am
new file mode 100644
index 0000000..a8af219
--- /dev/null
+++ b/utils/Makefile.am
@@ -0,0 +1,27 @@
+#
+# crmsh util scripts
+#
+# Copyright (C) 2014 Kristoffer Gronlund
+#
+# 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.
+#
+MAINTAINERCLEANFILES    = Makefile.in
+
+utilsdir 		= $(datadir)/@PACKAGE@/utils
+
+utils_DATA	= crm_script.py crm_init.py
+utils_SCRIPTS	= crm_clean.py crm_rpmcheck.py crm_pkg.py
+
+EXTRA_DIST	= $(utils_DATA) $(utils_SCRIPTS)
diff --git a/utils/crm_clean.py b/utils/crm_clean.py
new file mode 100755
index 0000000..506e3e7
--- /dev/null
+++ b/utils/crm_clean.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+import os
+import sys
+import shutil
+errors = []
+mydir = os.path.dirname(os.path.abspath(sys.modules[__name__].__file__))
+def bad(path):
+    return ((not os.path.isabs(path)) or os.path.dirname(path) == '/' or
+            path.startswith('/var') or path.startswith('/usr') or
+            (not path.startswith(mydir)))
+for f in sys.argv[1:]:
+    if bad(f):
+        errors.append("cannot remove %s from %s" % (f, mydir))
+        continue
+    try:
+        if os.path.isfile(f):
+            os.remove(f)
+        elif os.path.isdir(f):
+            if os.path.isfile(os.path.join(f, 'crm_script.debug')):
+                print open(os.path.join(f, 'crm_script.debug')).read()
+            shutil.rmtree(f)
+    except OSError, e:
+        errors.append(e)
+if errors:
+    print >>sys.stderr, '\n'.join(errors)
+    sys.exit(1)
diff --git a/utils/crm_init.py b/utils/crm_init.py
new file mode 100644
index 0000000..50b85b8
--- /dev/null
+++ b/utils/crm_init.py
@@ -0,0 +1,263 @@
+import os
+import re
+import platform
+import socket
+import crm_script
+
+PACKAGES = ['booth', 'cluster-glue', 'corosync', 'crmsh', 'csync2', 'drbd',
+            'fence-agents', 'gfs2', 'gfs2-utils', 'hawk', 'ocfs2',
+            'ocfs2-tools', 'pacemaker', 'pacemaker-mgmt',
+            'resource-agents', 'sbd']
+SERVICES = ['sshd', 'ntp', 'corosync', 'pacemaker', 'hawk', 'SuSEfirewall2_init']
+SSH_KEY = os.path.expanduser('~/.ssh/id_rsa')
+CSYNC2_KEY = '/etc/csync2/key_hagroup'
+CSYNC2_CFG = '/etc/csync2/csync2.cfg'
+COROSYNC_CONF = '/etc/corosync/corosync.conf'
+SYSCONFIG_SBD = '/etc/sysconfig/sbd'
+SYSCONFIG_FW = '/etc/sysconfig/SuSEfirewall2'
+SYSCONFIG_FW_CLUSTER = '/etc/sysconfig/SuSEfirewall2.d/services/cluster'
+
+
+def rpm_info():
+    'check installed packages'
+    return crm_script.rpmcheck(PACKAGES)
+
+
+def service_info(service):
+    "Returns information about a given service"
+    active, enabled = 'unknown', 'unknown'
+    rc, out, err = crm_script.call(["/usr/bin/systemctl", "is-enabled", "%s.service" % (service)])
+    if rc in (0, 1, 3) and out:
+        enabled = out.strip()
+    else:
+        return {'name': service, 'error': err.strip()}
+    rc, out, err = crm_script.call(["/usr/bin/systemctl", "is-active", "%s.service" % (service)])
+    if rc in (0, 1, 3) and out:
+        active = out.strip()
+    else:
+        return {'name': service, 'error': err.strip()}
+    return {'name': service, 'active': active, 'enabled': enabled}
+
+
+def services_info():
+    'check enabled/active services'
+    return [service_info(service) for service in SERVICES]
+
+
+def sys_info():
+    'system information'
+    system, node, release, version, machine, processor = platform.uname()
+    distname, distver, distid = platform.linux_distribution()
+    hostname = platform.node().split('.')[0]
+    return {'system': system,
+            'node': node,
+            'release': release,
+            'version': version,
+            'machine': machine,
+            'processor': processor,
+            'distname': distname,
+            'distver': distver,
+            'distid': distid,
+            'user': os.getlogin(),
+            'hostname': hostname,
+            'fqdn': socket.getfqdn()}
+
+
+def net_info():
+    ret = {}
+    interfaces = []
+    rc, out, err = crm_script.call(['netstat', '-nr'])
+    if rc == 0:
+        data = [l.split() for l in out.split('\n')]
+        if len(data) < 3:
+            return {'error': "Failed to parse netstat output"}
+        keys = data[1]
+        for line in data[2:]:
+            if len(line) == len(keys):
+                interfaces.append(dict(zip(keys, line)))
+    else:
+        interfaces.append({'error': err.strip()})
+    ret['interfaces'] = interfaces
+    hostname = platform.node().split('.')[0]
+    try:
+        ip = socket.gethostbyname(hostname)
+        ret['hostname'] = {'name': hostname, 'ip': ip}
+    except Exception, e:
+        ret['hostname'] = {'error': str(e)}
+    return ret
+
+
+def files_info():
+    def check(fn):
+        if os.path.isfile(os.path.expanduser(fn)):
+            return os.path.expanduser(fn)
+        return ''
+    return {'ssh_key': check(SSH_KEY),
+            'csync2_key': check(CSYNC2_KEY),
+            'csync2_cfg': check(CSYNC2_CFG),
+            'corosync_conf': check(COROSYNC_CONF),
+            'sysconfig_sbd': check(SYSCONFIG_SBD),
+            'sysconfig_fw': check(SYSCONFIG_FW),
+            'sysconfig_fw_cluster': check(SYSCONFIG_FW_CLUSTER),
+            }
+
+
+def logrotate_info():
+    rc, _, _ = crm_script.call(
+        'grep -r corosync.conf /etc/logrotate.d',
+        shell=True)
+    return {'corosync.conf': rc == 0}
+
+
+def disk_info():
+    rc, out, err = crm_script.call(['df'], shell=False)
+    if rc == 0:
+        disk_use = []
+        for line in out.split('\n')[1:]:
+            line = line.strip()
+            if line:
+                data = line.split()
+                if len(data) >= 6:
+                    disk_use.append((data[5], int(data[4][:-1])))
+        return disk_use
+    return []
+
+
+def info():
+    return {'rpm': rpm_info(),
+            'services': services_info(),
+            'system': sys_info(),
+            'net': net_info(),
+            'files': files_info(),
+            'logrotate': logrotate_info(),
+            'disk': disk_info()}
+
+
+def verify(data):
+    """
+    Given output from info(), verifies
+    as much as possible before init/add.
+    """
+    def check_diskspace():
+        for host, info in data.iteritems():
+            for mount, percent in info['disk']:
+                interesting = (mount == '/' or
+                               mount.startswith('/var/log') or
+                               mount.startswith('/tmp'))
+                if interesting and percent > 90:
+                    crm_script.exit_fail("Not enough space on %s:%s" % (host, mount))
+
+    def check_services():
+        for host, info in data.iteritems():
+            for svc in info['services']:
+                if svc['name'] == 'pacemaker' and svc['active'] == 'active':
+                    crm_script.exit_fail("%s already running pacemaker" % (host))
+                if svc['name'] == 'corosync' and svc['active'] == 'active':
+                    crm_script.exit_fail("%s already running corosync" % (host))
+
+    def verify_host(host, info):
+        if host != info['system']['hostname']:
+            crm_script.exit_fail("Hostname mismatch: %s is not %s" %
+                                 (host, info['system']['hostname']))
+
+    def compare_system(systems):
+        def check(value, msg):
+            vals = set([system[value] for host, system in systems])
+            if len(vals) > 1:
+                info = ', '.join('%s: %s' % (h, system[value]) for h, system in systems)
+                crm_script.exit_fail("%s: %s" % (msg, info))
+
+        check('machine', 'Architecture differs')
+        #check('release', 'Kernel release differs')
+        check('distname', 'Distribution differs')
+        check('distver', 'Distribution version differs')
+        #check('version', 'Kernel version differs')
+
+    for host, info in data.iteritems():
+        verify_host(host, info)
+
+    compare_system((h, info['system']) for h, info in data.iteritems())
+
+    check_diskspace()
+    check_services()
+
+
+# common functions to initialize a cluster node
+
+
+def is_service_enabled(name):
+    info = service_info(name)
+    if info.get('name') == name and info.get('enabled') == 'enabled':
+        return True
+    return False
+
+
+def is_service_active(name):
+    info = service_info(name)
+    if info.get('name') == name and info.get('active') == 'active':
+        return True
+    return False
+
+
+def install_packages(packages):
+    for pkg in packages:
+        try:
+            crm_script.package(pkg, 'latest')
+        except Exception, e:
+            crm_script.exit_fail("Failed to install %s: %s" % (pkg, e))
+
+
+def configure_firewall():
+    _SUSE_FW_TEMPLATE = """## Name: HAE cluster ports
+## Description: opens ports for HAE cluster services
+TCP="%(tcp)s"
+UDP="%(udp)s"
+"""
+    corosync_mcastport = crm_script.param('mcastport')
+    if not corosync_mcastport:
+        rc, out, err = crm_script.call(['crm', 'corosync', 'get', 'totem.interface.mcastport'])
+        if rc == 0:
+            corosync_mcastport = out.strip()
+    FW = '/etc/sysconfig/SuSEfirewall2'
+    FW_CLUSTER = '/etc/sysconfig/SuSEfirewall2.d/services/cluster'
+
+    tcp_ports = '30865 5560 7630 21064'
+    udp_ports = '%s %s' % (corosync_mcastport, int(corosync_mcastport) - 1)
+
+    if is_service_enabled('SuSEfirewall2'):
+        if os.path.isfile(FW_CLUSTER):
+            tmpl = open(FW_CLUSTER).read()
+            tmpl = re.sub(r'^TCP="(.*)"', 'TCP="%s"' % (tcp_ports), tmpl, flags=re.M)
+            tmpl = re.sub(r'^UDP="(.*)"', 'UDP="%s"' % (udp_ports), tmpl, flags=re.M)
+            with open(FW_CLUSTER, 'w') as f:
+                f.write(tmpl)
+        elif os.path.isdir(os.path.dirname(FW_CLUSTER)):
+            with open(FW_CLUSTER, 'w') as fwc:
+                fwc.write(_SUSE_FW_TEMPLATE % {'tcp': tcp_ports,
+                                               'udp': udp_ports})
+        else:
+            # neither the cluster file nor the services
+            # directory exists
+            crm_script.exit_fail("SUSE firewall is configured but %s does not exist" %
+                                 os.path.dirname(FW_CLUSTER))
+
+        # add cluster to FW_CONFIGURATIONS_EXT
+        if os.path.isfile(FW):
+            txt = open(FW).read()
+            m = re.search(r'^FW_CONFIGURATIONS_EXT="(.*)"', txt, re.M)
+            if m:
+                services = m.group(1).split()
+                if 'cluster' not in services:
+                    services.append('cluster')
+                txt = re.sub(r'^FW_CONFIGURATIONS_EXT="(.*)"',
+                             r'FW_CONFIGURATIONS_EXT="%s"' % (' '.join(services)),
+                             txt,
+                             flags=re.M)
+            else:
+                txt += '\nFW_CONFIGURATIONS_EXT="cluster"'
+            with open(FW, 'w') as fw:
+                fw.write(txt)
+        if is_service_active('SuSEfirewall2'):
+            crm_script.service('SuSEfirewall2', 'restart')
+
+    # TODO: other platforms
diff --git a/utils/crm_pkg.py b/utils/crm_pkg.py
new file mode 100755
index 0000000..2ffffe8
--- /dev/null
+++ b/utils/crm_pkg.py
@@ -0,0 +1,281 @@
+#!/usr/bin/python
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import os
+import sys
+import subprocess
+import json
+
+
+DRY_RUN = False
+
+
+def get_platform():
+    return os.uname()[0]
+
+
+def fail(msg):
+    print >>sys.stderr, msg
+    sys.exit(1)
+
+
+def run(cmd):
+    proc = subprocess.Popen(cmd,
+                            shell=False,
+                            stdin=None,
+                            stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE)
+    out, err = proc.communicate(None)
+    return proc.returncode, out, err
+
+
+def is_program(prog):
+    """Is this program available?"""
+    for p in os.getenv("PATH").split(os.pathsep):
+        filename = os.path.join(p, prog)
+        if os.path.isfile(filename) and os.access(filename, os.X_OK):
+            return filename
+    return None
+
+
+class PackageManager(object):
+    def dispatch(self, name, state):
+        if state in ('installed', 'present'):
+            return self.present(name)
+        elif state in ('absent', 'removed'):
+            return self.absent(name)
+        elif state == 'latest':
+            return self.latest(name)
+        fail(msg="Unknown state: " + state)
+
+    def present(self, name):
+        raise NotImplementedError
+
+    def latest(self, name):
+        raise NotImplementedError
+
+    def absent(self, name):
+        raise NotImplementedError
+
+
+class Zypper(PackageManager):
+    def __init__(self):
+        self._rpm = is_program('rpm')
+        self._zyp = is_program('zypper')
+        if self._rpm is None or self._zyp is None:
+            raise OSError("Missing tools: %s, %s" % (self._rpm, self._zyp))
+
+    def get_version(self, name):
+        cmd = [self._rpm, '-q', name]
+        rc, stdout, stderr = run(cmd)
+        if rc == 0:
+            for line in stdout.splitlines():
+                if name in line:
+                    return line.strip()
+        return None
+
+    def is_installed(self, name):
+        if not isinstance(self._rpm, basestring):
+            raise IOError(str(self._rpm))
+        if not isinstance(name, basestring):
+            raise IOError(str(name))
+        cmd = [self._rpm, '--query', '--info', name]
+        rc, stdout, stderr = run(cmd)
+        return rc == 0
+
+    def present(self, name):
+        if self.is_installed(name):
+            return (0, '', '', False)
+
+        if DRY_RUN:
+            return (0, '', '', True)
+
+        cmd = [self._zyp,
+               '--non-interactive',
+               '--no-refresh',
+               'install',
+               '--auto-agree-with-licenses',
+               name]
+        rc, stdout, stderr = run(cmd)
+        changed = rc == 0
+        return (rc, stdout, stderr, changed)
+
+    def latest(self, name):
+        if not self.is_installed(name):
+            return self.present(name)
+
+        if DRY_RUN:
+            return (0, '', '', True)
+
+        pre_version = self.get_version(name)
+        cmd = [self._zyp,
+               '--non-interactive',
+               '--no-refresh',
+               'update',
+               '--auto-agree-with-licenses',
+               name]
+        rc, stdout, stderr = run(cmd)
+        post_version = self.get_version(name)
+        changed = pre_version != post_version
+        return (rc, stdout, stderr, changed)
+
+    def absent(self, name):
+        if not self.is_installed(name):
+            return (0, '', '', False)
+
+        if DRY_RUN:
+            return (0, '', '', True)
+
+        cmd = [self._zyp,
+               '--non-interactive',
+               'remove',
+               name]
+        rc, stdout, stderr = run(cmd)
+        changed = rc == 0
+        return (rc, stdout, stderr, changed)
+
+
+class Yum(PackageManager):
+    def __init__(self):
+        self._rpm = is_program('rpm')
+        self._yum = is_program('yum')
+
+    def get_version(self, name):
+        cmd = [self._rpm, '-q', name]
+        rc, stdout, stderr = run(cmd)
+        if rc == 0:
+            for line in stdout.splitlines():
+                if name in line:
+                    return line.strip()
+        return None
+
+    def is_installed(self, name):
+        cmd = [self._rpm, '--query', '--info', name]
+        rc, stdout, stderr = run(cmd)
+        return rc == 0
+
+    def present(self, name):
+        if self.is_installed(name):
+            return (0, '', '', False)
+
+        if DRY_RUN:
+            return (0, '', '', True)
+
+        cmd = [self._yum,
+               '--assumeyes',
+               '-d', 2,
+               'install',
+               name]
+        rc, stdout, stderr = run(cmd)
+        changed = rc == 0
+        return (rc, stdout, stderr, changed)
+
+    def latest(self, name):
+        if not self.is_installed(name):
+            return self.present(name)
+
+        if DRY_RUN:
+            return (0, '', '', True)
+
+        pre_version = self.get_version(name)
+        cmd = [self._yum,
+               '--assumeyes',
+               '-d', 2,
+               'update',
+               name]
+        rc, stdout, stderr = run(cmd)
+        post_version = self.get_version(name)
+        changed = pre_version != post_version
+        return (rc, stdout, stderr, changed)
+
+    def absent(self, name):
+        if not self.is_installed(name):
+            return (0, '', '', False)
+
+        if DRY_RUN:
+            return (0, '', '', True)
+
+        cmd = [self._yum,
+               '--assumeyes',
+               '-d', 2,
+               'erase',
+               name]
+        rc, stdout, stderr = run(cmd)
+        changed = rc == 0
+        return (rc, stdout, stderr, changed)
+
+
+class Apt(PackageManager):
+    pass
+
+
+class Pacman(PackageManager):
+    pass
+
+
+def manage_package(pkg, state):
+    """
+    Gathers version and release information about a package.
+    """
+    if pkg is None:
+        raise IOError("PKG IS NONE")
+    pf = get_platform()
+    if pf != 'Linux':
+        fail(msg="Unsupported platform: " + pf)
+    managers = {
+        'zypper': Zypper,
+        'yum': Yum,
+        #'apt-get': Apt,
+        #'pacman': Pacman
+    }
+    for name, mgr in managers.iteritems():
+        exe = is_program(name)
+        if exe:
+            rc, stdout, stderr, changed = mgr().dispatch(pkg, state)
+            return {'rc': rc,
+                    'stdout': stdout,
+                    'stderr': stderr,
+                    'changed': changed
+                    }
+    fail(msg="No supported package manager found")
+
+
+def main():
+    import argparse
+    parser = argparse.ArgumentParser(
+        description="(Semi)-Universal package installer",
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+
+    parser.add_argument('-d', '--dry-run', dest='dry_run', action='store_true',
+                        help="Only check if changes would be made")
+
+    parser.add_argument('-n', '--name', metavar='name', type=str,
+                        help="Name of package")
+
+    parser.add_argument('-s', '--state', metavar='state', type=str,
+                        help="Desired state (present|latest|removed)", default="present")
+
+    args = parser.parse_args()
+    global DRY_RUN
+    DRY_RUN = args.dry_run
+    if not args.name or not args.state:
+        raise IOError("Bad arguments: %s" % (sys.argv))
+    data = manage_package(args.name, args.state)
+    print json.dumps(data)
+
+main()
diff --git a/utils/crm_rpmcheck.py b/utils/crm_rpmcheck.py
new file mode 100755
index 0000000..acf91c5
--- /dev/null
+++ b/utils/crm_rpmcheck.py
@@ -0,0 +1,56 @@
+#!/usr/bin/python
+# Copyright (C) 2013 Kristoffer Gronlund <kgronlund at suse.com>
+#
+# 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.1 of the License, or (at your option) any later version.
+#
+# This software 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 library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+
+import sys
+import json
+import subprocess
+
+
+def run(cmd):
+    proc = subprocess.Popen(cmd,
+                            shell=False,
+                            stdin=None,
+                            stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE)
+    out, err = proc.communicate(None)
+    proc.wait()
+    return proc.returncode, out, err
+
+
+def package_data(pkg):
+    """
+    Gathers version and release information about a package.
+    """
+    _qfmt = 'version: %{VERSION}\nrelease: %{RELEASE}\n'
+    rc, out, err = run(['/bin/rpm', '-q', '--queryformat=' + _qfmt, pkg])
+    if rc == 0:
+        data = {'name': pkg}
+        for line in out.split('\n'):
+            info = line.split(':', 1)
+            if len(info) == 2:
+                data[info[0].strip()] = info[1].strip()
+        return data
+    else:
+        return {'name': pkg, 'error': "package not installed"}
+
+
+def main():
+    data = [package_data(pkg) for pkg in sys.argv[1:]]
+    print json.dumps(data)
+
+main()
diff --git a/utils/crm_script.py b/utils/crm_script.py
new file mode 100644
index 0000000..c980601
--- /dev/null
+++ b/utils/crm_script.py
@@ -0,0 +1,180 @@
+import os
+import sys
+import getpass
+import select
+import subprocess as proc
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+_input = None
+
+# read stdin, if there's anything to read
+_stdin_data = {}
+while sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
+    line = sys.stdin.readline()
+    if line:
+        d = line.split(':', 1)
+        if len(d) == 2:
+            _stdin_data[d[0].strip()] = d[1].strip()
+    else:
+        break
+
+
+def host():
+    return os.uname()[1]
+
+
+def get_input():
+    global _input
+    if _input is None:
+        _input = json.load(open('./script.input'))
+    return _input
+
+
+def parameters():
+    return get_input()[0]
+
+
+def param(name):
+    return parameters().get(name)
+
+
+def output(step_idx):
+    if step_idx < len(get_input()):
+        return get_input()[step_idx]
+    return {}
+
+
+def exit_fail(msg):
+    print >>sys.stderr, msg
+    sys.exit(1)
+
+
+def exit_ok(data):
+    print json.dumps(data)
+    sys.exit(0)
+
+
+def is_true(s):
+    if s in (True, False):
+        return s
+    return isinstance(s, basestring) and s.lower() in ('yes', 'true', '1', 'on')
+
+
+_debug_enabled = None
+
+
+def debug_enabled():
+    global _debug_enabled
+    if _debug_enabled is None:
+        _debug_enabled = is_true(param('debug'))
+    return _debug_enabled
+
+
+def info(msg):
+    "writes msg to log"
+    with open('./crm_script.debug', 'a') as dbglog:
+        dbglog.write('%s' % (msg))
+
+
+def debug(msg):
+    "writes msg to log and syslog if debug is enabled"
+    if debug_enabled():
+        try:
+            with open('./crm_script.debug', 'a') as dbglog:
+                dbglog.write('%s\n' % (msg))
+            import syslog
+            syslog.openlog("crmsh", 0, syslog.LOG_USER)
+            syslog.syslog(syslog.LOG_NOTICE, unicode(msg).encode('utf8'))
+        except:
+            pass
+
+
+def call(cmd, shell=False):
+    debug("crm_script(call): %s" % (cmd))
+    p = proc.Popen(cmd, shell=shell, stdin=None, stdout=proc.PIPE, stderr=proc.PIPE)
+    out, err = p.communicate()
+    return p.returncode, out.strip(), err.strip()
+
+
+def use_sudo():
+    return getpass.getuser() != 'root' and is_true(param('sudo')) and _stdin_data.get('sudo')
+
+
+def sudo_call(cmd, shell=False):
+    if not use_sudo():
+        return call(cmd, shell=shell)
+    debug("crm_script(sudo_call): %s" % (cmd))
+    os.unsetenv('SSH_ASKPASS')
+    call(['sudo', '-k'], shell=False)
+    sudo_prompt = 'crm_script_sudo_prompt'
+    if isinstance(cmd, basestring):
+        cmd = "sudo -H -S -p '%s' %s" % (sudo_prompt, cmd)
+    else:
+        cmd = ['sudo', '-H', '-S', '-p', sudo_prompt] + cmd
+    p = proc.Popen(cmd, shell=shell, stdin=proc.PIPE, stdout=proc.PIPE, stderr=proc.PIPE)
+    sudo_pass = "%s\n" % (_stdin_data.get('sudo', 'linux'))
+    debug("CMD(SUDO): %s" % (str(cmd)))
+    out, err = p.communicate(input=sudo_pass)
+    return p.returncode, out.strip(), err.strip()
+
+
+def service(name, action):
+    if action.startswith('is-'):
+        return call(['/usr/bin/systemctl', action, name + '.service'])
+    return sudo_call(['/usr/bin/systemctl', action, name + '.service'])
+
+
+def package(name, state):
+    rc, out, err = sudo_call(['./crm_pkg.py', '-n', name, '-s', state])
+    if rc != 0:
+        raise IOError("%s / %s" % (out, err))
+    outp = json.loads(out)
+    if isinstance(outp, dict) and 'rc' in outp:
+        rc = int(outp['rc'])
+        if rc != 0:
+            raise IOError("(rc=%s) %s%s" % (rc, outp.get('stdout', ''), outp.get('stderr', '')))
+    return outp
+
+
+def check_package(name, state):
+    rc, out, err = call(['./crm_pkg.py', '--dry-run', '-n', name, '-s', state])
+    if rc != 0:
+        raise IOError(err)
+    outp = json.loads(out)
+    if isinstance(outp, dict) and 'rc' in outp:
+        rc = int(outp['rc'])
+        if rc != 0:
+            raise IOError("(rc=%s) %s%s" % (rc, outp.get('stdout', ''), outp.get('stderr', '')))
+    return outp
+
+
+def rpmcheck(names):
+    rc, out, err = call(['./crm_rpmcheck.py'] + names)
+    if rc != 0:
+        raise IOError(err)
+    return json.loads(out)
+
+
+def save_template(template, dest, **kwargs):
+    '''
+    1. Reads a template from <template>,
+    2. Replaces all template variables with those in <kwargs> and
+    3. writes the resulting file to <dest>
+    '''
+    import re
+    tmpl = open(template).read()
+    keys = re.findall(r'%\((\w+)\)s', tmpl, re.MULTILINE)
+    missing_keys = set(keys) - set(kwargs.keys())
+    if missing_keys:
+        raise ValueError("Missing template arguments: %s" % ', '.join(missing_keys))
+    tmpl = tmpl % kwargs
+    try:
+        with open(dest, 'w') as f:
+            f.write(tmpl)
+    except Exception, e:
+        raise IOError("Failed to write %s from template %s: %s" % (dest, template, e))
+    debug("crm_script(save_template): wrote %s" % (dest))
+
diff --git a/version.in b/version.in
new file mode 100644
index 0000000..a2446cf
--- /dev/null
+++ b/version.in
@@ -0,0 +1,2 @@
+ at VERSION@
+ at BUILD_VERSION@

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-ha/crmsh.git



More information about the Debian-HA-Commits mailing list