[Debian-ha-commits] [crmsh] 01/01: Imported Upstream version 2.2.0

Richard Winters devrik-guest at moszumanska.debian.org
Tue Jan 26 22:23:47 UTC 2016


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

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

commit b51736ab8d6fcbc6ecf22002f8efbbc63defba4e
Author: Richard B Winters <rik at mmogp.com>
Date:   Tue Jan 26 15:46:02 2016 -0500

    Imported Upstream version 2.2.0
---
 .gitignore                                         |    6 +
 .travis.yml                                        |    3 +-
 AUTHORS                                            |    1 +
 ChangeLog                                          |  432 ++-
 Makefile.am                                        |   73 +-
 README                                             |   58 -
 README.dev                                         |  263 --
 README.md                                          |   62 +
 TODO                                               |    2 +
 acinclude.m4                                       |   39 -
 configure.ac                                       |  277 +-
 contrib/Makefile.am                                |   25 -
 contrib/README.vimsyntax                           |   12 +-
 contrib/pygments_crmsh_lexers/__init__.py          |    2 +
 contrib/pygments_crmsh_lexers/ansiclr.py           |   50 +
 contrib/pygments_crmsh_lexers/crmsh.py             |   90 +
 contrib/setup.py                                   |   32 +
 crm                                                |   74 +-
 crm.conf.in                                        |   20 +-
 crmsh-cibadmin_can_patch.patch                     |   23 -
 crmsh.spec                                         |  105 +-
 data-manifest                                      |  172 +
 doc/Makefile.am                                    |   43 -
 doc/{crm.8.txt => crm.8.adoc}                      | 3949 +++++++++++---------
 ...rmsh_hb_report.8.txt => crmsh_hb_report.8.adoc} |    0
 doc/development.md                                 |  225 ++
 doc/sort-doc.py                                    |   82 +
 doc/website-v1/{404.txt => 404.adoc}               |    0
 doc/website-v1/Makefile                            |   94 +-
 doc/website-v1/{about.txt => about.adoc}           |    0
 .../{configuration.txt => configuration.adoc}      |    0
 doc/website-v1/crmold.conf                         |  602 +++
 doc/website-v1/css/crm.css                         |    8 +
 .../{development.txt => development.adoc}          |    2 +-
 .../{documentation.txt => documentation.adoc}      |    9 +-
 doc/website-v1/{faq.txt => faq.adoc}               |    0
 doc/website-v1/history-guide.adoc                  |  275 ++
 doc/website-v1/history-guide.txt                   |    3 -
 .../img/history-guide/sample-cluster.conf.png      |  Bin 0 -> 10009 bytes
 .../img/history-guide/smallapache-start.png        |  Bin 0 -> 1146 bytes
 doc/website-v1/img/icons/README                    |    5 +
 doc/website-v1/img/icons/callouts/1.png            |  Bin 0 -> 329 bytes
 doc/website-v1/img/icons/callouts/10.png           |  Bin 0 -> 361 bytes
 doc/website-v1/img/icons/callouts/11.png           |  Bin 0 -> 565 bytes
 doc/website-v1/img/icons/callouts/12.png           |  Bin 0 -> 617 bytes
 doc/website-v1/img/icons/callouts/13.png           |  Bin 0 -> 623 bytes
 doc/website-v1/img/icons/callouts/14.png           |  Bin 0 -> 411 bytes
 doc/website-v1/img/icons/callouts/15.png           |  Bin 0 -> 640 bytes
 doc/website-v1/img/icons/callouts/2.png            |  Bin 0 -> 353 bytes
 doc/website-v1/img/icons/callouts/3.png            |  Bin 0 -> 350 bytes
 doc/website-v1/img/icons/callouts/4.png            |  Bin 0 -> 345 bytes
 doc/website-v1/img/icons/callouts/5.png            |  Bin 0 -> 348 bytes
 doc/website-v1/img/icons/callouts/6.png            |  Bin 0 -> 355 bytes
 doc/website-v1/img/icons/callouts/7.png            |  Bin 0 -> 344 bytes
 doc/website-v1/img/icons/callouts/8.png            |  Bin 0 -> 357 bytes
 doc/website-v1/img/icons/callouts/9.png            |  Bin 0 -> 357 bytes
 doc/website-v1/img/icons/caution.png               |  Bin 0 -> 2734 bytes
 doc/website-v1/img/icons/example.png               |  Bin 0 -> 2599 bytes
 doc/website-v1/img/icons/home.png                  |  Bin 0 -> 1340 bytes
 doc/website-v1/img/icons/important.png             |  Bin 0 -> 2980 bytes
 doc/website-v1/img/icons/next.png                  |  Bin 0 -> 1302 bytes
 doc/website-v1/img/icons/note.png                  |  Bin 0 -> 2494 bytes
 doc/website-v1/img/icons/prev.png                  |  Bin 0 -> 1348 bytes
 doc/website-v1/img/icons/tip.png                   |  Bin 0 -> 2718 bytes
 doc/website-v1/img/icons/up.png                    |  Bin 0 -> 1320 bytes
 doc/website-v1/img/icons/warning.png               |  Bin 0 -> 3214 bytes
 .../history-guide/basic-transition.typescript      |   22 +
 .../include/history-guide/diff.typescript          |   11 +
 .../include/history-guide/info.typescript          |   16 +
 .../include/history-guide/nfs-probe-err.typescript |   20 +
 .../history-guide/resource-trace.typescript        |    7 +
 .../include/history-guide/resource.typescript      |    6 +
 .../include/history-guide/sample-cluster.conf.crm  |   54 +
 .../history-guide/status-probe-fail.typescript     |   15 +
 .../stonith-corosync-stopped.typescript            |    8 +
 .../history-guide/transition-log.typescript        |   13 +
 doc/website-v1/{index.txt => index.adoc}           |    5 +
 .../{installation.txt => installation.adoc}        |    0
 doc/website-v1/make-news.py                        |    9 +-
 doc/website-v1/{man-1.2.txt => man-1.2.adoc}       |    0
 doc/website-v1/{man-2.0.txt => man-2.0.adoc}       |    0
 doc/website-v1/news.adoc                           |   19 +
 doc/website-v1/news.txt                            |   11 -
 ...release-2_1.txt => 2014-06-30-release-2_1.adoc} |    0
 doc/website-v1/news/2014-10-28-release-2_1_1.adoc  |   58 +
 doc/website-v1/news/2015-01-26-release-2_1_2.adoc  |   69 +
 doc/website-v1/news/2015-04-10-release-2_1_3.adoc  |   68 +
 doc/website-v1/news/2015-05-13-release-2_1_4.adoc  |  126 +
 .../news/2015-05-25-getting-started-jp.adoc        |   17 +
 doc/website-v1/news/2016-01-12-release-2_1_5.adoc  |   56 +
 doc/website-v1/postprocess.py                      |    2 +-
 .../{rsctest-guide.txt => rsctest-guide.adoc}      |    0
 doc/website-v1/scripts.adoc                        |  643 ++++
 doc/website-v1/scripts.txt                         |  445 ---
 .../{start-guide.txt => start-guide.adoc}          |    2 +-
 hb_report/Makefile.am                              |   25 -
 hb_report/ha_cf_support.sh                         |   18 +-
 hb_report/hb_report.in                             |   90 +-
 hb_report/openais_conf_support.sh                  |   18 +-
 hb_report/utillib.sh                               |   24 +-
 modules/Makefile.am                                |   81 -
 modules/cache.py                                   |   16 +-
 modules/cibconfig.py                               |  718 ++--
 modules/cibstatus.py                               |   28 +-
 modules/cibverify.py                               |   25 +-
 modules/clidisplay.py                              |   33 +-
 modules/cliformat.py                               |   34 +-
 modules/cmd_status.py                              |   81 +-
 modules/command.py                                 |   72 +-
 modules/completers.py                              |   18 +-
 modules/config.py                                  |  138 +-
 modules/constants.py                               |  134 +-
 modules/corosync.py                                |  117 +-
 modules/crm_gv.py                                  |   43 +-
 modules/crm_pssh.py                                |   92 +-
 modules/handles.py                                 |  128 +
 modules/help.py                                    |   82 +-
 modules/{report.py => history.py}                  |  599 +--
 modules/idmgmt.py                                  |   24 +-
 modules/log_patterns.py                            |  109 +-
 modules/log_patterns_118.py                        |  105 +-
 modules/main.py                                    |  308 +-
 modules/msg.py                                     |   28 +-
 modules/options.py                                 |   17 +-
 modules/pacemaker.py                               |   44 +-
 modules/parse.py                                   |  236 +-
 modules/ra.py                                      |  258 +-
 modules/rsctest.py                                 |  102 +-
 modules/schema.py                                  |   47 +-
 modules/scripts.py                                 | 2449 +++++++++---
 modules/template.py                                |   25 +-
 modules/term.py                                    |   21 +-
 modules/tmpfiles.py                                |   18 +-
 modules/ui_assist.py                               |   32 +-
 modules/ui_cib.py                                  |   48 +-
 modules/ui_cibstatus.py                            |   28 +-
 modules/ui_cluster.py                              |  110 +-
 modules/ui_configure.py                            |  236 +-
 modules/ui_context.py                              |   42 +-
 modules/ui_corosync.py                             |   28 +-
 modules/ui_history.py                              |  211 +-
 modules/ui_maintenance.py                          |   94 +
 modules/ui_node.py                                 |  109 +-
 modules/ui_options.py                              |   58 +-
 modules/ui_ra.py                                   |   64 +-
 modules/ui_report.py                               |   39 +-
 modules/ui_resource.py                             |  156 +-
 modules/ui_root.py                                 |  164 +-
 modules/ui_script.py                               |  525 ++-
 modules/ui_site.py                                 |   26 +-
 modules/ui_template.py                             |   49 +-
 modules/ui_utils.py                                |   22 +-
 modules/userdir.py                                 |   20 +-
 modules/utils.py                                   |  298 +-
 modules/xmlbuilder.py                              |   19 +-
 modules/xmlutil.py                                 |  191 +-
 requirements.txt                                   |    1 +
 scripts/Makefile.am                                |    2 -
 scripts/add/add.py                                 |   32 +-
 scripts/add/main.yml                               |   71 +-
 scripts/apache/main.yml                            |   68 +
 scripts/check-uptime/Makefile.am                   |   28 -
 scripts/check-uptime/main.yml                      |   30 +-
 scripts/clvm-vg/main.yml                           |   46 +
 scripts/clvm/main.yml                              |   39 +
 scripts/database/main.yml                          |   34 +
 scripts/db2-hadr/main.yml                          |   43 +
 scripts/db2/main.yml                               |   45 +
 scripts/drbd/main.yml                              |   39 +
 scripts/exportfs/main.yml                          |   35 +
 scripts/filesystem/main.yml                        |   30 +
 scripts/gfs2-base/main.yml                         |   27 +
 scripts/gfs2/main.yml                              |   51 +
 scripts/haproxy/haproxy.cfg                        |   13 +
 scripts/haproxy/main.yml                           |   37 +
 scripts/health/Makefile.am                         |   27 -
 scripts/health/collect.py                          |    2 +-
 scripts/health/main.yml                            |   23 +-
 scripts/init/Makefile.am                           |   28 -
 scripts/init/authkey.py                            |   20 +-
 scripts/init/main.yml                              |  108 +-
 scripts/libvirt/main.yml                           |   63 +
 scripts/lvm/main.yml                               |   16 +
 scripts/mailto/main.yml                            |   27 +
 scripts/nfsserver/main.yml                         |   73 +
 scripts/ocfs2/main.yml                             |   56 +
 scripts/oracle/main.yml                            |   51 +
 scripts/raid-lvm/main.yml                          |   25 +
 scripts/raid1/main.yml                             |   17 +
 scripts/remove/Makefile.am                         |   28 -
 scripts/remove/main.yml                            |   38 +-
 scripts/sap-as/main.yml                            |   70 +
 scripts/sap-ci/main.yml                            |   70 +
 scripts/sap-db/main.yml                            |   63 +
 scripts/sap-simple-stack-plus/main.yml             |  220 ++
 scripts/sap-simple-stack/main.yml                  |  183 +
 scripts/sapdb/main.yml                             |   32 +
 scripts/sapinstance/main.yml                       |   48 +
 scripts/sbd/main.yml                               |   44 +
 scripts/virtual-ip/main.yml                        |   24 +
 setup.py                                           |   16 +
 templates/Makefile.am                              |   26 -
 test/Makefile.am                                   |   28 -
 test/bugs-test.txt                                 |   11 +
 test/cib-tests.sh                                  |   17 +-
 test/cibtests/002.exp.xml                          |    6 +-
 test/cibtests/002.input                            |    4 +-
 test/cibtests/003.exp.xml                          |    6 +-
 test/cibtests/003.input                            |    4 +-
 test/cibtests/004.exp.xml                          |    6 +-
 test/cibtests/004.input                            |    4 +-
 test/cibtests/Makefile.am                          |   29 -
 test/crm-interface                                 |   16 +-
 test/descriptions                                  |   16 +-
 test/evaltest.sh                                   |   19 +-
 test/list-undocumented-commands.py                 |    2 +-
 test/regression.sh                                 |   21 +-
 test/run                                           |   13 +
 test/testcases/Makefile.am                         |   36 -
 test/testcases/basicset                            |    2 +
 test/testcases/bugs                                |   42 +
 test/testcases/bugs.exp                            |   98 +
 test/testcases/commit.exp                          |   18 +-
 test/testcases/common.excl                         |    1 +
 test/testcases/confbasic                           |    1 +
 test/testcases/confbasic-xml.exp                   |    2 +-
 test/testcases/confbasic.exp                       |   15 +-
 test/testcases/delete.exp                          |   30 +-
 test/testcases/edit                                |   23 +-
 test/testcases/edit.exp                            |   99 +-
 test/testcases/history.exp                         |   12 +-
 test/testcases/newfeatures                         |    2 +
 test/testcases/newfeatures.exp                     |   11 +-
 test/testcases/node.exp                            |   20 +-
 test/testcases/ra.exp                              |   10 +-
 test/testcases/resource                            |    6 +
 test/testcases/resource.exp                        |  251 +-
 test/testcases/rset-xml.exp                        |    2 +-
 test/testcases/scripts                             |   12 +
 test/testcases/scripts.exp                         |  273 ++
 test/testcases/scripts.filter                      |    4 +
 test/testcases/shadow.exp                          |    8 +-
 test/unit-tests.sh                                 |   13 -
 test/unittests/__init__.py                         |   25 +-
 test/unittests/scripts/inc1/main.yml               |   22 +
 test/unittests/scripts/inc2/main.yml               |   26 +
 .../unittests/scripts/legacy}/main.yml             |    0
 test/unittests/scripts/templates/apache.xml        |   36 +
 test/unittests/scripts/templates/virtual-ip.xml    |   62 +
 test/unittests/scripts/unified/main.yml            |   26 +
 test/unittests/scripts/v2/main.yml                 |   46 +
 test/unittests/scripts/vip/main.yml                |   28 +
 test/unittests/scripts/vipinc/main.yml             |   14 +
 test/unittests/scripts/workflows/10-webserver.xml  |   50 +
 test/unittests/test_bugs.py                        |  538 ++-
 test/unittests/test_cib.py                         |   27 +-
 test/unittests/test_cliformat.py                   |   72 +-
 test/unittests/test_corosync.py                    |   30 +-
 test/unittests/test_gv.py                          |   26 +
 test/unittests/test_handles.py                     |  166 +
 test/unittests/test_objset.py                      |   34 +-
 test/unittests/test_parse.py                       |   52 +-
 test/unittests/test_resource.py                    |   19 +-
 test/unittests/test_scripts.py                     |  826 ++++
 test/unittests/test_time.py                        |   17 +
 test/unittests/test_utils.py                       |   21 +-
 scripts/add/Makefile.am => update-data-manifest.sh |   30 +-
 utils/Makefile.am                                  |   27 -
 utils/crm_init.py                                  |    4 +-
 utils/crm_pkg.py                                   |   22 +-
 utils/crm_rpmcheck.py                              |   16 +-
 version.in                                         |    1 -
 272 files changed, 16040 insertions(+), 7279 deletions(-)

diff --git a/.gitignore b/.gitignore
index 50adcad..b1cdedc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,9 @@ Makefile.in
 autom4te.cache
 patches/*
 
+# Tool specific files
+.README.md.html
+.*.*~
+.project
+.settings
+.pydevproject
diff --git a/.travis.yml b/.travis.yml
index 8a4b651..9186ad1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,10 @@
 ---
+sudo: false
 language: python
 python:
   - "2.6"
   - "2.7"
 install:
   - "pip install -r requirements.txt"
-script: ./test/unit-tests.sh --with-coverage
+script: ./test/run --with-coverage
 
diff --git a/AUTHORS b/AUTHORS
index 4dd3dc8..86fd4ea 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -20,6 +20,7 @@ NOTE:	The work of everyone on this project is dearly appreciated. If you
 	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>
+	Richard B Winters <rik[at]mmogp[dot]com>
 	seabres <rainer[dot]brestan[at]gmx[dot]net>
 	Tim Serong <tserong[at]suse[dot]com>
 	Vincenzo Pii <piiv[at]zhaw[dot]ch>
diff --git a/ChangeLog b/ChangeLog
index 525a121..f48ae06 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,101 +1,413 @@
-* 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
+* Fri Jan 15 2016 Kristoffer Grönlund <kgronlund at suse.com> and many others
+- Release 2.2.0
+- medium: history: Fix live report refresh (bsc#950422) (bsc#927414)
+- medium: history: Ignore central log
+- medium: cibconfig: Detect false container children
+- low: clidisplay: Avoid crash when colorizing None
+- medium: scripts: Load single file yml scripts
+- medium: scripts: Reformat scripts to simplified form
+- medium: ui_history: Add events command (bsc#952449)
+- low: hb_report: Drop function from event patterns
+- high: cibconfig: Preserve failure through edit (bsc#959965)
+- high: cibconfig: fail if new object already exists (bsc#959965)
+- medium: ui_cib: Call crm_shadow in batch mode to avoid spawning subshell (bsc#961392)
+- high: cibconfig: Fix XML import bug for cloned groups (bsc#959895)
+- high: ui_configure: Move validate-all validation to a separate command (bsc#956442)
+- high: scripts: Don't require scripts to be an array of one element
+- medium: scripts: Enable setting category in legacy wizards (bnc#957926)
+- high: scripts: Don't delete steps from upgraded wizards (bnc#957925)
+- high: ra: Only run validate-all if current user is root
+- high: cibconfig: Call validate-all action on agent in verify (bsc#956442)
+- high: script: Fix issues found in cluster scripts
+- high: ui_ra: Add ra validate command (bsc#956442)
+- low: resource: Fix unban alias for unmigrate
+- high: ui_resource: Add constraints and operations commands
+- high: ui_resource: Enable start/stop/status for multiple resources at once (bsc#952775)
+- high: scripts: Conservatively verify scripts that modify the CIB (bsc#951954)
+- high: xmlutil: Order is significant in resource_set (bsc#955434)
+- medium: scripts: Lower copy target to string
+- doc: configure load can read from stdin
+- medium: script: (filesystem) create stopped (bsc#952670)
+- medium: scripts: Check required parameters for optional sub-steps
+- high: scripts: Eval CIB text in correct scope (bsc#952600)
+- medium: utils: Fix python 2.6 compatibility
+- medium: ui_script: Tag legacy wizards as legacy in show (bsc#952226)
+- medium: scripts: No optional steps in legacy wizards (bsc#952226)
+- high: utils: Revised time zone handling (bsc#951759)
+- high: report: Fix syslog parser regexps (bsc#951759)
+- low: constants: Tweaked graph colours
+- high: scripts: Fix DRBD script resource reference (bsc#951028)
+- low: constants: Tweaked graph colors
+- medium: report: Make transitions without end stretch to 2525
+- high: utils: Handle time zones in parse_time (bsc#949511)
+- medium: hb_report: Remove reference to function name in event patterns (bsc#942906)
+- medium: ui_script: Optionally print common params
+- medium: cibconfig: Fix sanity check for attribute-based fencing topology (#110)
+- high: cibconfig: Fix bug with node/resource collision
+- high: scripts: Determine output format of script correctly (bsc#949980)
+- doc: add explanatory comments to fencing_topology
+- doc: add missing backslash in fencing_topology example
+- doc: add missing <> to fencing_topology syntax
+- low: don't use deprecated crm_attribute -U option
+- doc: resource-discovery for location constraints
+- high: utils: Fix cluster_copy_file error when nodes provided
+- low: xmlutil: More informative message when updating resource references after rename
+- doc: fix some command syntax grammar in the man page
+- high: cibconfig: Delete constraints before resources
+- high: cibconfig: Fix bug in is_edit_valid (bsc#948547)
+- medium: hb_report: Don't cat binary logs
+- high: cibconfig: Allow node/rsc id collision in _set_update (bsc#948547)
+- low: report: Silence tar warning on early stream close
+- high: cibconfig: Allow nodes and resources with the same ID (bsc#948547)
+- high: log_patterns_118: Update the correct set of log patterns (bsc#942906)
+- low: ui_resource: Silence spurious migration non-warning from pacemaker
+- medium: config: Always fall back to /usr/bin:/usr/sbin:/bin:/sbin for programs (bsc#947818)
+- medium: report: Enable opening .xz-compressed report tarballs
+- medium: cibconfig: Only warn for grouped children in colocations (bsc#927423)
+- medium: cibconfig: Allow order constraints on group children (bsc#927423)
+- medium: cibconfig: Warn if configuring constraint on child resource (bsc#927423) (#101)
+- high: ui_node: Show remote nodes in crm node list (bsc#877962)
+- high: config: Remove config.core.supported_schemas (bsc#946893)
+- medium: report: Mark transitions with errors with a star in info output (bsc#943470)
+- low: report: Remove first transition tag regex
+- medium: report: Add transition tags command (bsc#943470)
+- low: ui_history: Better error handling and documentation for the detail command
+- low: ui_history: Swap from and to times if to < from
+- medium: cibconfig: XML parser support for node-attr fencing topology
+- medium: parse: Updated syntax for fencing-topology target attribute
+- medium: parse: Add support for node attribute as fencing topology target
+- high: scripts: Add enum type to script values
+- low: scripts: [MailTo] install mailx package
+- low: scripts: Fix typo in email type verifier
+- high: script: Fix subscript agent reference bug
+- low: constants: Add meta attributes for remote nodes
+- medium: scripts: Fix typo in lvm script
+- high: scripts: Generate actions for includes if none are defined
+- low: scripts: [virtual-ip] make lvs_support an advanced parameter
+- medium: crm_pssh: Timeout is an int (bsc#943820)
+- medium: scripts: Add MailTo script
+- low: scripts: Improved script parameter validation
+- high: parse: Fix crash when referencing score types by name (bsc#940194)
+- doc: Clarify documentation for colocations using node-attribute
+- high: ui_script: Print cached errors in json run
+- medium: scripts: Use --no option over --force unless force: true is set in the script
+- medium: options: Add --no option
+- high: scripts: Default to passing --force to crm after all
+- high: scripts: Add force parameter to cib and crm actions, and don't pass --force by default
+- low: scripts: Make virtual IP optional [nfsserver]
+- medium: scripts: Ensure that the Filesystem resource exists [nfsserver] (bsc#898658)
+- medium: report: Reintroduce empty transition pruning (bsc#943291)
+- low: hb_report: Collect libqb version (bsc#943327)
+- medium: log_patterns: Remove reference to function name in log patterns (bsc#942906)
+- low: hb_report: Increase time to wait for the logmark
+- high: hb_report: Always prefer syslog if available (bsc#942906)
+- high: report: Update transition edge regexes (bsc#942906)
+- medium: scripts: Switch install default to false
+- low: scripts: Catch attempt to pass dict as parameter value
+- high: report: Output format from pacemaker has changed (bsc#941681)
+- high: hb_report: Prefer pacemaker.log if it exists (bsc#941681)
+- medium: report: Add pacemaker.log to find_node_log list (bsc#941734)
+- high: hb_report: Correct path to hb_report after move to subdirectory (bsc#936026)
+- low: main: Bash completion didn't handle sudo correctly
+- medium: config: Add report_tool_options (bsc#917638)
+- high: parse: Add attributes to terminator set (bsc#940920)
+- Medium: cibconfig: skip sanity check for properties other than cib-bootstrap-options
+- medium: ui_script: Fix bug in verify json encoding
+- low: ui_script: Check JSON command syntax
+- medium: ui_script: Add name to action output (fate#318211)
+- low: scripts: Preserve formatting of longdescs
+- low: scripts: Clearer shortdesc for filesystem
+- low: scripts: Fix formatting for SAP scripts
+- low: scripts: add missing type annotations to libvirt script
+- low: scripts: make overridden parameters non-advanced by default
+- low: scripts: Tweak description for libvirt
+- low: scripts: Strip shortdesc for scripts and params
+- low: scripts: Title and category for exportfs
+- high: ui_script: drop end sentinel from API output (fate#318211)
+- low: scripts: Fix possible reference error in agent include
+- low: scripts: Clearer error message
+- low: Remove build revision from version
+- low: Add HAProxy script to data manifest
+- medium: constants: Add 'provides' meta attribute (bsc#936587)
+- medium: scripts: Add HAProxy script
+- high: hb_report: find utility scripts after move (bsc#936026)
+- high: ui_report: Move hb_report to subdirectory (bsc#936026)
+- high: Makefile: Don't unstall hb_report using data-manifest (bsc#936026)
+- medium: report: Fall back to cluster-glue hb_report if necessary (bsc#936026)
+- medium: scripts: stop inserting comments as values
+- high: scripts: subscript values not required if subscript has no parameters / all defaults (fate#318211)
+- medium: scripts: Fix name override for subscripts (fate#318211)
+- low: scripts: Clean up generated CIB (fate#318211)
+
+* Sat Jun 13 2015 Kristoffer Grönlund <kgronlund at suse.com> and many others
+- Pre-release 2.2.0-rc3
+- high: Merge rewizards development branch (fate#318211)
+  (fate#318384) (fate#318483) (fate#318482) (fate#318550)
+
+- Summary of some of the changes included in the merge of
+  the rewizards branch:
+
+  + Colorized status output
+  + New and more capable cluster script implementation
+  + Deprecated the crmsh templates (not the CIB templates,
+    the configuration templates)
+  + Implemented a JSON API interface to the cluster scripts
+    for hawk to use instead of having its own wizards
+  + Handlebars-like templating language for cluster scripts
+    that modify the CIB
+  + Collect metadata from resource agents to avoid duplication
+    in configuration scripts
+  + Extended validation support for parameter values
+  + New cluster scripts:
+
+   - Stonith: SBD and libvirt
+   - Apache web server
+   - NFS server
+   - cLVM
+   - Databases: MySQL / MariaDB / Oracle / DB2
+   - SAP
+   - OCFS2
+   - etc.
+
+  + Radically simplified automake and autoconf setup
+  + Improved completion performance
+  + Added pygment lexers used by the history guide as stand-alone
+    python module in contrib/
+  + Removed dependency on corosync for regression test suite
+  + Sort topics and commands in help output
+  + Hide internal commands in help and ls
+  + Clearer debug output when simulating
+  + Cleaned up and fixed documentation bugs
+
+- high: cmd_status: Colorize status output
+- low: cmd_status: Add full argument to status
+- low: scripts: Handle local runs even if nodelist doesn't contain local node
+- low: scripts: Stricter regexp for identifiers
+- doc: Fix unterminated block
+- low: command: Hide internal commands from ls
+- low: script: Rename describe to show
+- doc: Document the script JSON API
+- low: handles: Clean up special values
+- medium: help: Sort topics and commands in help output
+- doc: scripts: Basic documentation for the cluster scripts
+- doc: Describe website compilation process in development.md
+- contrib: Add pygment lexers used by the history guide
+- build: Add update-data-manifest.sh to generate datadir file list
+- medium: ui_script: Add JSON API
+- medium: config: add config.path.hawk_wizards
+- medium: handles: Fix error in strict parameter handling
+- scripts: Add placeholders for some basic scripts
+- WIP: in-progress notes etc.
+- doc: Update reference to parallax in scripts documentation
+- low: handles: Also allow # and $ in identifiers
+- medium: handles: Replace magic value with callables
+- medium: handles: {{^feature}}invert blocks{{/feature}}
+- medium: resource: Add ban command
+- medium: ui_root: Make the cibstatus command available directly from the root
+- medium: hb_report: Collect logs from pacemaker.log
+- low: crm: Detect and report use of python 3
+- doc: Link to japanese translation of Getting Started
+- medium: crm_pkg: Fix cluster init bug on RH-based systems
+- medium: crm_gv: Improved quoting of non-identifier node names (bsc#931837)
+- medium: crm_gv: Wrap non-identifier names in quotes (bsc#931837)
+- low: Fix references to pssh to refer to parallax
+- medium: report: Try to load source as session if possible (bsc#927407)
+- low: xmlutil: Update comment to match the code
+- Merge pull request #91 from krig/missing-transitions
+- high: report: New detection to fix missing transitions (bnc#917131)
+- medium: ui_configure: Add resource as an alias for primitive
+- medium: parse: Allow implicit initial for groups as well
+- medium: parse: More robust implicit initial parser
+- doc: website: Embedded hawk video in announcement
+- doc: news: News update for 2.1.4
+- Merge pull request #95 from dmuhamedagic/history-guide
+- Medium: doc: add history guide
+- Low: doc: simplify to make it work with python 2.6
+- Medium: hb_report: use faster zypper interface if available
+- medium: ui_configure: Wait for DC when removing running resource
+- Merge pull request #94 from rikkotec/patch-queue/debian-multiarch-compat
+- Fix CFLAGS for supporting triplet paths with pacemaker
+- low: schema: Don't leak PacemakerError exceptions (#93)
+- high: ui_cluster: Add copy command
+- doc: Update the documentation for the upgrade command
+- parse: Don't require trailing colon in tag definitions
+- high: crm_pssh: Explicitly set parallax inline option (krig/parallax#1)
+- doc: Add quick links to website
+- high: ui_configure: Add show-property command
+- medium: utils: Allow 1/0 as boolean values for parameters
+- doc: Correct the URL to point to the new upstream repository
+- doc: Add announcement for release 2.1.3
 - low: hb_report: Use crmsh config to find pengine/cib dirs (bsc#926377)
-- low: main: Catch any ValueErrors that may leak through
+- low: ui_options: add alias list for show
+- medium: cliformat: Escape double-quotes in nvpair values
+- high: parse: Don't allow constraints without applicants
+- medium: parse: Disallow location rules without resources
+- medium: ui_template: Make new command more robust (bnc#924641)
+- high: fix typo in previous commit
+- high: ui_node: Don't fence node in clearstate (boo#912919)
+- low: Replaced README with README.md
+- medium: ui_template: Always generate id unless explicitly defined (boo#921028)
+- high: cibconfig: Derive id for ops from referenced resource name (boo#921028)
+- medium: templates: Clearer descriptions for editing templates (boo#921028)
+- high: ui_context: Wait for DC after commit, not before (#85)
+- high: cibconfig: Don't delete valid tickets when removing referenced objects (bnc#922039)
+- high: ui_configure: Remove acl_group command (bnc#921056)
+- doc: Document changes to template list|new
+- medium: help: Teach help to fuzzy match topics
+- doc: Describe the shorthand syntax for commands
+- low: command: Use fuzzy match for sublevel check
+- medium: command: Fuzzy match command names
+- low: ui_context: Use true command name when reporting errors
+- doc: Move the main crmsh repository to the ClusterLabs organization on github
+- Merge pull request #82 from dmuhamedagic/sync_hb_report
+- Low: hb_report: add -X option for extra ssh options
+- Merge pull request #81 from lge/for-krig
+- fix: catch exception if schema file does not exist
+- low: allow pacemaker 1.0 version detection
+- low: allow (0,1) as option booleans
+- medium: cibconfig: Allow removal of non-existing elements if --force is set
+- medium: cibconfig: Allow delete of objects that don't exist without returning error code
+- medium: cibconfig: If a change results in no diff, exit silently
+- low: pacemaker: Remove debug output
+- medium: schema: Remove extra debug output
+- medium: schema: Test if node type is optional via schema
+- medium: parse: Treat pacemaker-next schema as 2.0+
+- low: cibconfig: Improved debug output when schema change fails
+- medium: cibconfig: Fix inverted logic causing spurious warning
+- Merge pull request #80 from dmuhamedagic/schema-update
+- Medium: cibconf: preserve cib user attributes
+- medium: ra: Handle non-OCF agent meta-data better
+- medium: config: Fix case-sensitivity for booleans
+- medium: report: Include transitions with configuration changes (bnc#917131)
+- medium: xmlutil: Improved check for related elements
+- doc: Documentation for show related:<obj>
+- medium: report: Convert RE exception to simpler UI output
+- medium: cibconfig: add show related:<obj>
+- doc: Add link to clusterlabs.org
+- medium: parse: Encode unicode using xmlcharrefreplace in parser
+- medium: parse: nvpair attributes with no value = <nvpair name=".."/> (#71)
+- medium: ui_cluster: Add diff command (bnc#914525)
+- doc: website: Fix changelog in news entry
+- doc: website: Add news release for 2.1.2
+- medium: report: Fall back to end_ts = start_ts
+- medium: util: Don't fall back to current time
+- high: xmlutil: Treat node type=member as normal (boo#904698)
+- low: xmlutil: logic bug in sanity_check_nvpairs
+- medium: xmlutil: Modify sort order of object types
+- medium: cibconfig: Use orderedset to avoid reordering bugs (#79)
+- medium: orderedset: Add OrderedSet type
+- medium: cibconfig: Detect v1 format and don't patch container changes (bnc#914098)
+- medium: constants: Update transition regex (#77)
+- Revert "high: xmlutil: Reorder elements only if sort_elements is set (#78)"
+- low: ui_options: Add underscore aliases for legacy options
+- high: xmlutil: Reorder elements only if sort_elements is set (#78)
+- medium: cibconfig: Strip digest from v1 diffs (bnc#914098)
+- Merge pull request #77 from krig/mail-patchset
+- medium: crm_pssh: Make tar follow symlinks
+- medium: constants: Fix transition start detection
+- medium: crm_pssh: Handle incomplete Option argument
+- high: crm_pssh: Use correct Task API in do_pssh (bnc#913261)
+- medium: cibconfig: Break infinite edit loop if --force is set
+- Merge pull request #76 from dmuhamedagic/log-patterns
+- high: utils: Locate binaries across sudo boundary (bnc#912483)
+- low: config: Convert NoOptionError to ValueError
+- low: msg: Add note on modifying supported schemas
+- medium: config: Add 2.3 to list of supported schemas
+- medium: utils: crm_daemon_dir is added to PATH in envsetup (#67)
 
-* Mon Jan 26 2015 Kristoffer Grönlund <kgronlund at suse.com> and many others
-- Release 2.1.2
+* Fri Jan  9 2015 Kristoffer Grönlund <kgronlund at suse.com> and many others
 - medium: ui_resource: Set probe interval 0 if not set (bnc#905050)
 - doc: Document probe op in resource trace (bnc#905050)
+- 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: pacemaker: Support pacemaker-next as schema
+- medium: cibconfig: Allow unsupported schemas with warning
+- medium: ra: Use correct path for crmd (#67)
+- medium: cmd_status: Show pending if available, enable extra options
 - 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: Revised CIB schema handling
+- medium: ui_configure: Add replace option to commit
 - medium: cibconfig: Don't bump epoch if stripping version
 - medium: ui_context: Lazily import readline
+- medium: ui_configure: selectors in save command
 - 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: config: add alwayscolor to display output option
+- doc: Clarify documentation for property (boo#905637)
+- doc: Add documentation section describing rule expressions (boo#905637)
+- doc: Link to documentation on rule expressions
 - medium: Allow removing groups even if is_running (boo#905271)
 - medium: cibconfig: Delete containers first in edits (boo#905268)
+- doc: Improved documentation for show and save
+- doc: Add note about modeline for vim syntax
 - medium: ui_history: Fix crash using empty object set
-- Low: term: get rid of annying ^O in piped-to-less-R output
+- utils: append_file: open destination in append-mode (boo#907528)
 - medium: parse: Allow nvpair with no value using name= syntax (#71)
 - medium: parse: Enable name[=value] for nvpair (#71)
+- Low: term: get rid of annying ^O in piped-to-less-R output
+- high: parse: Implicit initial parameter list
+- high: crm_pssh: Switch to python-parallax over pssh (bnc#905116)
+- low: report: Fix references to PSSH
+- low: report: Delay Report creation until use
 - 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
+- medium: ui_options: Accept prefix or suffix of option as argument
+- medium: Remove CIB version in case no --no-version.
+- low: cibconfig: Use LXML to remove version data more robustly (#75)
+- low: crm_gv: Avoid crashing if passed None in my_edge
+- low: cibconfig: Protect against dereferencing None when building graph
 
 * Tue Oct 28 2014 Kristoffer Grönlund <kgronlund at suse.com> and many others
-- Release 2.1.1
+- Pre-release 2.2.0-rc1
 - cibconfig: Clean up output from crm_verify (bnc#893138)
 - high: constants: Add acl_target and acl_group to cib_cli_map (bnc#894041)
+- medium: cibconfig: Add set command
+- doc: Rename asciidoc files to %.adoc
 - 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)
+- low: template: Add 'new <template>' shortcut
+- low: ui_configure: add rm as alias for delete
+- low: ui_template: List both templates and configs by default
 - medium: config: Assign default path in all cases
+- low: main: Catch any ValueErrors that may leak through
+- doc: Update TODO
+- low: corosync: Check tools before use
+- low: ui_ra: Don't crash when no OCF agents installed
+- low: ra: Add systemd-support to RaOS
+- doc: Updated documentation
+- doc: Handle command names with underscore
+- doc: Add tool to sort command list in documentation
+- doc: Sort command list in documentation alphabetically
 - 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
+- low: ui_cluster: More informative error message
+- low: main: Replace getopt with optparse
 - high: parse: Allow empty attribute values in nvpairs (bnc#898625)
-- high: cibconfig: Delay reinitialization after commit
+- high: ui_maintenance: Add maintenance sublevel (bnc#899234)
+- medium: rsctest: Add basic support for systemd services
+- medium: ui_maintenance: Combine action and actionssh into a single command
+- low: rsctest: Better error message for unsupported action
 - low: cibconfig: Improve wording of commit prompt
+- high: cibconfig: Delay reinitialization after commit
+- doc: Add website template for the nongnu page
+- medium: main: Disable interspersed args
 - low: cibconfig: Fix vim modeline
 - high: report: Find nodes for any log type (boo#900654)
 - high: hb_report: Collect logs from journald (boo#900654)
+- doc: Clarified note for default-timeouts
+- doc: Remove reference to crmsh documentation at clusterlabs.org
+- doc: start-guide: Fix version check
+- medium: xmlutil: Use idmgmt when creating new elements (bnc#901543)
+- doc: cibconfig: Add note on inner ids after rename
 - 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
diff --git a/Makefile.am b/Makefile.am
index 4803bc9..d89affb 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,6 +1,7 @@
 #
-# shell: Pacemaker code
+# crmsh
 #
+# Copyright (C) 2015 Kristoffer Gronlund
 # Copyright (C) 2008 Andrew Beekhof
 #
 # This program is free software; you can redistribute it and/or
@@ -21,27 +22,71 @@ MAINTAINERCLEANFILES    = Makefile.in aclocal.m4 configure
 
 sbin_SCRIPTS		= crm
 
-EXTRA_DIST		= crm
+# in .spec, set --sysconfdir=/etc
 
-SUBDIRS = doc modules templates test contrib hb_report utils scripts
+# Documentation
+doc_DATA = AUTHORS COPYING README.md ChangeLog $(generated_docs)
+crmconfdir=$(sysconfdir)/crm
+crmconf_DATA = crm.conf
+contribdir      = $(docdir)/contrib
+contrib_DATA	= contrib/pacemaker-crm.vim  contrib/pcmk.vim  contrib/README.vimsyntax
+helpdir     = $(datadir)/$(PACKAGE)
+asciiman	= doc/crm.8.adoc doc/crmsh_hb_report.8.adoc
+help_DATA	= doc/crm.8.adoc
 
-doc_DATA = AUTHORS COPYING README ChangeLog
+generated_docs	=
+generated_mans	=
+if BUILD_ASCIIDOC
+generated_docs	+= $(ascii:%.adoc=%.html) $(asciiman:%.adoc=%.html)
+generated_mans	+= $(asciiman:%.8.adoc=%.8)
+$(generated_mans): $(asciiman)
+man8_MANS	= $(generated_mans)
+endif
 
-crmversiondir=$(datadir)/@PACKAGE@
-crmversion_DATA = version
+%.html: %.adoc
+	$(ASCIIDOC) --unsafe --backend=xhtml11 $<
 
-# in .spec, set --sysconfdir=/etc
-crmconfdir=$(sysconfdir)/crm
-crmconf_DATA = crm.conf
+%.8: %.8.adoc
+	a2x -f manpage $<
+
+# Shared data files
+install-data-hook:
+	mkdir -p $(DESTDIR)$(datadir)/@PACKAGE@/; \
+	for d in $$(cat data-manifest); do \
+	install -D -m $$(test -x $$d && echo 0755 || echo 0644) $$d $(DESTDIR)$(datadir)/@PACKAGE@/$$d; done; \
+	mv $(DESTDIR)$(datadir)/@PACKAGE@/test $(DESTDIR)$(datadir)/@PACKAGE@/tests; \
+	cp test/testcases/xmlonly.sh $(DESTDIR)$(datadir)/@PACKAGE@/tests/testcases/configbasic-xml.filter
+
+hanoarchdir = $(datadir)/@PACKAGE@/hb_report
+hanoarch_DATA = hb_report/utillib.sh hb_report/ha_cf_support.sh hb_report/openais_conf_support.sh
+hanoarch_SCRIPTS = hb_report/hb_report
+EXTRA_DIST = $(hanoarch_DATA)
+
+# Python module installation
+all-local:
+	(cd $(srcdir); $(PYTHON) setup.py build \
+		--build-base $(shell readlink -f $(builddir))/build \
+		--verbose)
+
+# Fix for GNU/Linux
+if UNAME_IS_DEBIAN
+python_prefix = 
+else
+python_prefix = --prefix=$(DESTDIR)$(prefix)
+endif
 
 install-exec-local:
+	-mkdir -p $(DESTDIR)$(pkgpythondir)
+	$(PYTHON) $(srcdir)/setup.py install \
+		$(python_prefix) \
+		--record $(DESTDIR)$(pkgpythondir)/install_files.txt \
+		--verbose
 	$(INSTALL) -d -m 770 $(DESTDIR)/$(CRM_CACHE_DIR)
-	-chown $(CRM_DAEMON_USER):$(CRM_DAEMON_GROUP) $(DESTDIR)/$(CRM_CACHE_DIR)
+	-rm -rf $(generated_docs) $(generated_mans)
 
-clean-generic:
-	rm -f $(TARFILE) *.tar.bz2 *.sed
+uninstall-local:
+	cat $(DESTDIR)$(pkgpythondir)/install_files.txt | xargs rm -rf
+	rm -rf $(DESTDIR)$(pkgpythondir)
 
 dist-clean-local:
 	rm -f autoconf automake autoheader
-
-.PHONY: rpm pkg handy handy-copy
diff --git a/README b/README
deleted file mode 100644
index 85befd8..0000000
--- a/README
+++ /dev/null
@@ -1,58 +0,0 @@
-# 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
deleted file mode 100644
index aaf5eb3..0000000
--- a/README.dev
+++ /dev/null
@@ -1,263 +0,0 @@
-== 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/README.md b/README.md
new file mode 100644
index 0000000..c7ced97
--- /dev/null
+++ b/README.md
@@ -0,0 +1,62 @@
+# crmsh
+
+[![Build Status](https://travis-ci.org/ClusterLabs/crmsh.svg?branch=master)](https://travis-ci.org/ClusterLabs/crmsh)
+
+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.
+
+<br />
+##### More Information
+
+* The website for crmsh is here: [crmsh @ Github.io](http://crmsh.github.io).
+* Documentation for the latest stable release is found at the [Github.io documentation](http://crmsh.github.io) page.
+
+
+<br />
+## Installation
+
+Autoconf is used to take care of platform dependent locations. It is mainly inherited from the Pacemaker source.
+
+```shell
+./autogen.sh
+./configure
+make
+make install
+```
+
+
+<br />
+## Manifest
+
+```shell
+./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
+```
+
+
+<br />
+## 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:
+
+```shell
+git clone https://github.com/ClusterLabs/crmsh
+```
+
+<br />
+* Bugs and issues can be reported at the [crmsh issues @ Github.com](https://github.com/clusterlabs/crmsh/issues) page.
+* Any other questions or comments can be made on the [Clusterlabs users mailing list](http://clusterlabs.org/mailman/listinfo/users).
diff --git a/TODO b/TODO
index 7dacce6..91600f6 100644
--- a/TODO
+++ b/TODO
@@ -3,6 +3,7 @@ Features
 . Audit
 
 	- add user auditing, i.e. save all commands that were run
+      (DONE: see the -R flag)
 
 	- save to a local file (distributed DB would probably be an
 	  overkill)
@@ -21,6 +22,7 @@ Features
 . CIB features
 
     - Support ACL commands in Pacemaker 1.1.12>
+      (DONE)
 
 . Command features
 
diff --git a/acinclude.m4 b/acinclude.m4
deleted file mode 100644
index fa8fef2..0000000
--- a/acinclude.m4
+++ /dev/null
@@ -1,39 +0,0 @@
-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/configure.ac b/configure.ac
index 747301e..f378f26 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,27 +1,14 @@
 dnl
-dnl autoconf for CRM shell
+dnl autoconf for crmsh
 dnl
+dnl Copyright (C) 2015 Kristoffer Gronlund
 dnl Copyright (C) 2008 Andrew Beekhof
 dnl
 dnl License: GNU General Public License (GPL)
 
-dnl ===============================================
-dnl Bootstrap 
-dnl ===============================================
-AC_PREREQ(2.53)
+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_INIT([crmsh],[2.2.0],[users at clusterlabs.org])
 
 AC_ARG_WITH(version,
     [  --with-version=version   Override package version (if you're a packager needing to pretend) ],
@@ -31,11 +18,22 @@ 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)
+OCF_ROOT_DIR="/usr/lib/ocf"
+AC_ARG_WITH(ocf-root,
+    [  --with-ocf-root=DIR      directory for OCF scripts [${OCF_ROOT_DIR}]],
+    [ if test x"$withval" = xprefix; then OCF_ROOT_DIR=${prefix}; else
+	 OCF_ROOT_DIR="$withval"; fi ])
+
+AC_ARG_WITH(daemon-user,
+    [  --with-daemon-user=USER_NAME
+                                User to run privileged non-root things as. [default=hacluster]  ],
+    [ CRM_DAEMON_USER="$withval" ],
+    [ CRM_DAEMON_USER="hacluster" ])
 
-PACKAGE_SERIES=`echo $PACKAGE_VERSION | awk -F. '{ print $1"."$2 }'`
-AC_SUBST(PACKAGE_SERIES)
+AM_INIT_AUTOMAKE([no-define foreign])
+m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES])
+AC_DEFINE_UNQUOTED(PACKAGE, "$PACKAGE_NAME")
+AC_DEFINE_UNQUOTED(VERSION, "$PACKAGE_VERSION")
 
 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 
@@ -43,255 +41,24 @@ 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...
+AM_CONDITIONAL([UNAME_IS_DEBIAN], [test x`uname -v | grep -oh Debian` = x"Debian"])
 
-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(OCF_ROOT_DIR)
 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}])
+AC_OUTPUT
diff --git a/contrib/Makefile.am b/contrib/Makefile.am
deleted file mode 100644
index 96a33f3..0000000
--- a/contrib/Makefile.am
+++ /dev/null
@@ -1,25 +0,0 @@
-#
-# 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
index fc92fff..7170774 100644
--- a/contrib/README.vimsyntax
+++ b/contrib/README.vimsyntax
@@ -8,10 +8,16 @@ 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.
+ 1. Copy one of them to ~/.vim/syntax/pcmk.vim.
+
+ 2. Make sure the following is added to your VIM rc file
+    (~/.vimrc or ~/.exrc):
+
+
+    syntax on
+    set modeline
+    set modelines=5
 
-Don't forget to put "syntax on" in the VIM rc file (~/.vimrc or
-~/.exrc).
 
 If you're editing a file directly, just type:
 
diff --git a/contrib/pygments_crmsh_lexers/__init__.py b/contrib/pygments_crmsh_lexers/__init__.py
new file mode 100644
index 0000000..3a6ac06
--- /dev/null
+++ b/contrib/pygments_crmsh_lexers/__init__.py
@@ -0,0 +1,2 @@
+from .ansiclr import ANSIColorsLexer
+from .crmsh import CrmshLexer
diff --git a/contrib/pygments_crmsh_lexers/ansiclr.py b/contrib/pygments_crmsh_lexers/ansiclr.py
new file mode 100644
index 0000000..42d975e
--- /dev/null
+++ b/contrib/pygments_crmsh_lexers/ansiclr.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+"""
+    pygments.lexers.console
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for misc console output.
+
+    :copyright: Copyright 2006-2015 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+from pygments.lexer import RegexLexer, include, bygroups
+from pygments.token import Generic, Comment, String, Text, Keyword, Name, \
+    Punctuation, Number
+
+__all__ = ['ANSIColorsLexer']
+
+_ESC = "\x1b\["
+# this is normally to reset (reset attributes, set primary font)
+# there could be however other reset sequences and in that case
+# sgr0 needs to be updated
+_SGR0 = "%s(?:0;10|10;0)m" % _ESC
+# BLACK RED GREEN YELLOW
+# BLUE MAGENTA CYAN WHITE
+_ANSI_COLORS = (Generic.Emph, Generic.Error, Generic.Inserted, Generic.Keyword,
+                Generic.Keyword, Generic.Prompt, Generic.Traceback, Generic.Output)
+
+
+def _ansi2rgb(lexer, match):
+    code = match.group(1)
+    text = match.group(2)
+    yield match.start(), _ANSI_COLORS[int(code)-30], text
+
+
+class ANSIColorsLexer(RegexLexer):
+    """
+    Interpret ANSI colors.
+    """
+    name = 'ANSI Colors'
+    aliases = ['ansiclr']
+    filenames = ["*.typescript"]
+
+    tokens = {
+        'root': [
+            (r'%s(3[0-7]+)m(.*?)%s' % (_ESC, _SGR0), _ansi2rgb),
+            (r'[^\x1b]+', Text),
+            # drop the rest of the graphic codes
+            (r'(%s[0-9;]+m)()' % _ESC, bygroups(None, Text)),
+        ]
+    }
diff --git a/contrib/pygments_crmsh_lexers/crmsh.py b/contrib/pygments_crmsh_lexers/crmsh.py
new file mode 100644
index 0000000..59b7a74
--- /dev/null
+++ b/contrib/pygments_crmsh_lexers/crmsh.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+"""
+    pygments.lexers.dsls
+    ~~~~~~~~~~~~~~~~~~~~
+
+    Lexers for various domain-specific languages.
+
+    :copyright: Copyright 2006-2015 by the Pygments team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+"""
+
+
+import re
+
+from pygments.lexer import RegexLexer, bygroups, words, include, default
+from pygments.token import Text, Comment, Operator, Keyword, Name, String, \
+    Number, Punctuation, Literal, Whitespace
+
+__all__ = ['CrmshLexer']
+
+
+class CrmshLexer(RegexLexer):
+    """
+    Lexer for `crmsh <http://crmsh.github.io/>`_ configuration files
+    for Pacemaker clusters.
+
+    .. versionadded:: 2.1
+    """
+    name = 'Crmsh'
+    aliases = ['crmsh', 'pcmk']
+    filenames = ['*.crmsh', '*.pcmk']
+    mimetypes = []
+
+    elem = words((
+        'node', 'primitive', 'group', 'clone', 'ms', 'location',
+        'colocation', 'order', 'fencing_topology', 'rsc_ticket',
+        'rsc_template', 'property', 'rsc_defaults',
+        'op_defaults', 'acl_target', 'acl_group', 'user', 'role',
+        'tag'), suffix=r'(?![\w#$-])')
+    sub = words((
+        'params', 'meta', 'operations', 'op', 'rule',
+        'attributes', 'utilization'), suffix=r'(?![\w#$-])')
+    acl = words(('read', 'write', 'deny'), suffix=r'(?![\w#$-])')
+    bin_rel = words(('and', 'or'), suffix=r'(?![\w#$-])')
+    un_ops = words(('defined', 'not_defined'), suffix=r'(?![\w#$-])')
+    date_exp = words(('in_range', 'date', 'spec', 'in'), suffix=r'(?![\w#$-])')
+    acl_mod = (r'(?:tag|ref|reference|attribute|type|xpath)')
+    bin_ops = (r'(?:lt|gt|lte|gte|eq|ne)')
+    val_qual = (r'(?:string|version|number)')
+    rsc_role_action=(r'(?:Master|Started|Slave|Stopped|'
+        r'start|promote|demote|stop)')
+
+    tokens = {
+        'root': [
+            (r'^#.*\n?', Comment),
+            # attr=value (nvpair)
+            (r'([\w#$-]+)(=)("(?:""|[^"])*"|\S+)',
+                bygroups(Name.Attribute, Punctuation, String)),
+            # need this construct, otherwise numeric node ids
+            # are matched as scores
+            # elem id:
+            (r'(node)(\s+)([\w#$-]+)(:)',
+                bygroups(Keyword, Whitespace, Name, Punctuation)),
+            # scores
+            (r'([+-]?([0-9]+|inf)):', Number),
+            # keywords (elements and other)
+            (elem, Keyword),
+            (sub, Keyword),
+            (acl, Keyword),
+            # binary operators
+            (r'(?:%s:)?(%s)(?![\w#$-])' % (val_qual,bin_ops),
+                Operator.Word),
+            # other operators
+            (bin_rel, Operator.Word),
+            (un_ops, Operator.Word),
+            (date_exp, Operator.Word),
+            # builtin attributes (e.g. #uname)
+            (r'#[a-z]+(?![\w#$-])', Name.Builtin),
+            # acl_mod:blah
+            (r'(%s)(:)("(?:""|[^"])*"|\S+)' % acl_mod,
+                bygroups(Keyword, Punctuation, Name)),
+            # rsc_id[:(role|action)]
+            # NB: this matches all other identifiers
+            (r'([\w#$-]+)(?:(:)(%s))?(?![\w#$-])' % rsc_role_action,
+                bygroups(Name, Punctuation, Operator.Word)),
+            # punctuation
+            (r'(\\(?=\n)|[[\](){}/:@])', Punctuation),
+            (r'\s+|\n', Whitespace),
+        ],
+    }
diff --git a/contrib/setup.py b/contrib/setup.py
new file mode 100644
index 0000000..bf7bc98
--- /dev/null
+++ b/contrib/setup.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python
+
+from setuptools import setup
+
+setup(name='pygments-crmsh-lexers',
+      version='0.0.5',
+      description='Pygments crmsh custom lexers.',
+      keywords='pygments crmsh lexer',
+      license='BSD',
+
+      author='Kristoffer Gronlund',
+      author_email='kgronlund at suse.com',
+
+      url='https://github.com/ClusterLabs/crmsh',
+
+      packages=['pygments_crmsh_lexers'],
+      install_requires=['pygments>=2.0.2'],
+
+      entry_points='''[pygments.lexers]
+                      ANSIColorsLexer=pygments_crmsh_lexers:ANSIColorsLexer
+                      CrmshLexer=pygments_crmsh_lexers:CrmshLexer''',
+
+      classifiers=[
+          'Environment :: Plugins',
+          'Intended Audience :: Developers',
+          'License :: OSI Approved :: BSD License',
+          'Operating System :: OS Independent',
+          'Programming Language :: Python',
+          'Programming Language :: Python :: 2',
+          'Programming Language :: Python :: 3',
+          'Topic :: Software Development :: Libraries :: Python Modules',
+      ],)
diff --git a/crm b/crm
index 31c5ff2..cc2e3ff 100755
--- a/crm
+++ b/crm
@@ -1,27 +1,14 @@
 #!/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
+# Copyright (C) 2008-2015 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# Copyright (C) 2013-2015 Kristoffer Gronlund <kgronlund at suse.com>
+# See COPYING for license information.
 #
 
-minimum_version = '2.6'
 import sys
-
 from distutils import version
+
+minimum_version = '2.6'
 v_min = version.StrictVersion(minimum_version)
 v_this = version.StrictVersion(sys.version[:3])
 if v_min > v_this:
@@ -30,28 +17,39 @@ if v_min > v_this:
     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/ .
+        from crmsh import main
+    except ImportError as msg:
+        try:
+            # Perhaps we're running from the source directory
+            import modules
+            sys.modules['crmsh'] = sys.modules['modules']
+            from crmsh import main
+        except ImportError as msg2:
+            sys.stderr.write('''Fatal error:
+        %s
+        %s
+
+    Failed to start crmsh! This is likely due to a broken
+    installation or a missing dependency.
+
+    If you are using a packaged version of crmsh, 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)
+            sys.exit(-1)
+except AttributeError as msg:
+    sys.stderr.write('''Fatal error: %s
+
+    Failed to start crmsh! This is likely due to having
+    configured Python 3 as the default python version.
+    crmsh requires Python 2.6 or higher, but not (yet)
+    Python 3.
+''' % (msg))
+    sys.exit(-1)
 
 rc = main.run()
 sys.exit(rc)
diff --git a/crm.conf.in b/crm.conf.in
index 78cf297..4f08b25 100644
--- a/crm.conf.in
+++ b/crm.conf.in
@@ -17,19 +17,21 @@
 ; ptest = ptest, crm_simulate
 ; dotty = dotty
 ; dot = dot
+; ignore_missing_metadata = no
+; report_tool_options =
 
 [path]
-sharedir = @datadir@/@PACKAGE@
-cache = @CRM_CACHE_DIR@
-crm_config = @CRM_CONFIG_DIR@
-crm_daemon_dir = @CRM_DAEMON_DIR@
+; sharedir = <detected>
+; cache = <detected>
+; crm_config = <detected>
+; crm_daemon_dir = <detected>
 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
+; crm_dtd_dir = <detected>
+; pe_state_dir = <detected>
+; heartbeat_dir = <detected>
+; hb_delnode = /usr/share/heartbeat/hb_delnode
+; nagios_plugins = /usr/lib/nagios/plugins
 
 ; [color]
 ; style = color
diff --git a/crmsh-cibadmin_can_patch.patch b/crmsh-cibadmin_can_patch.patch
deleted file mode 100644
index f90530e..0000000
--- a/crmsh-cibadmin_can_patch.patch
+++ /dev/null
@@ -1,23 +0,0 @@
-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
index 5f9e484..9d9075d 100644
--- a/crmsh.spec
+++ b/crmsh.spec
@@ -1,7 +1,7 @@
 #
 # spec file for package crmsh
 #
-# Copyright (c) 2015 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2016 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
@@ -30,51 +30,37 @@
 %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}}}
+%{!?python_sitelib: %define python_sitelib %(python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
 
 Name:           crmsh
 Summary:        High Availability cluster command-line interface
 License:        GPL-2.0+
 Group:          %{pkg_group}
-Version:        2.0
-Release:        %{?crmsh_release}%{?dist}
+Version:        2.2.0
+Release:        0
 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
+Source0:        %{name}-%{version}.tar.bz2
 BuildRoot:      %{_tmppath}/%{name}-%{version}-build
 Requires(pre):  pacemaker
+Requires:       %{name}-scripts >= %{version}-%{release}
 Requires:       /usr/bin/which
-Requires:       pssh
 Requires:       python >= 2.6
 Requires:       python-dateutil
 Requires:       python-lxml
+Requires:       python-parallax
 BuildRequires:  python-lxml
+BuildRequires:  python-setuptools
 
 %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
@@ -91,38 +77,65 @@ BuildRequires:  python
 BuildRequires:  libxslt-tools
 %endif
 
+%if 0%{?suse_version} > 1110
+BuildArch:      noarch
+%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:  mailx
 BuildRequires:  procps
 BuildRequires:  python-dateutil
 BuildRequires:  python-nose
+BuildRequires:  python-parallax
 BuildRequires:  vim
 Requires:       pacemaker
-Requires:       pssh
+
+%if 0%{?suse_version} > 1110
+BuildArch:      noarch
+%endif
+
+%if 0%{?suse_version}
+BuildRequires:  libglue-devel
+BuildRequires:  libpacemaker-devel
+%else
+BuildRequires:  cluster-glue-libs-devel
+BuildRequires:  pacemaker-libs-devel
+%endif
+%if 0%{?fedora_version}
+BuildRequires:  PyYAML
+%else
+BuildRequires:  python-PyYAML
+%endif
+
 %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.
+This package contains the regression test suite for crmsh.
+
+%package scripts
+Summary:        Crm Shell Cluster Scripts
+Group:          Productivity/Clustering/HA
 
-Authors: Dejan Muhamedagic <dejan at suse.de> and many others
+%description scripts
+Cluster scripts for crmsh. The cluster scripts can be run
+directly from the crm command line, or used by user interfaces
+like hawk to implement configuration wizards.
 
 %prep
-%setup -q -n %{upstream_prefix}
-%patch11 -p1
+%setup -q
 
 # Force the local time
 #
@@ -135,29 +148,16 @@ 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}
+make %{_smp_mflags} VERSION="%{version}-%{release}" sysconfdir=%{_sysconfdir} localstatedir=%{_var}
 
 %if 0%{?with_regression_tests}
-	./test/unit-tests.sh --quiet
+	./test/run --quiet
     if [ ! $? ]; then
         echo "Unit tests failed."
         exit 1
@@ -180,11 +180,8 @@ rm -rf %{buildroot}
 %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
+	%{_datadir}/%{name}/tests/regression.sh
+	result1=$?
 	cd %{_datadir}/%{name}/tests
 	./cib-tests.sh
 	result2=$?
@@ -199,10 +196,12 @@ fi
 %defattr(-,root,root)
 
 %{_sbindir}/crm
-%{py_sitedir}/crmsh
+%{python_sitelib}/crmsh
+%{python_sitelib}/crmsh*.egg-info
 
 %{_datadir}/%{name}
 %exclude %{_datadir}/%{name}/tests
+%exclude %{_datadir}/%{name}/scripts
 
 %doc %{_mandir}/man8/*
 %{crmsh_docdir}/COPYING
@@ -210,7 +209,7 @@ fi
 %{crmsh_docdir}/crm.8.html
 %{crmsh_docdir}/crmsh_hb_report.8.html
 %{crmsh_docdir}/ChangeLog
-%{crmsh_docdir}/README
+%{crmsh_docdir}/README.md
 %{crmsh_docdir}/contrib/*
 
 %config %{_sysconfdir}/crm
@@ -220,6 +219,10 @@ fi
 %dir %attr (770, %{uname}, %{gname}) %{_var}/cache/crm
 %config %{_sysconfdir}/bash_completion.d/crm.sh
 
+%files scripts
+%defattr(-,root,root)
+%{_datadir}/%{name}/scripts
+
 %files test
 %defattr(-,root,root)
 %{_datadir}/%{name}/tests
diff --git a/data-manifest b/data-manifest
new file mode 100644
index 0000000..d2cd550
--- /dev/null
+++ b/data-manifest
@@ -0,0 +1,172 @@
+scripts/add/add.py
+scripts/add/main.yml
+scripts/apache/main.yml
+scripts/check-uptime/fetch.py
+scripts/check-uptime/main.yml
+scripts/check-uptime/report.py
+scripts/clvm/main.yml
+scripts/clvm-vg/main.yml
+scripts/database/main.yml
+scripts/db2-hadr/main.yml
+scripts/db2/main.yml
+scripts/drbd/main.yml
+scripts/exportfs/main.yml
+scripts/filesystem/main.yml
+scripts/gfs2-base/main.yml
+scripts/gfs2/main.yml
+scripts/haproxy/haproxy.cfg
+scripts/haproxy/main.yml
+scripts/health/collect.py
+scripts/health/hahealth.py
+scripts/health/main.yml
+scripts/health/report.py
+scripts/init/authkey.py
+scripts/init/basic.cib.template
+scripts/init/collect.py
+scripts/init/configure.py
+scripts/init/corosync.conf.template
+scripts/init/init.py
+scripts/init/main.yml
+scripts/init/verify.py
+scripts/libvirt/main.yml
+scripts/lvm/main.yml
+scripts/mailto/main.yml
+scripts/nfsserver/main.yml
+scripts/ocfs2/main.yml
+scripts/oracle/main.yml
+scripts/raid1/main.yml
+scripts/raid-lvm/main.yml
+scripts/remove/main.yml
+scripts/remove/remove.py
+scripts/sap-as/main.yml
+scripts/sap-ci/main.yml
+scripts/sap-db/main.yml
+scripts/sapdb/main.yml
+scripts/sapinstance/main.yml
+scripts/sap-simple-stack/main.yml
+scripts/sap-simple-stack-plus/main.yml
+scripts/sbd/main.yml
+scripts/virtual-ip/main.yml
+templates/apache
+templates/clvm
+templates/filesystem
+templates/gfs2
+templates/gfs2-base
+templates/ocfs2
+templates/sbd
+templates/virtual-ip
+test/bugs-test.txt
+test/cibtests/001.exp.xml
+test/cibtests/001.input
+test/cibtests/002.exp.xml
+test/cibtests/002.input
+test/cibtests/003.exp.xml
+test/cibtests/003.input
+test/cibtests/004.exp.xml
+test/cibtests/004.input
+test/cib-tests.sh
+test/cibtests/shadow.base
+test/crm-interface
+test/defaults
+test/descriptions
+test/evaltest.sh
+test/history-test.tar.bz2
+test/list-undocumented-commands.py
+test/README.regression
+test/regression.sh
+test/run
+test/testcases/acl
+test/testcases/acl.excl
+test/testcases/acl.exp
+test/testcases/basicset
+test/testcases/bugs
+test/testcases/bugs.exp
+test/testcases/commit
+test/testcases/commit.exp
+test/testcases/common.excl
+test/testcases/common.filter
+test/testcases/confbasic
+test/testcases/confbasic.exp
+test/testcases/confbasic-xml
+test/testcases/confbasic-xml.exp
+test/testcases/delete
+test/testcases/delete.exp
+test/testcases/edit
+test/testcases/edit.excl
+test/testcases/edit.exp
+test/testcases/file
+test/testcases/file.exp
+test/testcases/history
+test/testcases/history.excl
+test/testcases/history.exp
+test/testcases/history.post
+test/testcases/history.pre
+test/testcases/newfeatures
+test/testcases/newfeatures.exp
+test/testcases/node
+test/testcases/node.exp
+test/testcases/options
+test/testcases/options.exp
+test/testcases/ra
+test/testcases/ra.exp
+test/testcases/ra.filter
+test/testcases/resource
+test/testcases/resource.exp
+test/testcases/rset
+test/testcases/rset.exp
+test/testcases/rset-xml
+test/testcases/rset-xml.exp
+test/testcases/scripts
+test/testcases/scripts.exp
+test/testcases/scripts.filter
+test/testcases/shadow
+test/testcases/shadow.exp
+test/testcases/xmlonly.sh
+test/unittests/bug-862577_corosync.conf
+test/unittests/corosync.conf.1
+test/unittests/corosync.conf.2
+test/unittests/__init__.py
+test/unittests/schemas/acls-1.1.rng
+test/unittests/schemas/acls-1.2.rng
+test/unittests/schemas/constraints-1.0.rng
+test/unittests/schemas/constraints-1.1.rng
+test/unittests/schemas/constraints-1.2.rng
+test/unittests/schemas/fencing.rng
+test/unittests/schemas/nvset.rng
+test/unittests/schemas/pacemaker-1.0.rng
+test/unittests/schemas/pacemaker-1.1.rng
+test/unittests/schemas/pacemaker-1.2.rng
+test/unittests/schemas/resources-1.0.rng
+test/unittests/schemas/resources-1.1.rng
+test/unittests/schemas/resources-1.2.rng
+test/unittests/schemas/rule.rng
+test/unittests/schemas/score.rng
+test/unittests/schemas/versions.rng
+test/unittests/scripts/inc1/main.yml
+test/unittests/scripts/inc2/main.yml
+test/unittests/scripts/legacy/main.yml
+test/unittests/scripts/templates/apache.xml
+test/unittests/scripts/templates/virtual-ip.xml
+test/unittests/scripts/v2/main.yml
+test/unittests/scripts/vipinc/main.yml
+test/unittests/scripts/vip/main.yml
+test/unittests/scripts/workflows/10-webserver.xml
+test/unittests/test_bugs.py
+test/unittests/test_cib.py
+test/unittests/test_cliformat.py
+test/unittests/test.conf
+test/unittests/test_corosync.py
+test/unittests/test_gv.py
+test/unittests/test_handles.py
+test/unittests/test_objset.py
+test/unittests/test_parse.py
+test/unittests/test_resource.py
+test/unittests/test_scripts.py
+test/unittests/test_time.py
+test/unittests/test_utils.py
+utils/crm_clean.py
+utils/crm_init.py
+utils/crm_pkg.py
+utils/crm_rpmcheck.py
+utils/crm_script.py
+version
diff --git a/doc/Makefile.am b/doc/Makefile.am
deleted file mode 100644
index 3451068..0000000
--- a/doc/Makefile.am
+++ /dev/null
@@ -1,43 +0,0 @@
-#
-# 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.adoc
similarity index 81%
rename from doc/crm.8.txt
rename to doc/crm.8.adoc
index 70fb1bf..305f334 100644
--- a/doc/crm.8.txt
+++ b/doc/crm.8.adoc
@@ -1,5 +1,5 @@
 :man source:   crm
-:man version:  2.1.3
+:man version:  2.2.0
 :man manual:   crmsh documentation
 
 crm(8)
@@ -23,7 +23,7 @@ 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/.
+For more information on Pacemaker itself, 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
@@ -35,28 +35,13 @@ 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 can be used to manage every aspect of configuring and
+maintaining a cluster. It provides a simplified line-based syntax on
+top of the XML configuration format used by Pacemaker, commands for
+starting and stopping resources, tools for exploring the history of a
+cluster including log scraping and a set of cluster scripts useful for
+automating the setup and installation of services on the cluster
+nodes.
 
 The `crm` shell is line oriented: every command must start and finish
 on the same line. It is possible to use a continuation character (+\+)
@@ -76,8 +61,8 @@ OPTIONS
     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
+    Choose one of the output options: +plain+, +color-always+, +color+,
+    or +uppercase+. The default is +color+ if the terminal emulation
     supports colors. Otherwise, +plain+ is used.
 
 *-F, --force*::
@@ -89,11 +74,10 @@ OPTIONS
     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, --history*='DIR|FILE|SESSION'::
+    A directory or file containing a cluster report to load
+    into the `history` commands, or the name of a previously
+    saved history session.
 
 *-h, --help*::
     Print help page.
@@ -110,8 +94,8 @@ OPTIONS
     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.
+    Extra directory where crm looks for cluster scripts, or a list of
+    directories separated by semi-colons (e.g. +/dir1;/dir2;etc.+).
 
 [[topics_Introduction,Introduction]]
 == Introduction
@@ -211,7 +195,7 @@ work with so-called <<topics_Features_Shadows,shadow CIBs>>. These are separate,
 configurations stored in files, that can be applied and thereby
 replace the live configuration at any time.
 
-[[topics_Introcution_Completion,Tab completion]]
+[[topics_Introduction_Completion,Tab completion]]
 === Tab completion
 
 The `crm` makes extensive use of tab completion. The completion
@@ -250,6 +234,24 @@ auth* (string)
 system shell. This should be installed automatically with the command
 itself.
 
+[[topics_Introduction_Shorthand,Shorthand syntax]]
+=== Shorthand syntax
+
+When using the `crm` shell to manage clusters, you will end up typing
+a lot of commands many times over. Clear command names like
++configure+ help in understanding and learning to use the cluster
+shell, but is easy to misspell and is tedious to type repeatedly. The
+interactive mode and tab completion both help with this, but the `crm`
+shell also has the ability to understand a variety of shorthand
+aliases for all of the commands.
+
+For example, instead of typing `crm status`, you can type `crm st` or
+`crm stat`. Instead of `crm configure` you can type `crm cfg` or even
+`crm cf`. `crm resource` can be shorted as `crm rsc`, and so on.
+
+The exact list of accepted aliases is too long to print in full, but
+experimentation and typoes should help in discovering more of them.
+
 [[topics_Features,Features]]
 == Features
 
@@ -321,6 +323,14 @@ configuration cannot be saved.
 [[topics_Features_Templates,Configuration templates]]
 === Configuration templates
 
+.Deprecation note
+****************************
+Configuration templates have been deprecated in favor of the more
+capable `cluster scripts`. To learn how to use cluster scripts, see
+the dedicated documentation on the `crmsh` website at
+http://crmsh.github.io/, or in the <<cmdhelp_script,Script section>>.
+****************************
+
 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.
@@ -447,12 +457,14 @@ 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
@@ -746,18 +758,99 @@ primitive virtual-ip IPaddr2 params $vip:ip=192.168.1.100
 primitive webserver apache params @vip:server_ip
 ............
 
+[[topics_Syntax_RuleExpressions,Syntax: Rule expressions]]
+=== Syntax: Rule expressions
+
+Many of the configuration commands in `crmsh` now support the use of
+_rule expressions_, which can influence what attributes apply to a
+resource or under which conditions a constraint is applied, depending
+on changing conditions like date, time, the value of attributes and
+more.
+
+Here is an example of a simple rule expression used to apply a
+a different resource parameter on the node named `node1`:
+
+..............
+primitive my_resource Special \
+  params 2: rule #uname eq node1 interface=eth1 \
+  params 1: interface=eth0
+..............
+
+This primitive resource has two lists of parameters with descending
+priority. The parameter list with the highest priority is applied
+first, but only if the rule expressions for that parameter list all
+apply. In this case, the rule `#uname eq node1` limits the parameter
+list so that it is only applied on `node1.
+
+Note that rule expressions are not terminated and are immediately
+followed by the data to which the rule is applied. In this case, the
+name-value pair `interface=eth1`.
+
+Rule expressions can contain multiple expressions connected using the
+boolean operator `or` and `and`. The full syntax for rule expressions
+is listed below.
+
+..............
+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>
+..............
+
 [[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 commands are structured to be compatible with the shell command
+line. Sometimes, the underlying Pacemaker grammar uses characters that
+have special meaning in bash, that will need to be quoted. This
+includes the hash or pound sign (`#`), single and double quotes, and
+any significant whitespace.
+
+Whitespace is also significant when assigning values, meaning that
++key=value+ is different from +key = value+.
+
+Commands can be referenced using short-hand as long as the short-hand
+is unique. This can be either a prefix of the command name or a prefix
+string of characters found in the name.
 
-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, `:`, `=`).
+For example, +status+ can be abbreviated as +st+ or +su+, and
++configure+ as +conf+ or +cfg+.
+
+The syntax for the commands is given below in an informal, BNF-like
+grammar.
+
+* `<value>` denotes a string.
+* `[value]` means that the construct is optional.
+* The ellipsis (`...`) signifies that the previous construct may be
+  repeated.
+* `first|second` means either first or second.
+* The rest are literals (strings, `:`, `=`).
 
 [[cmdhelp_root_status,Cluster status]]
 === `status`
@@ -766,15 +859,36 @@ 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.
 
+Example:
+...............
+status
+status simple
+status full
+...............
+
 Usage:
 ...............
 status [<option> ...]
 
-option :: bynode | inactive | ops | timing | failcounts
+option :: full
+        | bynode
+        | inactive
+        | ops
+        | timing
+        | failcounts
+        | verbose
+        | quiet
+        | html
+        | xml
+        | simple
+        | tickets
+        | noheaders
+        | detail
+        | brief
 ...............
 
 [[cmdhelp_cluster,Cluster setup and management]]
-=== `cluster`
+=== `cluster` - Cluster setup and management
 
 Whole-cluster configuration management with High Availability
 awareness.
@@ -787,25 +901,32 @@ 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`
+[[cmdhelp_cluster_add,Add a new node to the cluster]]
+==== `add`
 
-Starts the cluster-related system services on this node.
+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:
-.........
-start
-.........
+...............
+add <node>
+...............
 
-[[cmdhelp_cluster_stop,Stop cluster services]]
-==== `stop`
+[[cmdhelp_cluster_health,Cluster health check]]
+==== `health`
 
-Stops the cluster-related system services on this node.
+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:
-.........
-stop
-.........
+...............
+health
+...............
 
 [[cmdhelp_cluster_init,Initializes a new HA cluster]]
 ==== `init`
@@ -818,32 +939,60 @@ init node1 node2 node3
 init --dry-run node1 node2 node3
 ........
 
-[[cmdhelp_cluster_add,Add a new node to the cluster]]
-==== `add`
+[[cmdhelp_cluster_remove,Remove a node from the cluster]]
+==== `remove`
 
-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 simplifies the process of removing a node from the
+cluster, moving any resources hosted by that node to other nodes.
 
-This command should be executed from a node already in the cluster.
+Usage:
+...............
+remove <node>
+...............
+
+[[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:
 ...............
-add <node>
+run <command>
 ...............
 
-[[cmdhelp_cluster_remove,Remove a node from the cluster]]
-==== `remove`
+Example:
+...............
+run "cat /proc/uptime"
+...............
 
-This command simplifies the process of removing a node from the
-cluster, moving any resources hosted by that node to other nodes.
+[[cmdhelp_cluster_copy,Copy file to other cluster nodes]]
+==== `copy`
+
+Copy file to other cluster nodes.
+
+Copies the given file to all other nodes unless given a
+list of nodes to copy to as argument.
 
 Usage:
 ...............
-remove <node>
+copy <filename> [nodes ...]
+...............
+
+Example:
 ...............
+copy /etc/motd
+...............
+
+[[cmdhelp_cluster_start,Start cluster services]]
+==== `start`
+
+Starts the cluster-related system services on this node.
+
+Usage:
+.........
+start
+.........
 
 [[cmdhelp_cluster_status,Cluster status check]]
 ==== `status`
@@ -856,16 +1005,15 @@ Usage:
 status
 ...............
 
-[[cmdhelp_cluster_health,Cluster health check]]
-==== `health`
+[[cmdhelp_cluster_stop,Stop cluster services]]
+==== `stop`
 
-Runs a larger set of tests and queries on all nodes in the cluster to
-verify the general system health and detect potential problems.
+Stops the cluster-related system services on this node.
 
 Usage:
-...............
-health
-...............
+.........
+stop
+.........
 
 [[cmdhelp_cluster_wait_for_startup,Wait for cluster to start]]
 ==== `wait_for_startup`
@@ -881,86 +1029,117 @@ Usage:
 wait_for_startup
 ........
 
-[[cmdhelp_cluster_run,Execute an arbitrary command on all nodes]]
-==== `run`
+[[cmdhelp_cluster_diff,Diff file across cluster]]
+==== `diff`
 
-This command takes a shell statement as argument, executes that
-statement on all nodes in the cluster, and reports the result.
+Displays the difference, if any, between a given file
+on different nodes. If the second argument is `--checksum`,
+a checksum of the file will be calculated and displayed for
+each node.
 
 Usage:
 ...............
-run <command>
+diff <file> [--checksum] [nodes...]
 ...............
 
 Example:
 ...............
-run "cat /proc/uptime"
+diff /etc/crm/crm.conf node2
+diff /etc/resolv.conf --checksum
 ...............
 
 [[cmdhelp_script,Cluster script management]]
-=== `script`
+=== `script` - Cluster script management
+
+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.
 
-Cluster scripts can perform cluster-wide configuration,
-validation and management. See the `list` command for
-an overview of available scripts.
+Scripts are implemented using the python `parallax` 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.
 
 [[cmdhelp_script_list,List available scripts]]
 ==== `list`
 
-Lists the available cluster scripts.
+Lists the available scripts, sorted by category. Scripts that have the
+special `Script` category are hidden by default, since they are mainly
+used by other scripts or commands. To also show these, pass `all` as
+argument.
+
+To get a flat list of script names, not sorted by category, pass
+`names` as an extra argument.
 
 Usage:
 ............
-list
+list [all] [names]
 ............
 
-[[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:
+Example:
 ............
-verify <script>
+list
+list all names
 ............
 
-[[cmdhelp_script_describe,Describe the cluster script]]
-==== `describe`
+[[cmdhelp_script_show,Describe the script]]
+==== `show`
+
+Prints a description and short summary of the script, with
+descriptions of the accepted parameters.
 
-Prints a description and short summary of the cluster script, with
-descriptions of all parameters, both required and optional.
+Advanced parameters are hidden by default. To show the complete list
+of parameters accepted by the script, pass `all` as argument.
 
 Usage:
 ............
-describe <script>
+show <script> [all]
 ............
 
-[[cmdhelp_script_steps,List steps in cluster script]]
-==== `steps`
+Example:
+............
+show virtual-ip
+............
 
-List the names of all steps in the cluster script.
+[[cmdhelp_script_verify,Verify the script]]
+==== `verify`
 
-This command is intended for use by automated tools
-and the web frontend.
+Checks the given parameter values, and returns a list
+of actions that will be executed when running the script
+if provided the same list of parameter values.
 
 Usage:
 ............
-steps <script>
+verify <script> [args...]
 ............
 
+Example:
+............
+verify sbd id=sbd-1 node=node1 sbd_device=/dev/disk/by-uuid/F00D-CAFE
+............
 
-[[cmdhelp_script_run,Execute the cluster script]]
+[[cmdhelp_script_run,Run the script]]
 ==== `run`
 
-Runs a cluster script. Can optionally take at least two arguments:
+Given a list of parameter values, this command will execute the
+actions specified by the cluster script. The format for the parameter
+values is the same as for the `verify` command.
+
+Can optionally take at least two parameters:
 * `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.
+Additional parameters may be available depending on the script.
+
+Use the `show` command to see what parameters are available.
 
 Usage:
 .............
@@ -969,34 +1148,96 @@ run <script> [args...]
 
 Example:
 .............
-run health dry_run=yes verbose=yes
-run init nodes="node-1 node-2 node-3"
+run apache install=true
+run sbd id=sbd-1 node=node1 sbd_device=/dev/disk/by-uuid/F00D-CAFE
 .............
 
+[[cmdhelp_script_json,JSON API for cluster scripts]]
+==== `json`
+
+This command provides a JSON API for the cluster scripts, intended for
+use in user interface tools that want to interact with the cluster via
+scripts.
+
+The command takes a single argument, which should be a JSON array with
+the first member identifying the command to perform.
+
+The output is line-based: Commands that return multiple results will
+return them line-by-line, ending with a terminator value: "end".
+
+When providing parameter values to this command, they should be
+provided as nested objects, so +virtual-ip:ip=192.168.0.5+ on the
+command line becomes the JSON object
++{"virtual-ip":{"ip":"192.168.0.5"}}+.
+
+API:
+........
+["list"]
+=> [{name, shortdesc, category}]
+
+["show", <name>]
+=> [{name, shortdesc, longdesc, category, <<steps>>}]
+
+<<steps>> := [{name, shortdesc], longdesc, required, parameters, steps}]
+
+<<params>> := [{name, shortdesc, longdesc, required, unique, advanced,
+                type, value, example}]
+
+["verify", <name>, <<values>>]
+=> [{shortdesc, longdesc, text, nodes}]
+
+["run", <name>, <<values>>]
+=> [{shortdesc, rc, output|error}]
+........
+
+
 [[cmdhelp_corosync,Corosync management]]
-=== `corosync`
+=== `corosync` - Corosync management
 
 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`
+[[cmdhelp_corosync_add-node,Add a corosync node]]
+==== `add-node`
 
-Displays the status of Corosync, including the votequorum state.
+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:
 .........
-status
+add-node <addr>
 .........
 
-[[cmdhelp_corosync_show,Display the corosync configuration]]
-==== `show`
+[[cmdhelp_corosync_del-node,Remove a corosync node]]
+==== `del-node`
 
-Displays the corosync configuration on the current node.
+Removes a node from the corosync configuration. The argument given is
+the `ring0_addr` address set in the configuration file.
 
+Usage:
 .........
-show
+del-node <addr>
+.........
+
+[[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 display a checksum
+for each file instead of calculating a diff.
+
+Usage:
+.........
+diff [--checksum] [node...]
 .........
 
 [[cmdhelp_corosync_edit,Edit the corosync configuration]]
@@ -1009,6 +1250,24 @@ Usage:
 edit
 .........
 
+[[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_log,Show the corosync log file]]
 ==== `log`
 
@@ -1023,18 +1282,15 @@ Usage:
 log
 .........
 
-[[cmdhelp_corosync_reload,Reload the corosync configuration]]
-==== `reload`
+[[cmdhelp_corosync_pull,Pulls the corosync configuration]]
+==== `pull`
 
-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.
+Gets the corosync configuration from another node and copies
+it to this node.
 
 Usage:
 .........
-reload
+pull <node>
 .........
 
 [[cmdhelp_corosync_push,Push the corosync configuration]]
@@ -1057,93 +1313,53 @@ 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.
+[[cmdhelp_corosync_reload,Reload the corosync configuration]]
+==== `reload`
 
-`diff` takes an option argument `--checksum`, to force checksum mode.
+Tells all instances of corosync in this cluster to reload
+`corosync.conf`.
 
-If the number of nodes to compare are greater than two, `diff`
-automatically switches to checksum mode.
+After pushing a new configuration to all cluster nodes, call this
+command to make corosync use the new configuration.
 
 Usage:
 .........
-diff [--checksum] [node...]
+reload
 .........
 
-[[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.
+[[cmdhelp_corosync_set,Set a corosync configuration value]]
+==== `set`
 
-Note that this command assumes that only a single ring is used, and
-sets only the address for ring0.
+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:
 .........
-add-node <addr>
+set quorum.expected_votes 2
 .........
 
-[[cmdhelp_corosync_del-node,Remove a corosync node]]
-==== `del-node`
+[[cmdhelp_corosync_show,Display the corosync configuration]]
+==== `show`
 
-Removes a node from the corosync configuration. The argument given is
-the `ring0_addr` address set in the configuration file.
+Displays the corosync configuration on the current node.
 
-Usage:
 .........
-del-node <addr>
+show
 .........
 
-[[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`
+[[cmdhelp_corosync_status,Display the corosync status]]
+==== `status`
 
-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.
+Displays the status of Corosync, including the votequorum state.
 
 Usage:
 .........
-set quorum.expected_votes 2
+status
 .........
 
 [[cmdhelp_cib,CIB shadow management]]
-=== `cib` (shadow CIBs)
+=== `cib` - CIB shadow management
 
 This level is for management of shadow CIBs. It is available both
 at the top level and the `configure` level.
@@ -1152,51 +1368,11 @@ 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.
+[[cmdhelp_cib_cibstatus,CIB status management and editing]]
+==== `cibstatus`
 
-Usage:
-...............
-reset <cib>
-...............
+Enter edit and manage the CIB status section level. See the
+<<cmdhelp_cibstatus,CIB status management section>>.
 
 [[cmdhelp_cib_commit,copy a shadow CIB to the cluster]]
 ==== `commit`
@@ -1211,16 +1387,14 @@ Usage:
 commit [<cib>]
 ...............
 
-[[cmdhelp_cib_use,change working CIB]]
-==== `use`
+[[cmdhelp_cib_delete,delete a shadow CIB]]
+==== `delete`
 
-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.
+Delete an existing shadow CIB.
 
 Usage:
 ...............
-use [<cib>] [withstatus]
+delete <cib>
 ...............
 
 [[cmdhelp_cib_diff,diff between the shadow CIB and the live CIB]]
@@ -1234,16 +1408,6 @@ 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`
 
@@ -1271,14 +1435,66 @@ import pe-warn-2222
 import 2289 issue2
 ...............
 
-[[cmdhelp_cib_cibstatus,CIB status management and editing]]
-==== `cibstatus`
+[[cmdhelp_cib_list,list all shadow CIBs]]
+==== `list`
 
-Enter edit and manage the CIB status section level. See the
-<<cmdhelp_cibstatus,CIB status management section>>.
+List existing shadow CIBs.
+
+Usage:
+...............
+list
+...............
+
+[[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_reset,copy live cib to a shadow CIB]]
+==== `reset`
+
+Copy the current cluster configuration into the shadow CIB.
+
+Usage:
+...............
+reset <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_ra,Resource Agents (RA) lists and documentation]]
-=== `ra`
+=== `ra` - Resource Agents (RA) lists and documentation
 
 This level contains commands which show various information about
 the installed resource agents. It is available both at the top
@@ -1295,22 +1511,6 @@ 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`)
 
@@ -1333,6 +1533,22 @@ info stonith:ipmilan
 info pengine
 ...............
 
+[[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_providers,show providers for a RA and a class]]
 ==== `providers`
 
@@ -1348,101 +1564,81 @@ Example:
 providers apache
 ...............
 
+[[cmdhelp_ra_validate,validate parameters for RA]]
+==== `validate`
+
+If the resource agent supports the `validate-all` action, this calls
+the action with the given parameters, printing any warnings or errors
+reported by the agent.
+
+Usage:
+................
+validate <agent> [<key>=<value> ...]
+................
+
 [[cmdhelp_resource,Resource management]]
-=== `resource`
+=== `resource` - Resource management
 
 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`)
+[[cmdhelp_resource_cleanup,cleanup resource status]]
+==== `cleanup`
 
-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`>>.
+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:
 ...............
-start <rsc>
+cleanup <rsc> [<node>]
 ...............
 
-[[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.
+[[cmdhelp_resource_demote,demote a master-slave resource]]
+==== `demote`
 
-For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+Demote a master-slave resource using the `target-role`
+attribute.
 
 Usage:
 ...............
-stop <rsc>
+demote <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.
+[[cmdhelp_resource_failcount,manage failcounts]]
+==== `failcount`
 
-For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+Show/edit/delete the failcount of a resource.
 
 Usage:
 ...............
-restart <rsc>
+failcount <rsc> set <node> <value>
+failcount <rsc> delete <node>
+failcount <rsc> show <node>
 ...............
 Example:
 ...............
-# crm resource restart g_webserver
-INFO: ordering g_webserver to stop
-waiting for stop to finish .... done
-INFO: ordering g_webserver to start
-#
+failcount fs_0 delete node2
 ...............
 
-[[cmdhelp_resource_promote,promote a master-slave resource]]
-==== `promote`
+[[cmdhelp_resource_maintenance,Enable/disable per-resource maintenance mode]]
+==== `maintenance`
 
-Promote a master-slave resource using the `target-role`
-attribute.
+Enables or disables the per-resource maintenance mode. When this mode
+is enabled, no monitor operations will be triggered for the resource.
 
 Usage:
-...............
-promote <rsc>
-...............
-
-[[cmdhelp_resource_demote,demote a master-slave resource]]
-==== `demote`
-
-Demote a master-slave resource using the `target-role`
-attribute.
+..................
+maintenance <resource> [on|off|true|false]
+..................
 
-Usage:
-...............
-demote <rsc>
-...............
+Example:
+..................
+maintenance rsc1
+maintenance rsc2 off
+..................
 
 [[cmdhelp_resource_manage,put a resource into managed mode]]
 ==== `manage`
@@ -1459,19 +1655,22 @@ 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.
+[[cmdhelp_resource_meta,manage a meta attribute]]
+==== `meta`
 
-For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
+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:
 ...............
-unmanage <rsc>
+meta <rsc> set <attr> <value>
+meta <rsc> delete <attr>
+meta <rsc> show <attr>
+...............
+Example:
+...............
+meta ip_0 set target-role stopped
 ...............
 
 [[cmdhelp_resource_migrate,migrate a resource to another node]]
@@ -1488,10 +1687,51 @@ Usage:
 migrate <rsc> [<node>] [<lifetime>] [force]
 ...............
 
-[[cmdhelp_resource_unmigrate,unmigrate a resource to another node]]
-==== `unmigrate` (`unmove`)
+[[cmdhelp_resource_ban,ban a resource from a node]]
+==== `ban`
 
-Remove the constraint generated by the previous migrate command.
+Ban a resource from running on a certain node. If no node is given
+as argument, the resource is banned from the current location.
+
+See `migrate` for details on other arguments.
+
+Usage:
+...............
+ban <rsc> [<node>] [<lifetime>] [force]
+...............
+
+
+[[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_promote,promote a master-slave resource]]
+==== `promote`
+
+Promote a master-slave resource using the `target-role`
+attribute.
+
+Usage:
+...............
+promote <rsc>
+...............
+
+[[cmdhelp_resource_refresh,refresh CIB from the LRM status]]
+==== `refresh`
+
+Refresh CIB from the LRM status.
 
 .Note
 ****************************
@@ -1501,14 +1741,13 @@ an alias for `cleanup`.
 
 Usage:
 ...............
-unmigrate <rsc>
+refresh [<node>]
 ...............
 
-[[cmdhelp_resource_maintenance,Enable/disable per-resource maintenance mode]]
-==== `maintenance`
+[[cmdhelp_resource_reprobe,probe for resources not started by the CRM]]
+==== `reprobe`
 
-Enables or disables the per-resource maintenance mode. When this mode
-is enabled, no monitor operations will be triggered for the resource.
+Probe for resources not started by the CRM.
 
 .Note
 ****************************
@@ -1517,32 +1756,67 @@ an alias for `cleanup`.
 ****************************
 
 Usage:
-..................
-maintenance <resource> [on|off|true|false]
-..................
+...............
+reprobe [<node>]
+...............
 
-Example:
-..................
-maintenance rsc1
-maintenance rsc2 off
-..................
+[[cmdhelp_resource_restart,restart resources]]
+==== `restart`
 
-[[cmdhelp_resource_param,manage a parameter of a resource]]
-==== `param`
+Restart one or more resources. 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.
 
-Show/edit/delete a parameter of a resource.
+For details on group management see
+<<cmdhelp_options_manage-children,`options manage-children`>>.
 
 Usage:
 ...............
-param <rsc> set <param> <value>
-param <rsc> delete <param>
-param <rsc> show <param>
+restart <rsc> [<rsc> ...]
 ...............
 Example:
 ...............
-param ip_0 show ip
+# 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_constraints,Show constraints affecting a resource]]
+==== `constraints`
+
+Display the location and colocation constraints affecting the
+resource.
+
+Usage:
+................
+constraints <rsc>
+................
+
+[[cmdhelp_resource_operations,Show active resource operations]]
+==== `operations`
+
+Show active operations, optionally filtered by resource and node.
+
+Usage:
+................
+operations [<rsc>] [<node>]
+................
+
+[[cmdhelp_resource_scores,Display resource scores]]
+==== `scores`
+
+Display the allocation scores for all resources.
+
+Usage:
+................
+scores
+................
+
 [[cmdhelp_resource_secret,manage sensitive parameters]]
 ==== `secret`
 
@@ -1571,118 +1845,101 @@ secret fence_1 stash password
 secret fence_1 set password secret_value
 ...............
 
-[[cmdhelp_resource_meta,manage a meta attribute]]
-==== `meta`
+[[cmdhelp_resource_start,start resources]]
+==== `start`
 
-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`.
+Start one or more resources 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:
 ...............
-meta <rsc> set <attr> <value>
-meta <rsc> delete <attr>
-meta <rsc> show <attr>
-...............
-Example:
-...............
-meta ip_0 set target-role stopped
+start <rsc> [<rsc> ...]
 ...............
 
-[[cmdhelp_resource_utilization,manage a utilization attribute]]
-==== `utilization`
+[[cmdhelp_resource_status,show status of resources]]
+==== `status` (`show`, `list`)
 
-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>>.
+Print resource status. More than one resource can be shown at once. If
+the resource parameter is left out, the status of all resources is
+printed.
 
 Usage:
 ...............
-utilization <rsc> set <attr> <value>
-utilization <rsc> delete <attr>
-utilization <rsc> show <attr>
+status [<rsc> ...]
 ...............
-Example:
+
+[[cmdhelp_resource_stop,stop resources]]
+==== `stop`
+
+Stop one or more resources 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:
 ...............
-utilization xen1 set memory 4096
+stop <rsc> [<rsc> ...]
 ...............
 
-[[cmdhelp_resource_failcount,manage failcounts]]
-==== `failcount`
+[[cmdhelp_resource_trace,start RA tracing]]
+==== `trace`
 
-Show/edit/delete the failcount of a resource.
+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:
 ...............
-failcount <rsc> set <node> <value>
-failcount <rsc> delete <node>
-failcount <rsc> show <node>
+trace <rsc> [<op> [<interval>] ]
 ...............
 Example:
 ...............
-failcount fs_0 delete node2
+trace fs start
+trace webserver
+trace webserver probe
+trace fs monitor 0
 ...............
 
-[[cmdhelp_resource_cleanup,cleanup resource status]]
-==== `cleanup`
+[[cmdhelp_resource_unmanage,put a resource into unmanaged mode]]
+==== `unmanage`
 
-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`
+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.
 
-Probe for resources not started by the CRM.
+For details on group management see <<cmdhelp_options_manage-children,`options manage-children`>>.
 
 Usage:
 ...............
-reprobe [<node>]
+unmanage <rsc>
 ...............
 
-[[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.
+[[cmdhelp_resource_unmigrate,unmigrate a resource to another node]]
+==== `unmigrate` (`unmove`)
 
-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.
+Remove the constraint generated by the previous migrate command.
 
 Usage:
 ...............
-trace <rsc> [<op> [<interval>] ]
-...............
-Example:
-...............
-trace fs start
-trace webserver
-trace webserver probe
-trace fs monitor 0
+unmigrate <rsc>
 ...............
 
 [[cmdhelp_resource_untrace,stop RA tracing]]
@@ -1701,77 +1958,91 @@ untrace fs start
 untrace webserver
 ...............
 
-[[cmdhelp_resource_scores,Display resource scores]]
-=== `scores`
+[[cmdhelp_resource_utilization,manage a utilization attribute]]
+==== `utilization`
 
-Display the allocation scores for all resources.
+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:
-................
-scores
-................
+...............
+utilization <rsc> set <attr> <value>
+utilization <rsc> delete <attr>
+utilization <rsc> show <attr>
+...............
+Example:
+...............
+utilization xen1 set memory 4096
+...............
 
-[[cmdhelp_node,Nodes management]]
-=== `node`
+[[cmdhelp_node,Node management]]
+=== `node` - Node management
 
 Node management and status commands.
 
-[[cmdhelp_node_status,show nodes' status as XML]]
-==== `status`
+[[cmdhelp_node_attribute,manage attributes]]
+==== `attribute`
 
-Show nodes' status as XML. If the node parameter is omitted then
-all nodes are shown.
+Edit node attributes. This kind of attribute should refer to
+relatively static properties, such as memory size.
 
 Usage:
 ...............
-status [<node>]
+attribute <node> set <attr> <value>
+attribute <node> delete <attr>
+attribute <node> show <attr>
+...............
+Example:
+...............
+attribute node_1 set memory_size 4096
 ...............
 
-[[cmdhelp_node_show,show node]]
-==== `show`
+[[cmdhelp_node_clearstate,Clear node state]]
+==== `clearstate`
 
-Show a node definition. If the node parameter is omitted then all
-nodes are shown.
+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:
 ...............
-show [<node>]
+clearstate <node>
 ...............
 
-[[cmdhelp_node_standby,put node into standby]]
-==== `standby`
+[[cmdhelp_node_delete,delete node]]
+==== `delete`
 
-Set a node to standby status. The node parameter defaults to the
-node where the command is run.
+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.
 
-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`.
+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:
 ...............
-standby [<node>] [<lifetime>]
-
-lifetime :: reboot | forever
-...............
-
-Example:
-...............
-standby bob reboot
+delete <node>
 ...............
 
+[[cmdhelp_node_fence,fence node]]
+==== `fence`
 
-[[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.
+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:
 ...............
-online [<node>]
+fence <node>
 ...............
 
 [[cmdhelp_node_maintenance,put node into maintenance mode]]
@@ -1788,6 +2059,18 @@ Usage:
 maintenance [<node>]
 ...............
 
+[[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_ready,put node into ready mode]]
 ==== `ready`
 
@@ -1802,66 +2085,68 @@ Usage:
 ready [<node>]
 ...............
 
-[[cmdhelp_node_fence,fence node]]
-==== `fence`
+[[cmdhelp_node_show,show node]]
+==== `show`
 
-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.
+Show a node definition. If the node parameter is omitted then all
+nodes are shown.
 
 Usage:
 ...............
-fence <node>
+show [<node>]
 ...............
 
-[[cmdhelp_node_clearstate,Clear node state]]
-==== `clearstate`
+[[cmdhelp_node_standby,put node into standby]]
+==== `standby`
 
-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).
+Set a node to standby status. The node parameter defaults to the
+node where the command is run.
 
-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.
+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:
 ...............
-clearstate <node>
+standby [<node>] [<lifetime>]
+
+lifetime :: reboot | forever
 ...............
 
-[[cmdhelp_node_delete,delete node]]
-==== `delete`
+Example:
+...............
+standby bob reboot
+...............
 
-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.
+[[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:
 ...............
-delete <node>
+status [<node>]
 ...............
 
-[[cmdhelp_node_attribute,manage attributes]]
-==== `attribute`
+[[cmdhelp_node_status-attr,manage status attributes]]
+==== `status-attr`
 
-Edit node attributes. This kind of attribute should refer to
-relatively static properties, such as memory size.
+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:
 ...............
-attribute <node> set <attr> <value>
-attribute <node> delete <attr>
-attribute <node> show <attr>
+status-attr <node> set <attr> <value>
+status-attr <node> delete <attr>
+status-attr <node> show <attr>
 ...............
 Example:
 ...............
-attribute node_1 set memory_size 4096
+status-attr node_1 show pingd
 ...............
 
 [[cmdhelp_node_utilization,manage utilization attributes]]
@@ -1886,26 +2171,8 @@ 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`
+[[cmdhelp_site,GEO clustering site support]]
+=== `site` - GEO clustering site support
 
 A cluster may consist of two or more subclusters in different and
 distant locations. This set of commands supports such setups.
@@ -1929,125 +2196,60 @@ Example:
 ticket grant ticket1
 ...............
 
-[[cmdhelp_options,user preferences]]
-=== `options`
+[[cmdhelp_options,User preferences]]
+=== `options` - User preferences
 
 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.
+[[cmdhelp_options_add-quotes,add quotes around parameters containing spaces]]
+==== `add-quotes`
 
-Usage:
-...............
-skill-level <level>
-
-level :: operator | administrator | expert
-...............
+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 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).
+.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`.
 
-[[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:
+For instance, with adding quotes enabled, it is possible to do
+the following:
 ...............
-editor vim
+# crm configure primitive d1 Dummy \
+    meta description="some description here"
+# crm configure filter 'sed "s/hostlist=./&node-c /"' fencing
 ...............
+****************************
 
-[[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`.
+[[cmdhelp_options_check-frequency,when to perform semantic check]]
+==== `check-frequency`
 
-Usage:
-...............
-sort-elements {yes|no}
-...............
-Example:
-...............
-sort-elements no
-...............
+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`.
 
-[[cmdhelp_options_wait,synchronous operation]]
-==== `wait`
+The checks require that resource agents are present. If they are
+not installed at the configuration time set this preference to
+`never`.
 
-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.
+See <<topics_Features_Checks,Configuration semantic checks>> for more details.
 
-Usage:
-...............
-wait {yes|no}
-...............
-Example:
-...............
-wait yes
-...............
+[[cmdhelp_options_check-mode,how to treat semantic errors]]
+==== `check-mode`
 
-[[cmdhelp_options_output,set output type]]
-==== `output`
+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`.
 
-`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`.
+See <<topics_Features_Checks,Configuration semantic checks>> for more details.
 
 [[cmdhelp_options_colorscheme,set colors for output]]
 ==== `colorscheme`
@@ -2078,55 +2280,22 @@ 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`.
+[[cmdhelp_options_editor,set preferred editor program]]
+==== `editor`
 
-.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`.
+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`).
 
-For instance, with adding quotes enabled, it is possible to do
-the following:
+Usage:
 ...............
-# crm configure primitive d1 Dummy \
-    meta description="some description here"
-# crm configure filter 'sed "s/hostlist=./&node-c /"' fencing
+editor program
+...............
+Example:
+...............
+editor vim
 ...............
-****************************
 
 [[cmdhelp_options_manage-children,how to handle children resource attributes]]
 ==== `manage-children`
@@ -2171,27 +2340,39 @@ 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`
+[[cmdhelp_options_output,set output type]]
+==== `output`
 
-Display all current settings.
+`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-always`, `color`,
+and 'uppercase'. It is possible to combine `uppercase` with one
+of the color values in order to get an upper case xmass tree. Just
+set this option to `color,uppercase` or `color-always,uppercase`.
+In case you need color codes in pipes, `color-always` forces color
+codes even in case the terminal is not a tty (just like `ls
+--color=always`).
 
-Given an option name as argument, `show` will display only the value
-of that argument.
+[[cmdhelp_options_pager,set preferred pager program]]
+==== `pager`
 
-Given +all+ as argument, `show` displays all available user options.
+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`).
 
-Usage:
-........
-show [all|<option>]
-........
+[[cmdhelp_options_reset,reset user preferences to factory defaults]]
+==== `reset`
 
-Example:
-........
-show
-show skill-level
-show all
-........
+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_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_set,Set the value of a given option]]
 ==== `set`
@@ -2213,37 +2394,126 @@ 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`
+[[cmdhelp_options_show,show current user preference]]
+==== `show`
 
-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.
+Display all current settings.
 
-[[cmdhelp_configure,CIB configuration]]
-=== `configure`
+Given an option name as argument, `show` will display only the value
+of that argument.
 
-This level enables all CIB object definition commands.
+Given +all+ as argument, `show` displays all available user options.
 
-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.
+Usage:
+........
+show [all|<option>]
+........
 
-Nodes and attributes describing nodes are managed using the
-`node` command.
+Example:
+........
+show
+show skill-level
+show all
+........
 
-Commands for resources are:
+[[cmdhelp_options_skill-level,set skill level]]
+==== `skill-level`
 
-- `primitive`
-- `monitor`
-- `group`
+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_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_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_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_configure,CIB configuration]]
+=== `configure` - CIB configuration
+
+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)
 
@@ -2295,205 +2565,358 @@ 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`
+[[cmdhelp_configure_acl_target,Define target access rights]]
+==== `acl_target`
 
-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.
+Defines an ACL target.
 
 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
-...............
+................
+acl_target <tid> [<role> ...]
+................
 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.
+................
+acl_target joe resource_admin constraint_editor
+................
 
-* 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.
+[[cmdhelp_configure_cib,CIB shadow management]]
+==== `cib`
 
-Operation attributes which are not recognized are saved as
-instance attributes of that operation. A typical example is
-+OCF_CHECK_LEVEL+.
+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.
 
-For multistate resources, roles are specified as +role=<role>+.
+[[cmdhelp_configure_cibstatus,CIB status management and editing]]
+==== `cibstatus`
 
-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.
+Enter edit and manage the CIB status section level. See the
+<<cmdhelp_cibstatus,CIB status management section>>.
 
-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:
+[[cmdhelp_configure_clone,define a clone]]
+==== `clone`
 
-* +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
+The `clone` command creates a resource clone. It may contain a
+single primitive resource or one group of resources.
 
 Usage:
 ...............
-primitive <rsc> {[<class>:[<provider>:]]<type>|@<template>}
+clone <name> <rsc>
   [description=<description>]
-  [params attr_list]
-  [meta attr_list]
-  [utilization attr_list]
-  [operations id_spec]
-    [op op_type [<attribute>=<value>...] ...]
+  [meta <attr_list>]
+  [params <attr_list>]
 
-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
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
 ...............
 Example:
 ...............
-primitive apcfence stonith:apcsmart \
-  params ttydev=/dev/ttyS0 hostlist="node1 node2" \
-  op start timeout=60s \
-  op monitor interval=30m timeout=60s
+clone cl_fence apc_1 \
+  meta clone-node-max=1 globally-unique=false
+...............
 
-primitive www8 apache \
-  params configfile=/etc/apache/www8.conf \
-  operations $id-ref=apache_ops
+[[cmdhelp_configure_colocation,colocate resources]]
+==== `colocation` (`collocation`)
 
-primitive db0 mysql \
-  params config=/etc/mysql/db0.conf \
-  op monitor interval=60s \
-  op monitor interval=300s OCF_CHECK_LEVEL=10
+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.
 
-primitive r0 ocf:linbit:drbd \
-  params drbd_resource=r0 \
-  op monitor role=Master interval=60s \
-  op monitor role=Slave interval=300s
+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.
 
-primitive xen0 @vm_scheme1 \
-  params xmfile=/etc/xen/vm/xen0
+In the two resource form, the cluster will place +<with-rsc>+ first,
+and then decide where to put the +<rsc>+ resource.
 
-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
+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.
 
-[[cmdhelp_configure_monitor,add monitor operation to a primitive]]
-==== `monitor`
+The optional +node-attribute+ can be used to colocate resources on a
+set of nodes and not necessarily on the same node. For example, by
+setting a node attribute +color+ on all nodes and setting the
++node-attribute+ value to +color+ as well, the colocated resources
+will be placed on any node that has the same color.
 
-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.
+For more details on how to configure resource sets, see
+<<topics_Features_Resourcesets,`Syntax: Resource sets`>>.
 
 Usage:
 ...............
-monitor <rsc>[:<role>] <interval>[:<timeout>]
+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:
 ...............
-monitor apcfence 60m:60s
+colocation never_put_apache_with_dummy -inf: apache dummy
+colocation c1 inf: A ( B C )
 ...............
 
-Note that after executing the command, the monitor operation may
-be shown as part of the primitive definition.
+[[cmdhelp_configure_commit,commit the changes to the CIB]]
+==== `commit`
 
-[[cmdhelp_configure_group,define a group]]
-==== `group`
+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.
 
-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.
+If you know that it's fine to still apply them, add +force+ to the
+command line.
 
-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.
+To disable CIB patching and apply the changes by replacing the CIB
+completely, add +replace+ to the command line. Note that this can lead
+to previous changes being overwritten if some other process
+concurrently modifies the CIB.
 
-`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:
+...............
+commit [force] [replace]
+...............
+
+[[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:
 ...............
-group <name> <rsc> [<rsc>...]
-  [description=<description>]
-  [meta attr_list]
-  [params attr_list]
+default-timeouts <id> [<id>...]
+...............
 
-attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+.Note on `default-timeouts`
+****************************
+The use of this command is discouraged in favor of manually
+determining the best timeouts required for the particular
+configuration. Relying on the resource agent to supply appropriate
+timeouts can cause the resource to fail at the worst possible moment.
+
+Appropriate timeouts for resource actions are context-sensitive, and
+should be carefully considered with the whole configuration in mind.
+****************************
+
+[[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_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_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_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.
+
+From Pacemaker version 1.1.14, it is possible to use a node attribute
+as the +target+ in a fencing topology. The syntax for this usage is
+described below.
+
+Usage:
+...............
+fencing_topology <stonith_resources> [<stonith_resources> ...]
+fencing_topology <fencing_order> [<fencing_order> ...]
+
+fencing_order :: <target> <stonith_resources> [<stonith_resources> ...]
+
+stonith_resources :: <rsc>[,<rsc>...]
+target :: <node>: | attr:<node-attribute>=<value>
 ...............
 Example:
 ...............
-group internal_www disk0 fs0 internal_ip apache \
-  meta target_role=stopped
+# Only kill the power if poison-pill fails
+fencing_topology poison-pill power
 
-group vm-and-services vm vm-sshd meta container="vm"
+# As above for node-a, but a different strategy for node-b
+fencing_topology \
+    node-a: poison-pill power \
+    node-b: ipmi serial
+
+# Fencing anything on rack 1 requires fencing via both APC 1 and 2,
+# to defeat the redundancy provided by two separate UPS units.
+fencing_topology attr:rack=1 apc01,apc02
 ...............
 
-[[cmdhelp_configure_clone,define a clone]]
-==== `clone`
+[[cmdhelp_configure_filter,filter CIB objects]]
+==== `filter`
 
-The `clone` command creates a resource clone. It may contain a
-single primitive resource or one group of resources.
+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:
 ...............
-clone <name> <rsc>
-  [description=<description>]
-  [meta attr_list]
-  [params attr_list]
+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
+...............
 
-attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+.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_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:
 ...............
-clone cl_fence apc_1 \
-  meta clone-node-max=1 globally-unique=false
+graph dot
+graph dot clu1.conf.dot
+graph dot clu1.conf.svg svg
 ...............
 
-[[cmdhelp_configure_ms,define a master-slave resource]]
-==== `ms` (`master`)
+[[cmdhelp_configure_group,define a group]]
+==== `group`
 
-The `ms` command creates a master/slave resource type. It may contain a
-single primitive resource or one group of resources.
+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:
 ...............
-ms <name> <rsc>
+group <name> <rsc> [<rsc>...]
   [description=<description>]
   [meta attr_list]
   [params attr_list]
@@ -2502,63 +2925,33 @@ 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:
+group internal_www disk0 fs0 internal_ip apache \
+  meta target_role=stopped
 
+group vm-and-services vm vm-sshd meta container="vm"
 ...............
-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`
+[[cmdhelp_configure_load,import the CIB from a file]]
+==== `load`
 
-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.
+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.
+
+If the URL is `-`, the configuration is read from standard input.
 
 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>...] ...]
+load [xml] <method> URL
 
-attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
-id_spec :: $id=<id> | $id-ref=<id>
-op_type :: start | stop | monitor
+method :: replace | update
 ...............
 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
+load xml update myfirstcib.xml
+load xml replace http://storage.big.com/cibs/bigcib.xml
 ...............
 
 [[cmdhelp_configure_location,a location preference]]
@@ -2576,19 +2969,28 @@ following:
 * 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`>>.
+The +resource-discovery+ attribute allows probes to be selectively
+enabled or disabled per resource and node.
+
+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`>>.
 
+For more information on rule expressions, see
+<<topics_Syntax_RuleExpressions,Syntax: Rule expressions>>.
+
 Usage:
 ...............
-location <id> rsc [role=<role>] {node_pref|rules}
+location <id> <rsc> [<attributes>] {<node_pref>|<rules>}
 
 rsc :: /<rsc-pattern>/
         | { resource_sets }
         | <rsc>
 
+attributes :: role=<role> | resource-discovery=always|never|exclusive
+
 node_pref :: <score>: <node>
 
 rules ::
@@ -2597,7 +2999,7 @@ rules ::
 
 id_spec :: $id=<id> | $id-ref=<id>
 score :: <number> | <attribute> | [-]inf
-expression :: <simple_exp> [bool_op <simple_exp> ...]
+expression :: <simple_exp> [<bool_op> <simple_exp> ...]
 bool_op :: or | and
 simple_exp :: <attribute> [type:]<binary_op> <value>
           | <unary_op> <attribute>
@@ -2632,57 +3034,132 @@ location conn_1 internal_www \
 
 location conn_2 dummy_float \
   rule -inf: not_defined pingd or pingd number:lte 0
+
+# never probe for rsc1 on node1
+location no-probe rsc1 resource-discovery=never -inf: node1
 ...............
 
-[[cmdhelp_configure_colocation,colocate resources]]
-==== `colocation` (`collocation`)
+[[cmdhelp_configure_modgroup,modify group]]
+==== `modgroup`
 
-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.
+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.
 
-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.
+Usage:
+...............
+modgroup <id> add <id> [after <id>|before <id>]
+modgroup <id> remove <id>
+...............
+Examples:
+...............
+modgroup share1 add storage2 before share1-fs
+...............
 
-In the two resource form, the cluster will place +<with-rsc>+ first,
-and then decide where to put the +<rsc>+ resource.
+[[cmdhelp_configure_monitor,add monitor operation to a primitive]]
+==== `monitor`
 
-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.
+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.
 
-Sets cannot be nested.
+Usage:
+...............
+monitor <rsc>[:<role>] <interval>[:<timeout>]
+...............
+Example:
+...............
+monitor apcfence 60m:60s
+...............
 
-The optional +node-attribute+ references an attribute in nodes'
-instance attributes.
+Note that after executing the command, the monitor operation may
+be shown as part of the primitive definition.
 
-For more details on how to configure resource sets, see
-<<topics_Features_Resourcesets,`Syntax: Resource sets`>>.
+[[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:
 ...............
-colocation <id> <score>: <rsc>[:<role>] <with-rsc>[:<role>]
-  [node-attribute=<node_attr>]
+ms <name> <rsc>
+  [description=<description>]
+  [meta attr_list]
+  [params attr_list]
 
-colocation <id> <score>: resource_sets
-  [node-attribute=<node_attr>]
+attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
+...............
+Example:
+...............
+ms disk1 drbd1 \
+  meta notify=true globally-unique=false
+...............
 
-resource_sets :: resource_set [resource_set ...]
+.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:
 
-resource_set :: ["("|"["] <rsc>[:<role>] [<rsc>[:<role>] ...] \
-                [attributes]  [")"|"]"]
+...............
+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_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_op_defaults,set resource operations defaults]]
+==== `op_defaults`
+
+Set defaults for the operations meta attributes.
 
-attributes :: [require-all=(true|false)] [sequential=(true|false)]
+For more information on rule expressions, see
+<<topics_Syntax_RuleExpressions,Syntax: Rule expressions>>.
 
+Usage:
+...............
+op_defaults [$id=<set_id>] [rule ...] <option>=<value> [<option>=<value> ...]
 ...............
 Example:
 ...............
-colocation never_put_apache_with_dummy -inf: apache dummy
-colocation c1 inf: A ( B C )
+op_defaults record-pending=true
 ...............
 
 [[cmdhelp_configure_order,order resources]]
@@ -2738,44 +3215,105 @@ order o-3 inf: [ A B ] C
 order o-4 first-resource then-resource
 ...............
 
-[[cmdhelp_configure_rsc_ticket,resources ticket dependency]]
-==== `rsc_ticket`
+[[cmdhelp_configure_primitive,define a resource]]
+==== `primitive`
 
-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 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.
 
-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.
+Operations may be specified anonymously, as a group or by reference:
 
-See also the <<cmdhelp_site_ticket,`site`>> set of commands.
+* "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:
 ...............
-rsc_ticket <id> <ticket_id>: <rsc>[:<role>] [<rsc>[:<role>] ...]
-  [loss-policy=<loss_policy_action>]
+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>...] ...]
 
-loss_policy_action :: stop | demote | fence | freeze
+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:
 ...............
-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
-...............
+primitive apcfence stonith:apcsmart \
+  params ttydev=/dev/ttyS0 hostlist="node1 node2" \
+  op start timeout=60s \
+  op monitor interval=30m timeout=60s
+
+primitive www8 apache \
+  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 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_property,set a cluster property]]
 ==== `property`
 
-Set the cluster (+crm_config+) options.
+Set cluster configuration properties. To list the
+available cluster configuration properties, use the
+<<cmdhelp_ra_info,`ra info`>> command with +pengine+, +crmd+,
++cib+ and +stonithd+ as arguments.
+
+For more information on rule expressions, see
+<<topics_Syntax_RuleExpressions,Syntax: Rule expressions>>.
 
 Usage:
 ...............
-property [$id=<set_id>] [rule ...] <option>=<value> [<option>=<value> ...]
+property [<set_id>:] [rule ...] <option>=<value> [<option>=<value> ...]
 ...............
 Example:
 ...............
@@ -2783,51 +3321,71 @@ property stonith-enabled=true
 property rule date spec years=2014 stonith-enabled=false
 ...............
 
-[[cmdhelp_configure_rsc_defaults,set resource defaults]]
-==== `rsc_defaults`
+[[cmdhelp_configure_ptest,show cluster actions if changes were committed]]
+==== `ptest` (`simulate`)
 
-Set defaults for the resource meta attributes.
+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:
 ...............
-rsc_defaults [$id=<set_id>] [rule ...] <option>=<value> [<option>=<value> ...]
+ptest [nograph] [v...] [scores] [actions] [utilization]
 ...............
-Example:
+Examples:
 ...............
-rsc_defaults failure-timeout=3m
+ptest scores
+ptest vvvvv
+simulate actions
 ...............
 
-[[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.
+[[cmdhelp_configure_refresh,refresh from CIB]]
+==== `refresh`
 
-If the node is left out, the order is used for all nodes.
-That should reduce the configuration size in some stonith setups.
+Refresh the internal structures from the CIB. All changes made
+during this session are lost.
 
 Usage:
 ...............
-fencing_topology stonith_resources [stonith_resources ...]
-fencing_topology fencing_order [fencing_order ...]
+refresh
+...............
 
-fencing_order :: <node>: stonith_resources [stonith_resources ...]
+[[cmdhelp_configure_rename,rename a CIB object]]
+==== `rename`
 
-stonith_resources :: <rsc>[,<rsc>...]
-...............
-Example:
+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:
 ...............
-fencing_topology poison-pill power
-fencing_topology \
-    node-a: poison-pill power
-    node-b: ipmi serial
+rename <old_id> <new_id>
 ...............
 
 [[cmdhelp_configure_role,define role access rights]]
@@ -2853,6 +3411,9 @@ 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`).
 
+For more information on rule expressions, see
+<<topics_Syntax_RuleExpressions,Syntax: Rule expressions>>.
+
 Usage:
 ...............
 role <role-id> rule [rule ...]
@@ -2884,378 +3445,287 @@ role app1_admin \
     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`:
+[[cmdhelp_configure_rsc_defaults,set resource defaults]]
+==== `rsc_defaults`
 
-* +pacemaker-1.0+
-* +pacemaker-1.1+
-* +pacemaker-1.2+
-* +pacemaker-1.3+
-* +pacemaker-2.0+
+Set defaults for the resource meta attributes.
 
-Use this command to display or switch to another RNG schema.
+For more information on rule expressions, see
+<<topics_Syntax_RuleExpressions,Syntax: Rule expressions>>.
 
 Usage:
 ...............
-schema [<schema>]
+rsc_defaults [<set_id>:] [rule ...] <option>=<value> [<option>=<value> ...]
 ...............
 Example:
 ...............
-schema pacemaker-1.1
+rsc_defaults failure-timeout=3m
 ...............
 
-[[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.
+[[cmdhelp_configure_rsc_template,define a resource template]]
+==== `rsc_template`
 
-To show all objects of a certain type, use the +type:+ prefix.
-To show all objects in a given tag, use the +tag:+ prefix.
+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:
 ...............
-show [xml] [<id> ...]
-show [xml] changed
-...............
+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:
 ...............
-show webapp
-show type:primitive
-show xml type:node
-show tag:web
+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_edit,edit CIB objects]]
-==== `edit`
+[[cmdhelp_configure_rsc_ticket,resources ticket dependency]]
+==== `rsc_ticket`
 
-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.
+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.
 
-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.
+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:
 ...............
-edit [xml] [<id> ...]
-edit [xml] changed
+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
 ...............
 
-.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`
+[[cmdhelp_configure_rsctest,test resources as currently configured]]
+==== `rsctest`
 
-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.
+Test resources with current resource configuration. If no nodes
+are specified, tests are run on all known nodes.
 
-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.
+The order of resources is significant: it is assumed that later
+resources depend on earlier ones.
 
-Don't forget to put quotes around the filter if it contains
-spaces.
+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:
 ...............
-filter <prog> [xml] [<id> ...]
-filter <prog> [xml] changed
+rsctest <rsc_id> [<rsc_id> ...] [<node_id> ...]
 ...............
 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
+rsctest my_ip websvc
+rsctest websvc nodeB
 ...............
 
-.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`
+[[cmdhelp_configure_save,save the CIB to a file]]
+==== `save`
 
-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.
+Save the current configuration to a file. Optionally, as XML. Use
++-+ instead of file name to write the output to `stdout`.
 
-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.
+The `save` command accepts the same selection arguments as the `show`
+command. See the <<cmdhelp_configure_show,help section>> for `show`
+for more details.
 
 Usage:
 ...............
-delete [--force] <id> [<id>...]
+save [xml] [<id> | type:<type | tag:<tag> |
+            related:<obj> | changed ...] <file>
 ...............
-
-[[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:
+Example:
 ...............
-default-timeouts <id> [<id>...]
+save myfirstcib.txt
+save web-server server-config.txt
 ...............
 
-.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_schema,set or display current CIB RNG schema]]
+==== `schema`
 
-[[cmdhelp_configure_rename,rename a CIB object]]
-==== `rename`
+CIB's content is validated by a RNG schema. Pacemaker supports
+several, depending on version. At least the following schemas are
+accepted by `crmsh`:
 
-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.
+* +pacemaker-1.0+
+* +pacemaker-1.1+
+* +pacemaker-1.2+
+* +pacemaker-1.3+
+* +pacemaker-2.0+
 
-If you want to rename a resource, it must be in the stopped state.
+Use this command to display or switch to another RNG schema.
 
 Usage:
 ...............
-rename <old_id> <new_id>
+schema [<schema>]
+...............
+Example:
+...............
+schema pacemaker-1.1
 ...............
 
-[[cmdhelp_configure_modgroup,modify group]]
-==== `modgroup`
+[[cmdhelp_configure_set,set an attribute value]]
+==== `set`
 
-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.
+Set the value of a configured attribute. The attribute must
+have a value configured previously, and can be an agent
+parameter, meta attribute or utilization value.
+
+The first argument to the command is a path to an attribute.
+This is a dot-separated sequence beginning with the name of
+the resource, and ending with the name of the attribute to
+set.
 
 Usage:
 ...............
-modgroup <id> add <id> [after <id>|before <id>]
-modgroup <id> remove <id>
+set <path> <value>
 ...............
 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
+set vip1.ip 192.168.20.5
+set vm-a.force_stop 1
 ...............
 
-[[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.
+[[cmdhelp_configure_show,display CIB objects]]
+==== `show`
 
-Usage:
-...............
-erase [nodes]
-...............
+The `show` command displays CIB objects. Without any argument, it
+displays all objects in the CIB, but the set of objects displayed by
+`show` can be limited to only objects with the given IDs or by using
+one or more of the special prefixes described below.
 
-[[cmdhelp_configure_ptest,show cluster actions if changes were committed]]
-==== `ptest` (`simulate`)
+The XML representation for the objects can be displayed by passing
++xml+ as the first argument.
 
-Show PE (Policy Engine) motions using `ptest(8)` or
-`crm_simulate(8)`.
+To show one or more specific objects, pass the object IDs as
+arguments.
 
-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.
+To show all objects of a certain type, use the +type:+ prefix.
 
-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.
+To show all objects in a tag, use the +tag:+ prefix.
 
-If you have graphviz installed and X11 session, `dotty(1)` is run
-to display the changes graphically.
+To show all constraints related to a primitive, use the +related:+ prefix.
 
-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.
+To show all modified objects, pass the argument +changed+.
 
-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`.
+The prefixes can be used together on a single command line. For
+example, to show both the tag itself and the objects tagged by it the
+following combination can be used: +show tag:my-tag my-tag+.
 
 Usage:
 ...............
-ptest [nograph] [v...] [scores] [actions] [utilization]
+show [xml] [<id>
+           | changed
+           | type:<type>
+           | tag:<id>
+           | related:<obj>
+           ...]
+
+type :: node | primitive | group | clone | ms | rsc_template
+      | location | colocation | order
+      | rsc_ticket
+      | property | rsc_defaults | op_defaults
+      | fencing_topology
+      | role | user | acl_target
+      | tag
 ...............
-Examples:
+
+Example:
 ...............
-ptest scores
-ptest vvvvv
-simulate actions
+show webapp
+show type:primitive
+show xml tag:db tag:fs
+show related:webapp
 ...............
 
-[[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.
+[[cmdhelp_configure_show_property,Show property value]]
+==== `show-property`
 
-If a resource is multi-state, it is assumed that the role on
-which later resources depend is master.
+Show the value of the given property. If the value is not set, the
+command will print the default value for the property, if known.
 
-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.
+If no property name is passed to the command, the list of known
+cluster properties is printed.
 
-Superuser privileges are obviously required: either run this as
-root or setup the `sudoers` file appropriately.
+If the property is set multiple times, for example using multiple
+property sets with different rule expressions, the output of this
+command is undefined.
 
-Note that resource testing may take some time.
+Pass the argument +-t+ or +--true+ to `show-property` to translate
+the argument value into +true+ or +false+. If the value is not
+set, the command will print +false+.
 
 Usage:
 ...............
-rsctest <rsc_id> [<rsc_id> ...] [<node_id> ...]
+show-property [-t|--true] [<name>]
 ...............
-Examples:
+
+Example:
 ...............
-rsctest my_ip websvc
-rsctest websvc nodeB
+show-property stonith-enabled
+show-property -t maintenance-mode
 ...............
 
-[[cmdhelp_configure_cib,CIB shadow management]]
-=== `cib` (shadow CIBs)
+[[cmdhelp_configure_tag,Define resource tags]]
+==== `tag`
 
-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:
+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:
 ...............
-crm(live)configure# cib new test-2
-INFO: test-2 shadow CIB created
-crm(test-2)configure# commit
+tag <tag-name>: <rsc> [<rsc> ...]
+tag <tag-name> <rsc> [<rsc> ...]
+...............
+Example:
+...............
+tag web: p-webserver p-vip
+tag ips server-vip admin-vip
 ...............
-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`
@@ -3273,121 +3743,79 @@ Example:
 template two-apaches.txt
 ...............
 
-[[cmdhelp_configure_commit,commit the changes to the CIB]]
-==== `commit`
+[[cmdhelp_configure_upgrade,upgrade the CIB]]
+==== `upgrade`
 
-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.
+Attempts to upgrade the CIB to validate with the current
+version. Commonly, this is required if the error
+`CIB not supported` occurs. It typically means that the
+active CIB version is coming from an older release.
 
-If you know that it's fine to still apply them, add +force+ to the
-command line.
+As a safety precaution, the force argument is required if the
++validation-with+ attribute is set to anything other than
++0.6+. Thus in most cases, it is required.
 
 Usage:
 ...............
-commit [force]
+upgrade [force]
 ...............
 
-[[cmdhelp_configure_verify,verify the CIB with crm_verify]]
-==== `verify`
-
-Verify the contents of the CIB which would be committed.
-
-Usage:
+Example:
 ...............
-verify
+upgrade force
 ...............
 
-[[cmdhelp_configure_upgrade,upgrade the CIB to version 1.0]]
-==== `upgrade`
+[[cmdhelp_configure_user,define user access rights]]
+==== `user`
 
-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
-...............
+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.
 
-If we don't recognize the current CIB as the old one, but you're
-sure that it is, you may force the command.
+For more information on rule expressions, see
+<<topics_Syntax_RuleExpressions,Syntax: Rule expressions>>.
 
 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`.
+user <uid> {roles|rules}
 
-Usage:
-...............
-save [xml] <file>
+roles :: role:<role-ref> [role:<role-ref> ...]
+rules :: rule [rule ...]
 ...............
 Example:
 ...............
-save myfirstcib.txt
+user joe \
+    role:app1_admin \
+    role:read_all
 ...............
 
-[[cmdhelp_configure_load,import the CIB from a file]]
-==== `load`
+[[cmdhelp_configure_validate_all,call agent validate-all for resource]]
+==== `validate-all`
 
-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.
+Call the `validate-all` action for the resource, if possible.
 
-Usage:
-...............
-load [xml] <method> URL
+Limitations:
 
-method :: replace | update
-...............
-Example:
+* The resource agent must implement the `validate-all` action.
+* The current user must be root.
+* The primitive resource must not use nvpair references.
+
+Usage:
 ...............
-load xml update myfirstcib.xml
-load xml replace http://storage.big.com/cibs/bigcib.xml
+validate-all <rsc>
 ...............
 
-[[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.
+[[cmdhelp_configure_verify,verify the CIB with crm_verify]]
+==== `verify`
 
-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.
+Verify the contents of the CIB which would be committed.
 
 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
+verify
 ...............
 
 [[cmdhelp_configure_xml,raw xml]]
@@ -3408,7 +3836,7 @@ xml <xml>
 ...............
 
 [[cmdhelp_template,edit and import a configuration from a template]]
-=== `template`
+=== `template` - Import configuration from templates
 
 User may be assisted in the cluster configuration by templates
 prepared in advance. Templates consist of a typical ready
@@ -3417,39 +3845,29 @@ 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.
+[[cmdhelp_template_apply,process and apply the current configuration to the current CIB]]
+==== `apply`
 
-The parameter name +id+ is set by default to the name of the
-configuration.
+Copy the current or given configuration to the current CIB. By
+default, the CIB is replaced, unless the method is set to
+"update".
 
 Usage:
 ...............
-new <config> <template> [<template> ...] [params name=value ...]
-...............
+apply [<method>] [<config>]
 
-Example:
-...............
-new vip virtual-ip
-new bigfs ocfs2 params device=/dev/sdx8 directory=/bigfs
+method :: replace | update
 ...............
 
-[[cmdhelp_template_load,load a configuration]]
-==== `load`
+[[cmdhelp_template_delete,delete a configuration]]
+==== `delete`
 
-Load an existing configuration. Further `edit`, `show`, and
-`apply` commands will refer to this configuration.
+Remove a configuration. The loaded (active) configuration may be
+removed by force.
 
 Usage:
 ...............
-load <config>
+delete <config> [force]
 ...............
 
 [[cmdhelp_template_edit,edit a configuration]]
@@ -3462,39 +3880,60 @@ Usage:
 edit [<config>]
 ...............
 
-[[cmdhelp_template_delete,delete a configuration]]
-==== `delete`
+[[cmdhelp_template_list,list configurations/templates]]
+==== `list`
 
-Remove a configuration. The loaded (active) configuration may be
-removed by force.
+When called with no argument, lists existing templates and
+configurations.
+
+Given the argument +templates+, lists the available templates.
+
+Given the argument +configs+, lists the available configurations.
 
 Usage:
 ...............
-delete <config> [force]
+list [templates|configs]
 ...............
 
-[[cmdhelp_template_list,list configurations/templates]]
-==== `list`
+[[cmdhelp_template_load,load a configuration]]
+==== `load`
 
-List existing configurations or templates.
+Load an existing configuration. Further `edit`, `show`, and
+`apply` commands will refer to this configuration.
 
 Usage:
 ...............
-list [templates]
+load <config>
 ...............
 
-[[cmdhelp_template_apply,process and apply the current configuration to the current CIB]]
-==== `apply`
+[[cmdhelp_template_new,create a new configuration from templates]]
+==== `new`
 
-Copy the current or given configuration to the current CIB. By
-default, the CIB is replaced, unless the method is set to
-"update".
+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.
+
+If no parameters are being set and you don't want a particular name
+for your configuration, you can call this command with a template name
+as the only parameter. A unique configuration name based on the
+template name will be generated.
 
 Usage:
 ...............
-apply [<method>] [<config>]
+new [<config>] <template> [<template> ...] [params name=value ...]
+...............
 
-method :: replace | update
+Example:
+...............
+new vip virtual-ip
+new bigfs ocfs2 params device=/dev/sdx8 directory=/bigfs
+new apache
 ...............
 
 [[cmdhelp_template_show,show the processed configuration]]
@@ -3508,7 +3947,7 @@ show [<config>]
 ...............
 
 [[cmdhelp_cibstatus,CIB status management and editing]]
-=== `cibstatus`
+=== `cibstatus` - CIB status management and editing
 
 The `status` section of the CIB keeps the current status of nodes
 and resources. It is modified _only_ on events, i.e. when some
@@ -3548,64 +3987,18 @@ 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.
+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:
 ...............
-origin
+load {<file>|shadow:<cib>|live}
 ...............
-
-[[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:
+Example:
 ...............
-show [changed]
+load bug-12299.xml
+load shadow:test1
 ...............
 
 [[cmdhelp_cibstatus_node,change node status]]
@@ -3668,33 +4061,29 @@ op monitor d1 xen-b not_running
 op stop d1 xen-b 0 timeout
 ...............
 
-[[cmdhelp_cibstatus_quorum,set the quorum]]
-==== `quorum`
+[[cmdhelp_cibstatus_origin,display origin of the CIB status section]]
+==== `origin`
 
-Set the quorum value.
+Show the origin of the status section currently in use. This
+essentially shows the latest `load` argument.
 
 Usage:
 ...............
-quorum <bool>
-...............
-Example:
-...............
-quorum false
+origin
 ...............
 
-[[cmdhelp_cibstatus_ticket,manage tickets]]
-==== `ticket`
+[[cmdhelp_cibstatus_quorum,set the quorum]]
+==== `quorum`
 
-Modify the ticket status. Tickets can be granted and revoked.
-Granted tickets could be activated or put in standby.
+Set the quorum value.
 
 Usage:
 ...............
-ticket <ticket> {grant|revoke|activate|standby}
+quorum <bool>
 ...............
 Example:
 ...............
-ticket ticketA grant
+quorum false
 ...............
 
 [[cmdhelp_cibstatus_run,run policy engine]]
@@ -3718,6 +4107,41 @@ Example:
 run
 ...............
 
+[[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_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_simulate,simulate cluster transition]]
 ==== `simulate`
 
@@ -3740,8 +4164,23 @@ Example:
 simulate
 ...............
 
+[[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_assist,Configuration assistant]]
-=== `assist`
+=== `assist` - Configuration assistant
 
 The `assist` sublevel is a collection of helper
 commands that create or modify resources and
@@ -3751,6 +4190,18 @@ configurations.
 For more information on individual commands, see
 the help text for those commands.
 
+[[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_assist_weak-bond,Create a weak bond between resources]]
 ==== `weak-bond`
 
@@ -3772,49 +4223,115 @@ Usage:
 weak-bond resource-1 resource-2
 ........
 
-[[cmdhelp_assist_template,Create template for primitives]]
-==== `template`
+[[cmdhelp_maintenance,Maintenance mode commands]]
+=== `maintenance` - Maintenance mode commands
 
-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.
+Maintenance mode commands are commands that manipulate resources
+directly without going through the cluster infrastructure. Therefore,
+it is essential to ensure that the cluster does not attempt to monitor
+or manipulate the resources while these commands are being executed.
+
+To ensure this, these commands require that maintenance mode is set
+either for the particular resource, or for the whole cluster.
+
+[[cmdhelp_maintenance_on,Enable maintenance mode]]
+==== `on`
+
+Enables maintenances mode, either for the whole cluster
+or for the given resource.
 
 Usage:
-........
-template primitive-1 primitive-2 primitive-3
-........
+...............
+on
+on <rsc>
+...............
+Example:
+...............
+on rsc1
+...............
+
+[[cmdhelp_maintenance_off,Disable maintenance mode]]
+==== `off`
+
+Disables maintenances mode, either for the whole cluster
+or for the given resource.
+
+Usage:
+...............
+off
+off <rsc>
+...............
+Example:
+...............
+off rsc1
+...............
+
+[[cmdhelp_maintenance_action,Invoke a resource action]]
+==== `action`
+
+Invokes the given action for the resource. This is
+done directly via the resource agent, so the command must
+be issued while the cluster or the resource is in 
+maintenance mode.
+
+Unless the action is `start` or `monitor`, the action must be invoked
+on the same node as where the resource is running. If the resource is
+running on multiple nodes, the command will fail.
 
-[[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.
+To use SSH for executing resource actions on multiple nodes, append
+`ssh` after the action name. This requires SSH access to be configured
+between the nodes and the parallax python package to be installed.
+
+Usage:
+...............
+action <rsc> <action>
+action <rsc> <action> ssh
+...............
+Example:
+...............
+action webserver reload
+action webserver monitor ssh
+...............
+
+[[cmdhelp_history,Cluster history]]
+=== `history` - Cluster 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 the 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 `crm report`. Since this process takes some time and
+we always need fresh logs, information is refreshed in a much faster
+way using the python parallax module. If +python-parallax+ is not
+found on the system, examining a live cluster is still possible --
+though not as comfortable.
+
+Apart from examining a live cluster, events may be retrieved from a
+report generated by `crm 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 have discovered an issue that you want to show someone else,
+you can use the `session pack` command to save the current session as
+a tarball, similar to those generated by `crm report`.
+
+In order to minimize the size of the tarball, and to make it easier
+for others to find the interesting events, it is recommended to limit
+the time frame which the saved session covers. This can be done using
+the `timeframe` command (example below).
+
+It is also possible to name the saved session using the `session save`
+command.
 
 Example:
 ...............
@@ -3825,9 +4342,92 @@ 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_detail,set the level of detail shown]]
+==== `detail`
+
+How much detail to show from the logs. Valid detail levels are either
+`0` or `1`, where `1` is the highest detail level. The default detail
+level is `0`.
+
+Usage:
+...............
+detail <detail_level>
+
+detail_level :: small integer (defaults to 0)
+...............
+Example:
+...............
+detail 1
+...............
+
+[[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_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_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_info,Cluster information summary]]
 ==== `info`
@@ -3865,30 +4465,33 @@ 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.
+This command can be used to modify the time span to examine. All
+history commands look at events within a certain time span.
+
+For the `live` source, the default time span is the _last hour_.
+
+There is no time span limit for the `hb_report` source.
 
-The time period is parsed by the dateutil python module. It
-covers wide range of date formats. For instance:
+The time period is parsed by the `dateutil` python module. It
+covers a wide range of date formats. For instance:
 
-- 3:00      (today at 3am)
-- 15:00     (today at 3pm)
+- 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.
+For more examples of valid time/date statements, please refer to the
+`python-dateutil` documentation:
 
-If dateutil is not available, then the time is parsed using
+- https://dateutil.readthedocs.org/[dateutil.readthedocs.org]
+
+If the dateutil module 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>]]
+limit [<from_time>] [<to_time>]
 ...............
 Examples:
 ...............
@@ -3897,72 +4500,99 @@ limit 15h22m 16h
 limit "Sun 5 20:46" "Sun 5 22:00"
 ...............
 
-[[cmdhelp_history_source,set source to be examined]]
-==== `source`
+[[cmdhelp_history_log,log content]]
+==== `log`
 
-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.
+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.
 
-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.
+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:
 ...............
-source [<dir>|<file>|live]
+log [<node> [<node> ...] ]
 ...............
-Examples:
+Example:
 ...............
-source live
-source /tmp/customer_case_22.tar.bz2
-source /tmp/customer_case_22
-source
+log node-a
 ...............
 
-[[cmdhelp_history_refresh,refresh live report]]
-==== `refresh`
+[[cmdhelp_history_events,Show events in log]]
+==== `events`
+
+By analysing the log output and looking for particular
+patterns, the `events` command helps sifting through
+the logs to find when particular events like resources
+changing state or node failure may have occurred.
 
-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+.
+This can be used to generate a combined list of events
+from all nodes.
 
 Usage:
 ...............
-refresh [force]
+events
 ...............
 
-[[cmdhelp_history_detail,set the level of detail shown]]
-==== `detail`
+Example:
+...............
+events
+...............
 
-How much detail to show from the logs.
+[[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:
 ...............
-detail <detail_level>
-
-detail_level :: small integer (defaults to 0)
+node <node> [<node> ...]
 ...............
 Example:
 ...............
-detail 1
+node node1
 ...............
 
-[[cmdhelp_history_setnodes,set the list of cluster nodes]]
-==== `setnodes`
+[[cmdhelp_history_peinputs,list or get PE input files]]
+==== `peinputs`
 
-In case the host this program runs on is not part of the cluster,
-it is necessary to set the list of nodes.
+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:
 ...............
-setnodes node <node> [<node> ...]
+peinputs [{<range>|<number>} ...] [v]
+
+range :: <n1>:<n2>
 ...............
 Example:
 ...............
-setnodes node_a node_b
+peinputs
+peinputs 440:444 446
+peinputs v
+...............
+
+[[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_resource,resource events]]
@@ -3987,84 +4617,91 @@ resource my_.*_db2
 resource ping_clone
 ...............
 
-[[cmdhelp_history_node,node events]]
-==== `node`
+[[cmdhelp_history_session,manage history sessions]]
+==== `session`
 
-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.
+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:
 ...............
-node <node> [<node> ...]
+session [{save|load|delete} <name> | pack [<name>] | update | list]
 ...............
-Example:
+Examples:
 ...............
-node node1
+session save bnc966622
+session load rsclost-2
+session list
 ...............
 
-[[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.
+[[cmdhelp_history_setnodes,set the list of cluster nodes]]
+==== `setnodes`
 
-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.
+In case the host this program runs on is not part of the cluster,
+it is necessary to set the list of nodes.
 
 Usage:
 ...............
-log [<node> [<node> ...] ]
+setnodes node <node> [<node> ...]
 ...............
 Example:
 ...............
-log node-a
+setnodes node_a node_b
 ...............
 
-[[cmdhelp_history_exclude,exclude log messages]]
-==== `exclude`
+[[cmdhelp_history_show,show status or configuration of the PE input file]]
+==== `show`
 
-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.
+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:
 ...............
-exclude [<regex>|clear]
+show <pe> [status]
+
+pe :: <number>|<index>|<file>|live
 ...............
-Example:
+Examples:
 ...............
-exclude kernel.*ocfs2
+show 2066
+show pe-input-2080.bz2 status
 ...............
 
-[[cmdhelp_history_peinputs,list or get PE input files]]
-==== `peinputs`
+[[cmdhelp_history_source,set source to be examined]]
+==== `source`
 
-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.
+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:
 ...............
-peinputs [{<range>|<number>} ...] [v]
-
-range :: <n1>:<n2>
+source [<dir>|<file>|live]
 ...............
-Example:
+Examples:
 ...............
-peinputs
-peinputs 440:444 446
-peinputs v
+source live
+source /tmp/customer_case_22.tar.bz2
+source /tmp/customer_case_22
+source
 ...............
 
 [[cmdhelp_history_transition,show transition]]
@@ -4103,12 +4740,18 @@ of the path to the file.
 After the `ptest` output, logs about events that happened during
 the transition are printed.
 
+The `tags` subcommand scans the logs for the transition and return a
+list of key events during that transition. For example, the tag
++error+ will be returned if there are any errors logged during the
+transition.
+
 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]]
+transition tags [<number>|<index>|<file>]
 ...............
 Examples:
 ...............
@@ -4122,75 +4765,6 @@ 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`
 
@@ -4219,35 +4793,6 @@ 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`
 
diff --git a/doc/crmsh_hb_report.8.txt b/doc/crmsh_hb_report.8.adoc
similarity index 100%
rename from doc/crmsh_hb_report.8.txt
rename to doc/crmsh_hb_report.8.adoc
diff --git a/doc/development.md b/doc/development.md
new file mode 100644
index 0000000..a8aebb5
--- /dev/null
+++ b/doc/development.md
@@ -0,0 +1,225 @@
+# Notes for developers and contributors
+
+This is mostly a list of notes that Dejan prepared for me when I
+started working on crmsh (me being Kristoffer). I've decided to update
+it at least enough to not be completely outdated, so the information
+here should be mostly up-to-date for crmsh 2.1.
+
+## data-manifest
+
+This file contains a list of all shared data files to install.
+
+Whenever a file that is to be installed to `/usr/share/crmsh` is added,
+for example a cluster script or crmsh template, the `data-manifest`
+file needs to be regenerated, by running `./update-data-manifest.sh`.
+
+## Website
+
+To build the website, you will need **Asciidoc**, **Pygments** plus
+two special lexers for Pygments installed as a separate module. This
+module is included in the source tree for crmsh under `contrib`. To
+install the module and build the website, do the following:
+
+```
+cd contrib
+sudo python setup.py install
+cd ..
+cd doc/website-v1
+make
+```
+
+If everything worked out as it should, the website should now be
+generated in `doc/website-v1/gen`.
+
+## Modules
+
+This is the list of all modules including short descriptions.
+
+- `crm`
+
+	The program. Tries to detect incompatible python versions or a
+    missing crmsh module, and report an understandable error message
+    in either case.
+
+- `modules/main.py`
+
+    This is where execution really starts. Verifies the environment
+	and detects the pacemaker version.
+
+- `modules/config.py`
+
+    Reads the `crm.conf` configuration file and tries to detect basic
+    information about where pacemaker is located etc. Some magic is
+    used to generate an object hierarchy based on the configuration,
+    so that the rest of the code can access configuration variables
+    directly.
+
+- `modules/constants.py`
+
+    Various hard-coded constants. Many of these should probably be
+    read from pacemaker metadata for better compatibility across
+    different versions.
+ 
+- `modules/ui_*.py`
+
+    The UI context (`ui_context.py`) parses the input command and
+    keeps track of which is the current level in the UI. `ui_root.py`
+    is the root of the UI hierarchy.
+
+- `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.
+
+- `doc/crm.8.adoc`
+
+	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.
+
+- `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/scripts.py`
+
+    Implements the cluster scripts. Reads multiple kinds of script
+    definition languages including the XML wizard format used by
+    Hawk.
+
+- `modules/handles.py`
+
+    A primitive handlebar-style templating language used in cluster
+    scripts.
+
+- `modules/idmgmt.py`
+
+	CIB id management. Guarantees that all ids are unique.
+	A helper for CibFactory.
+
+- `modules/parse.py`
+
+    Parses CLI -> XML.
+
+- `modules/cliformat.py`
+
+    Parses XML -> CLI.
+
+    Not as cleanly separated as the CLI parser, mostly a set of
+    functions called from `cibconfig.py`.
+
+- `modules/clidisplay.py`, `modules/term.py`
+
+	Applies colors to terminal output.
+
+- `modules/crm_gv.py`
+
+	Interface to GraphViz. Generates graph specs for dotty(1).
+
+- `modules/cibstatus.py`
+
+	CIB status section editor and manipulator (cibstatus
+	level). Interface to crm_simulate.
+
+- `modules/ra.py`
+
+	Resource agents interface.
+
+- `modules/rsctest.py`
+
+	Resource tester (configure rsctest command).
+
+- `modules/history.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.
+
+- `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 the parallax library for remote SSH commands.
+
+- `modules/corosync.py`
+
+    Parse and edit the `corosync.conf` configuration file.
+
+- `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.
+
+### 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)
+
+### 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 *')
diff --git a/doc/sort-doc.py b/doc/sort-doc.py
new file mode 100644
index 0000000..87c35a2
--- /dev/null
+++ b/doc/sort-doc.py
@@ -0,0 +1,82 @@
+# Tool to sort the documentation alphabetically
+# Makes a lot of assumptions about the structure of the document it edits
+# Looks for special markers that indicate structure
+
+# prints output to stdout
+
+# print lines until in a cmdhelp_<section>
+# collect all cmdhelp_<section>_<subsection> subsections
+# sort and print
+
+import sys
+import re
+
+
+class Sorter(object):
+    def __init__(self):
+        self.current_section = None
+        self.current_subsection = None
+        self.subsections = []
+        self.re_section = re.compile(r'^\[\[cmdhelp_([^_,]+),')
+        self.re_subsection = re.compile(r'^\[\[cmdhelp_([^_]+)_([^,]+),')
+
+    def beginsection(self, line):
+        m = self.re_section.match(line)
+        name = m.group(1)
+        self.current_section = [name, line]
+        self.current_subsection = None
+        self.subsections = []
+        return self.insection
+
+    def insection(self, line):
+        if line.startswith('[[cmdhelp_%s_' % (self.current_section[0])):
+            return self.beginsubsection(line)
+        elif line.startswith('[['):
+            self.finishsection()
+            return self.preprint(line)
+        else:
+            self.current_section[1] += line
+        return self.insection
+
+    def beginsubsection(self, line):
+        m = self.re_subsection.match(line)
+        name = m.group(2)
+        self.current_subsection = [name, line]
+        return self.insubsection
+
+    def insubsection(self, line):
+        if line.startswith('[['):
+            self.subsections.append(self.current_subsection)
+            self.current_subsection = None
+            return self.insection(line)
+        self.current_subsection[1] += line
+        return self.insubsection
+
+    def finishsection(self):
+        if self.current_section:
+            print self.current_section[1],
+            for name, text in sorted(self.subsections, key=lambda x: x[0]):
+                print text,
+        self.current_section = None
+        self.subsections = []
+
+    def preprint(self, line):
+        if self.re_section.match(line):
+            return self.beginsection(line)
+        print line,
+        return self.preprint
+
+    def run(self, lines):
+        action = self.preprint
+        for line in lines:
+            prevaction = action
+            action = action(line)
+            if action is None:
+                print prevaction
+                print self.current_section
+                print self.current_subsection
+                sys.exit(1)
+        if self.current_section:
+            self.finishsection()
+
+Sorter().run(open(sys.argv[1]).readlines())
diff --git a/doc/website-v1/404.txt b/doc/website-v1/404.adoc
similarity index 100%
rename from doc/website-v1/404.txt
rename to doc/website-v1/404.adoc
diff --git a/doc/website-v1/Makefile b/doc/website-v1/Makefile
index 2036d9e..8e1cbcd 100644
--- a/doc/website-v1/Makefile
+++ b/doc/website-v1/Makefile
@@ -1,11 +1,50 @@
 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))
+CRMCONF := crm.conf
+SRC := faq.adoc documentation.adoc development.adoc installation.adoc \
+	configuration.adoc about.adoc rsctest-guide.adoc \
+	history-guide.adoc start-guide.adoc man-1.2.adoc scripts.adoc man-2.0.adoc
+HISTORY_LISTINGS = include/history-guide/nfs-probe-err.typescript \
+	include/history-guide/sample-cluster.conf.crm \
+	include/history-guide/status-probe-fail.typescript \
+	include/history-guide/resource-trace.typescript \
+	include/history-guide/stonith-corosync-stopped.typescript \
+	include/history-guide/basic-transition.typescript \
+	include/history-guide/diff.typescript \
+	include/history-guide/info.typescript \
+	include/history-guide/resource.typescript \
+	include/history-guide/transition-log.typescript
+TGT := $(patsubst %.adoc,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
+ICONS := \
+	img/icons/caution.png \
+	img/icons/example.png \
+	img/icons/home.png \
+	img/icons/important.png \
+	img/icons/next.png \
+	img/icons/note.png \
+	img/icons/prev.png \
+	img/icons/tip.png \
+	img/icons/up.png \
+	img/icons/warning.png \
+	img/icons/callouts/10.png \
+	img/icons/callouts/11.png \
+	img/icons/callouts/12.png \
+	img/icons/callouts/13.png \
+	img/icons/callouts/14.png \
+	img/icons/callouts/15.png \
+	img/icons/callouts/1.png \
+	img/icons/callouts/2.png \
+	img/icons/callouts/3.png \
+	img/icons/callouts/4.png \
+	img/icons/callouts/5.png \
+	img/icons/callouts/6.png \
+	img/icons/callouts/7.png \
+	img/icons/callouts/8.png \
+	img/icons/callouts/9.png
+IMG := $(ICONS) img/loader.gif img/laptop.png img/servers.gif \
+	img/history-guide/sample-cluster.conf.png \
+	img/history-guide/smallapache-start.png
 IMG  := $(patsubst %,gen/%,$(IMG))
 FONTS := fonts/FontAwesome.otf fonts/fontawesome-webfont.eot \
 	fonts/fontawesome-webfont.svg fonts/fontawesome-webfont.ttf \
@@ -13,40 +52,42 @@ FONTS := fonts/FontAwesome.otf fonts/fontawesome-webfont.eot \
 FONTS := $(patsubst %,gen/%,$(FONTS))
 WATCHDIR := watchdir
 XDGOPEN := xdg-open
-NEWS := $(wildcard news/*.txt)
-NEWSDOC := $(patsubst %.txt,gen/%/index.html,$(NEWS))
+NEWS := $(wildcard news/*.adoc)
+NEWSDOC := $(patsubst %.adoc,gen/%/index.html,$(NEWS))
 
 .PHONY: all clean deploy open
 
 all: site
 
-gen/index.html: index.txt crm.conf
+gen/index.html: index.adoc $(CRMCONF)
 	@mkdir -p $(dir $@)
-	@$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	@$(ASCIIDOC) --unsafe -b html5 -a icons -a iconsdir=/img/icons -f $(CRMCONF) -o $@ $<
 	@python ./postprocess.py -o $@ $<
 
-gen/%/index.html: %.txt crm.conf
+gen/%/index.html: %.adoc $(CRMCONF)
 	@mkdir -p $(dir $@)
-	@$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	@$(ASCIIDOC) --unsafe -b html5 -a icons -a iconsdir=/img/icons -f $(CRMCONF) -o $@ $<
 	@python ./postprocess.py -o $@ $<
 
-gen/man/index.html: ../crm.8.txt crm.conf
+gen/history-guide/index.html: $(HISTORY_LISTINGS)
+
+gen/man/index.html: ../crm.8.adoc $(CRMCONF)
 	@mkdir -p $(dir $@)
-	@$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	@$(ASCIIDOC) --unsafe -b html5 -f $(CRMCONF) -o $@ $<
 	@python ./postprocess.py -o $@ $<
 
-gen/404.html: 404.txt crm.conf
+gen/404.html: 404.adoc $(CRMCONF)
 	@mkdir -p $(dir $@)
-	@$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	@$(ASCIIDOC) --unsafe -b html5 -f $(CRMCONF) -o $@ $<
 	@python ./postprocess.py -o $@ $<
 
-news.txt: $(NEWS) crm.conf
+news.adoc: $(NEWS) $(CRMCONF)
 	@echo "news:" $(NEWS)
 	python ./make-news.py $@ $(NEWS)
 
-gen/news/index.html: news.txt
+gen/news/index.html: news.adoc
 	@mkdir -p $(dir $@)
-	$(ASCIIDOC) --unsafe -b html5 -f crm.conf -o $@ $<
+	$(ASCIIDOC) --unsafe -b html5 -f $(CRMCONF) -o $@ $<
 	@python ./postprocess.py -o $@ $<
 
 gen/css/%.css: css/%.css
@@ -59,6 +100,21 @@ gen/js/%.js: js/%.js
 	@cp -r $< $@
 	@echo "+ $@"
 
+gen/img/icons/callouts/%: img/icons/callouts/%
+	@mkdir -p gen/img/icons/callouts
+	@cp -r $< $@
+	@echo "+ $@"
+
+gen/img/icons/%: img/icons/%
+	@mkdir -p gen/img/icons
+	@cp -r $< $@
+	@echo "+ $@"
+
+gen/img/history-guide/%: img/history-guide/%
+	@mkdir -p gen/img/history-guide
+	@cp -r $< $@
+	@echo "+ $@"
+
 gen/img/%: img/%
 	@mkdir -p gen/img
 	@cp -r $< $@
@@ -85,4 +141,4 @@ watch:
 	@$(WATCHDIR) --verbose --cmd "make" . css img fonts
 
 clean:
-	-@$(RM) -rf gen/* news.txt
+	-@$(RM) -rf gen/* news.adoc
diff --git a/doc/website-v1/about.txt b/doc/website-v1/about.adoc
similarity index 100%
rename from doc/website-v1/about.txt
rename to doc/website-v1/about.adoc
diff --git a/doc/website-v1/configuration.txt b/doc/website-v1/configuration.adoc
similarity index 100%
rename from doc/website-v1/configuration.txt
rename to doc/website-v1/configuration.adoc
diff --git a/doc/website-v1/crmold.conf b/doc/website-v1/crmold.conf
new file mode 100644
index 0000000..271d88d
--- /dev/null
+++ b/doc/website-v1/crmold.conf
@@ -0,0 +1,602 @@
+#
+# 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="http://crmsh.nongnu.org/css/font-awesome.min.css">
+<link rel="stylesheet" href="http://crmsh.nongnu.org/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="http://crmsh.github.io/atom.xml" type="application/atom+xml" rel="alternate" title="crmsh atom feed">
+<style>
+\#movenotice {
+            width: 600px;
+            margin-top: 1em;
+            margin-bottom: 1em;
+            margin-left: auto;
+            margin-right: auto;
+            font-size: 100%;
+            padding: 4px;
+            border: 2px dashed red;
+}
+</style>
+</head>
+<body>
+<div id="header">
+<h1><a href="http://crmsh.github.io/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="http://crmsh.github.io/news">News</a></li>
+<li><a href="http://crmsh.github.io/documentation">Documentation</a></li>
+<li><a href="http://crmsh.github.io/development">Development</a></li>
+<li><a href="http://crmsh.github.io/about">About</a></li>
+</ul>
+</div>
+</div>
+<!--TOC-->
+<div id="container">
+<div id="content">
+
+<div id="movenotice">We have moved! The website for crmsh is now <a href="http://crmsh.github.io">http://crmsh.github.io</a>.</div>
+
+<h1>{doctitle}</h1>
+
+[footer]
+</div>
+</div>
+<div id="footer">
+<div id="footer-text">
+</div>
+</div>
+
+<a href="https://github.com/crmsh/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
index 4a26960..e6b2ab0 100644
--- a/doc/website-v1/css/crm.css
+++ b/doc/website-v1/css/crm.css
@@ -476,3 +476,11 @@ div.sidebarblock .title {
 .vg { color: #008080 } /* Name.Variable.Global */
 .vi { color: #008080 } /* Name.Variable.Instance */
 .il { color: #009999 } /* Literal.Number.Integer.Long */
+.highlight .-Color-Black { color: #000000 } /* Color.Black */
+.highlight .-Color-Blue { color: #0000c0 } /* Color.Blue */
+.highlight .-Color-Cyan { color: #008080 } /* Color.Cyan */
+.highlight .-Color-Green { color: #008000 } /* Color.Green */
+.highlight .-Color-Magenta { color: #c000c0 } /* Color.Magenta */
+.highlight .-Color-Red { color: #c00000 } /* Color.Red */
+.highlight .-Color-White { color: #c0c0c0 } /* Color.White */
+.highlight .-Color-Yellow { color: #808000 } /* Color.Yellow */
diff --git a/doc/website-v1/development.txt b/doc/website-v1/development.adoc
similarity index 98%
rename from doc/website-v1/development.txt
rename to doc/website-v1/development.adoc
index 6b4e603..e6f88bc 100644
--- a/doc/website-v1/development.txt
+++ b/doc/website-v1/development.adoc
@@ -48,7 +48,7 @@ To run the unit test suite, go to the source code directory of `crmsh`
 and call:
 
 ----
-./test/unit-tests.sh
+./test/run
 ----
 
 `crmsh` also comes with a comprehensive regression test suite. The regression tests need
diff --git a/doc/website-v1/documentation.txt b/doc/website-v1/documentation.adoc
similarity index 89%
rename from doc/website-v1/documentation.txt
rename to doc/website-v1/documentation.adoc
index 4f6a72b..9f5e4fa 100644
--- a/doc/website-v1/documentation.txt
+++ b/doc/website-v1/documentation.adoc
@@ -18,11 +18,16 @@ possible.
 * 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:/history-guide[History Guide]
 * link:/rsctest-guide[Resource Testing Guide]
 * link:/configuration[Configuration]
 * link:/scripts[Cluster scripts]
 * link:/faq[Frequently Asked Questions]
 
+== Translations ==
+
+* https://blog.3ware.co.jp/2015/05/crmsh-getting-started/[Getting Started (Japanese)]
+
 == External documentation ==
 
 The SUSE
@@ -35,9 +40,5 @@ 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.adoc
similarity index 100%
rename from doc/website-v1/faq.txt
rename to doc/website-v1/faq.adoc
diff --git a/doc/website-v1/history-guide.adoc b/doc/website-v1/history-guide.adoc
new file mode 100644
index 0000000..a3dd9c6
--- /dev/null
+++ b/doc/website-v1/history-guide.adoc
@@ -0,0 +1,275 @@
+= Cluster history =
+:source-highlighter: pygments
+
+This guide should help administrators and consultants tackle
+issues in Pacemaker cluster installations. We concentrate
+on troubleshooting and analysis methods with the crmsh history.
+
+Cluster leaves numerous traces behind, more than any other
+system.  The logs and the rest are spread among all cluster nodes
+and multiple directories. The obvious difficulty is to show that
+information in a consolidated manner. This is where crmsh
+history helps.
+
+Hopefully, the guide will help you investigate your
+specific issue with more efficiency and less effort.
+
+== Sample cluster
+
+In <<Listing 1>> a modestly complex sample cluster is shown with
+which we can experiment and break in some hopefully instructive
+ways.
+
+NOTE: We won't be going into how to setup nodes or configure the
+	  cluster. For that, please refer to the
+	  link:/start-guide[Getting Started] document.
+
+[source,crmsh]
+[caption="Listing 1: "]
+.Sample cluster configuration[[Listing 1]]
+-----------------
+include::include/history-guide/sample-cluster.conf.crm[]
+-----------------
+
+If you're new to clusters, that configuration may look
+overwhelming. A graphical presentation in <<Image 1>> of the
+essential elements and relations between them is easier on the eye
+(and the mind).
+
+[caption="Image 1: "]
+.Sample cluster configuration as a graph[[Image 1]]
+image::/img/history-guide/sample-cluster.conf.png[link="/img/history-guide/sample-cluster.conf.png"]
+
+As homework, try to match the two cluster representations.
+
+== Quick (& dirty) start
+
+For the impatient, we give here a few examples of history use.
+
+Most of the time you will be dealing with various resource
+(a.k.a. application) induced phenomena. For instance, while
+preparing this document we noticed that a probe failed repeatedly
+on a node which wasn't even running the resource <<Listing 2>>.
+
+[source,ansiclr]
+[caption="Listing 2: "]
+.crm status output[[Listing 2]]
+-----------------
+include::include/history-guide/status-probe-fail.typescript[]
+-----------------
+
+The history +resource+ command shows log messages relevant to the
+supplied resource <<Listing 3>>.
+
+[source,ansiclr]
+[caption="Listing 3: "]
+.Logs on failed +nfs-server+ probe operation[[Listing 3]]
+-----------------
+include::include/history-guide/nfs-probe-err.typescript[]
+-----------------
+
+<1> NFS server error message.
+<2> Warning about a non-existing user id.
+
+NOTE: Messages logged by resource agents are always tagged with
+      'type(ID)' (in <<Listing 3>>: +nfsserver(nfs-server)+).
+      +
+	  Everything dumped to +stderr/stdout+ (in <<Listing 3>>:
+	  +id: rpcuser: no such user+) is captured and subsequently
+	  logged by +lrmd+. The +stdout+ output is at the 'info'
+	  severity which is by default _not_ logged by pacemaker
+	  since version 1.1.12.
+
+At the very top we see error message reporting that the
+NFS server is running, but some other stuff, apparently
+unexpectedly, is not. However, we know that it cannot be
+running on the 'c' node as it is already running on the 'a' node.
+Not being able to figure out what is going on, we had to turn on
+tracing of the resource agent. <<Listing 4>> shows how to do
+that.
+
+[source,ansiclr]
+[caption="Listing 4: "]
+.Set `nfs-server` probe operation resource tracing[[Listing 4]]
+-----------------
+include::include/history-guide/resource-trace.typescript[]
+-----------------
+
+Trace of the +nfsserver+ RA revealed that the +nfs-server+ init
+script (used internally by the resource agent) _always_ exits
+with success for status. That was actually due to the recent port
+to systemd and erroneous interpretation of `systemctl status`
+semantics: it always exits with success (due to some paradigm
+shift, we guess). FYI, `systemctl is-active` should be used
+instead and it does report a service status as expected.
+
+As a bonus, a minor issue about a non-existing user id +rpcuser+
+is also revealed.
+
+NOTE: Messages in the crm history log output are colored
+      depending on the originating host.
+
+The rest of this document gives more details about crmsh history.
+If you're more of a just-try-it-out person, enter +crm history+
+and experiment. With +history+ commands you cannot really break
+anything (fingers crossed).
+
+== Introduction to crmsh `history`
+
+The history crmsh feature, as the name suggests, deals with the
+past. It was conceived as a facility to bring to the fore all
+trails pacemaker cluster leaves behind which are relevant to a
+particular resource, node, or event. It is used in the first
+place as a troubleshooting tool, but it can also be helpful in
+studying pacemaker clusters.
+
+To begin, we run the `info` command which gives an overview, as
+shown in <<Listing 5>>.
+
+[source,ansiclr]
+[caption="Listing 5: "]
+.Basic history information[[Listing 5]]
+-----------------
+include::include/history-guide/info.typescript[]
+-----------------
+
+The `timeframe` command limits the observed period and helps
+focus on the events of interest. Here we wanted to look at the
+10 minute period. Two transitions were executed during this time.
+
+== Transitions
+
+Transitions are basic units capturing cluster movements
+(resource operations and node events). A transition
+consists of a set of actions to reach a desired cluster
+status as specified in the cluster configuration by the
+user.
+
+Every configuration or status change results in a transition.
+
+Every transition is also a CIB, which is how cluster
+configuration and status are stored. Transitions are saved
+to files, the so called PE (Policy Engine) inputs.
+
+In  <<Listing 6>> we show how to display transitions.
+The listing is annotated to explain the output in more detail.
+
+
+[source,ansiclr]
+[caption="Listing 6: "]
+.Viewing transitions[[Listing 6]]
+-----------------
+include::include/history-guide/basic-transition.typescript[]
+-----------------
+
+<1> The transition command without arguments displays the latest
+transition.
+<2> Graph of transition actions is provided by `graphviz`. See
+<<Image 2>>.
+<3> Output of `crm_simulate` with irrelevant stuff edited out.
+`crm_simulate` was formerly known as `ptest`.
+<4> Transition summary followed by selection of log messages.
+History weeds out messages which are of lesser importance. See
+<<Listing 8>> if you want to see what history has been hiding
+from you here.
+
+Incidentally, if you wonder why all transitions in these examples
+are green, that is not because they were green in any sense of
+the color, but just due to that being color of node 'c': as
+chance would have it, 'c' was calling shots at the time (being
+Designated Coordinator or DC). That is also why all `crmd` and
+`pengine` messages are coming from 'c'.
+
+NOTE: Transitions are the basis of pacemaker operation, make sure
+that you understand them.
+
+What you cannot see in the listing is a graph generated and shown
+in a separate window in your X11 display. <<Image 2>> may not be
+very involved, but we reckon it's as good a start as starts go.
+
+[caption="Image 2: "]
+.Graph for transition 1907[[Image 2]]
+image::/img/history-guide/smallapache-start.png[link="/img/history-guide/smallapache-start.png"]
+
+It may sometimes be useful to see what changed between two
+transitions. History `diff` command is in action in <<Listing 7>>.
+
+[source,ansiclr]
+[caption="Listing 7: "]
+.Viewing transitions[[Listing 7]]
+-----------------
+include::include/history-guide/diff.typescript[]
+-----------------
+
+<1> Configuration diff between two last transitions. Transitions
+may be referenced with indexes starting at 0 and going backwards.
+<2> Status diff between two last transitions.
+
+Whereas configuration diff is (hopefully) obvious, status diff
+needs some explanation: the status section of the PE inputs
+(transitions) always lags behind the configuration.  This is
+because at the time the transition is saved to a file, the
+actions of that transition are yet to be executed. So, the status
+section of transition _N_ corresponds to the configuration _N-1_.
+
+[source,ansiclr]
+[caption="Listing 8: "]
+.Full transition log[[Listing 8]]
+-----------------
+include::include/history-guide/transition-log.typescript[]
+-----------------
+
+== Resource and node events
+
+Apart from transitions, events such as resource start or stop are
+what we usually want to examine. In our extremely exciting
+example of apache resource restart, the history `resource`
+command picks the most interesting resource related messages as
+shown in <<Listing 9>>. Again, history shows only the most
+important log parts.
+
+NOTE: If you want to see more detail (which may not always be
+	  recommendable), then use the history `detail` command to
+	  increase the level of detail displayed.
+
+[source,ansiclr]
+[caption="Listing 9: "]
+.Resource related messages[[Listing 9]]
+-----------------
+include::include/history-guide/resource.typescript[]
+-----------------
+
+Node related events are node start and stop (cluster-wise),
+node membership changes, and stonith events (aka node fence).
+We'll refrain from showing examples of the history `node`
+command--it is analogue to the `resource` command.
+
+== Viewing logs
+
+History `log` command, unsurprisingly, displays logs. The
+messages from various nodes are weaved and shown in different
+colors for the sake of easier viewing. Unlike other history
+commands, `log` shows all messages captured in the report. If you
+find some of them irrelevant they can be filtered out:
+the `exclude` command takes extended regular expressions and it
+is additive. We usually set the exclude expression to at least
+`ssh|systemd|kernel`. Use `exclude clear` to remove all
+expressions. And don't forget the `timeframe` command that
+imposes a time window on the report.
+
+== External reports, configurations, and graphs
+
+The information source history works with is `hb_report`
+generated report. Even when examining live cluster, `hb_report` is
+run behind the scene to collect the data before presenting it to
+the user. Well, at least to generate the first report: there is a
+special procedure for log refreshing and collecting new PE
+inputs, which runs much faster than creating a report from
+scratch. However, juggling with multiple sources, appending logs,
+moving time windows, may not always be foolproof, and if
+the source gets borked you can always ask for a brand new report
+with `refresh force`.
+
+Analyzing reports from external source is no different from what
+we've seen so far. In fact, there's a `source` command which
+tells history where to look for data.
diff --git a/doc/website-v1/history-guide.txt b/doc/website-v1/history-guide.txt
deleted file mode 100644
index 343658a..0000000
--- a/doc/website-v1/history-guide.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-= Cluster history =
-
-Work in Progress. Stay tuned.
diff --git a/doc/website-v1/img/history-guide/sample-cluster.conf.png b/doc/website-v1/img/history-guide/sample-cluster.conf.png
new file mode 100644
index 0000000..0863923
Binary files /dev/null and b/doc/website-v1/img/history-guide/sample-cluster.conf.png differ
diff --git a/doc/website-v1/img/history-guide/smallapache-start.png b/doc/website-v1/img/history-guide/smallapache-start.png
new file mode 100644
index 0000000..47853c9
Binary files /dev/null and b/doc/website-v1/img/history-guide/smallapache-start.png differ
diff --git a/doc/website-v1/img/icons/README b/doc/website-v1/img/icons/README
new file mode 100644
index 0000000..f12b2a7
--- /dev/null
+++ b/doc/website-v1/img/icons/README
@@ -0,0 +1,5 @@
+Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook
+icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency
+from the Jimmac icons to get round MS IE and FOP PNG incompatibilies.
+
+Stuart Rackham
diff --git a/doc/website-v1/img/icons/callouts/1.png b/doc/website-v1/img/icons/callouts/1.png
new file mode 100644
index 0000000..7d47343
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/1.png differ
diff --git a/doc/website-v1/img/icons/callouts/10.png b/doc/website-v1/img/icons/callouts/10.png
new file mode 100644
index 0000000..997bbc8
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/10.png differ
diff --git a/doc/website-v1/img/icons/callouts/11.png b/doc/website-v1/img/icons/callouts/11.png
new file mode 100644
index 0000000..ce47dac
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/11.png differ
diff --git a/doc/website-v1/img/icons/callouts/12.png b/doc/website-v1/img/icons/callouts/12.png
new file mode 100644
index 0000000..31daf4e
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/12.png differ
diff --git a/doc/website-v1/img/icons/callouts/13.png b/doc/website-v1/img/icons/callouts/13.png
new file mode 100644
index 0000000..14021a8
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/13.png differ
diff --git a/doc/website-v1/img/icons/callouts/14.png b/doc/website-v1/img/icons/callouts/14.png
new file mode 100644
index 0000000..64014b7
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/14.png differ
diff --git a/doc/website-v1/img/icons/callouts/15.png b/doc/website-v1/img/icons/callouts/15.png
new file mode 100644
index 0000000..0d65765
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/15.png differ
diff --git a/doc/website-v1/img/icons/callouts/2.png b/doc/website-v1/img/icons/callouts/2.png
new file mode 100644
index 0000000..5d09341
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/2.png differ
diff --git a/doc/website-v1/img/icons/callouts/3.png b/doc/website-v1/img/icons/callouts/3.png
new file mode 100644
index 0000000..ef7b700
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/3.png differ
diff --git a/doc/website-v1/img/icons/callouts/4.png b/doc/website-v1/img/icons/callouts/4.png
new file mode 100644
index 0000000..adb8364
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/4.png differ
diff --git a/doc/website-v1/img/icons/callouts/5.png b/doc/website-v1/img/icons/callouts/5.png
new file mode 100644
index 0000000..4d7eb46
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/5.png differ
diff --git a/doc/website-v1/img/icons/callouts/6.png b/doc/website-v1/img/icons/callouts/6.png
new file mode 100644
index 0000000..0ba694a
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/6.png differ
diff --git a/doc/website-v1/img/icons/callouts/7.png b/doc/website-v1/img/icons/callouts/7.png
new file mode 100644
index 0000000..472e96f
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/7.png differ
diff --git a/doc/website-v1/img/icons/callouts/8.png b/doc/website-v1/img/icons/callouts/8.png
new file mode 100644
index 0000000..5e60973
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/8.png differ
diff --git a/doc/website-v1/img/icons/callouts/9.png b/doc/website-v1/img/icons/callouts/9.png
new file mode 100644
index 0000000..a0676d2
Binary files /dev/null and b/doc/website-v1/img/icons/callouts/9.png differ
diff --git a/doc/website-v1/img/icons/caution.png b/doc/website-v1/img/icons/caution.png
new file mode 100644
index 0000000..9a8c515
Binary files /dev/null and b/doc/website-v1/img/icons/caution.png differ
diff --git a/doc/website-v1/img/icons/example.png b/doc/website-v1/img/icons/example.png
new file mode 100644
index 0000000..1199e86
Binary files /dev/null and b/doc/website-v1/img/icons/example.png differ
diff --git a/doc/website-v1/img/icons/home.png b/doc/website-v1/img/icons/home.png
new file mode 100644
index 0000000..37a5231
Binary files /dev/null and b/doc/website-v1/img/icons/home.png differ
diff --git a/doc/website-v1/img/icons/important.png b/doc/website-v1/img/icons/important.png
new file mode 100644
index 0000000..be685cc
Binary files /dev/null and b/doc/website-v1/img/icons/important.png differ
diff --git a/doc/website-v1/img/icons/next.png b/doc/website-v1/img/icons/next.png
new file mode 100644
index 0000000..64e126b
Binary files /dev/null and b/doc/website-v1/img/icons/next.png differ
diff --git a/doc/website-v1/img/icons/note.png b/doc/website-v1/img/icons/note.png
new file mode 100644
index 0000000..7c1f3e2
Binary files /dev/null and b/doc/website-v1/img/icons/note.png differ
diff --git a/doc/website-v1/img/icons/prev.png b/doc/website-v1/img/icons/prev.png
new file mode 100644
index 0000000..3e8f12f
Binary files /dev/null and b/doc/website-v1/img/icons/prev.png differ
diff --git a/doc/website-v1/img/icons/tip.png b/doc/website-v1/img/icons/tip.png
new file mode 100644
index 0000000..f087c73
Binary files /dev/null and b/doc/website-v1/img/icons/tip.png differ
diff --git a/doc/website-v1/img/icons/up.png b/doc/website-v1/img/icons/up.png
new file mode 100644
index 0000000..2db1ce6
Binary files /dev/null and b/doc/website-v1/img/icons/up.png differ
diff --git a/doc/website-v1/img/icons/warning.png b/doc/website-v1/img/icons/warning.png
new file mode 100644
index 0000000..d41edb9
Binary files /dev/null and b/doc/website-v1/img/icons/warning.png differ
diff --git a/doc/website-v1/include/history-guide/basic-transition.typescript b/doc/website-v1/include/history-guide/basic-transition.typescript
new file mode 100644
index 0000000..a5a0a31
--- /dev/null
+++ b/doc/website-v1/include/history-guide/basic-transition.typescript
@@ -0,0 +1,22 @@
+crm(live)history# transition <1>
+INFO: running ptest with /var/cache/crm/history/live/sle12-c/pengine/pe-input-1907.bz2
+INFO: starting dotty to show transition graph <2>
+Current cluster status: <3>
+Online: [ sle12-a sle12-c ]
+ s-libvirt	(stonith:external/libvirt):	Started sle12-c 
+ ...
+ small-apache	(ocf::heartbeat:apache):	Stopped 
+Transition Summary:
+ * Start   small-apache	(sle12-a)
+Executing cluster transition:
+ * Resource action: small-apache    start on sle12-a
+Revised cluster status:
+Online: [ sle12-a sle12-c ]
+ s-libvirt	(stonith:external/libvirt):	Started sle12-c 
+ ...
+ small-apache	(ocf::heartbeat:apache):	Started sle12-a
+
+Transition sle12-c:pe-input-1907 (20:30:14 - 20:30:15): <4>
+	total 1 actions: 1 Complete
+Apr 15 20:30:14 sle12-c crmd[1136]:   notice: te_rsc_command: Initiating action 60: start small-apache_start_0 on sle12-a
+Apr 15 20:30:14 sle12-a apache(small-apache)[1586]: INFO: AH00558: httpd2: Could not reliably determine the server's fully qualified domain name, using 10.2.12.51. Set the 'ServerName' directive globally to suppress this message
diff --git a/doc/website-v1/include/history-guide/diff.typescript b/doc/website-v1/include/history-guide/diff.typescript
new file mode 100644
index 0000000..129febc
--- /dev/null
+++ b/doc/website-v1/include/history-guide/diff.typescript
@@ -0,0 +1,11 @@
+crm(live)history# diff -1 0 <1>
+--- -1
++++ 0
+@@ -11 +11 @@
+-primitive small-apache apache params configfile="/etc/apache2/small.conf" meta target-role=Stopped
++primitive small-apache apache params configfile="/etc/apache2/small.conf" meta target-role=Started
+crm(live)history# diff -1 0 status <2>
+--- -1
++++ 0
+@@ -15 +14,0 @@
+- small-apache	(ocf::heartbeat:apache):	Started sle12-a
diff --git a/doc/website-v1/include/history-guide/info.typescript b/doc/website-v1/include/history-guide/info.typescript
new file mode 100644
index 0000000..d7aae8d
--- /dev/null
+++ b/doc/website-v1/include/history-guide/info.typescript
@@ -0,0 +1,16 @@
+# crm history
+crm(live)history# timeframe "Apr 15 20:25" "Apr 15 20:35"
+crm(live)history# info
+Source: live
+Created on: Thu Apr 16 11:32:36 CEST 2015
+By: report -Z -Q -f Wed Apr 15 20:25:00 2015 -t 2015-04-15 20:35:00 /var/cache/crm/history/live
+Period: 2015-04-15 20:25:00 - 2015-04-15 20:35:00
+Nodes: sle12-a sle12-c
+Groups: nfs-srv nfs-disk
+Resources: s-libvirt p_drbd_nfs nfs-vg fs1 virtual-ip nfs-server websrv websrv-ip small-apache
+Transitions: 1906 1907
+crm(live)history# peinputs v
+Date       Start    End       Filename      Client     User       Origin
+====       =====    ===       ========      ======     ====       ======
+2015-04-15 20:29:59 20:30:01  pe-input-1906 no-client  no-user    no-origin
+2015-04-15 20:30:14 20:30:15  pe-input-1907 no-client  no-user    no-origin
diff --git a/doc/website-v1/include/history-guide/nfs-probe-err.typescript b/doc/website-v1/include/history-guide/nfs-probe-err.typescript
new file mode 100644
index 0000000..ca34ba5
--- /dev/null
+++ b/doc/website-v1/include/history-guide/nfs-probe-err.typescript
@@ -0,0 +1,20 @@
+# crm history resource nfs-server
+INFO: fetching new logs, please wait ...
+Dec 16 11:53:23 sle12-c nfsserver(nfs-server)[14911]: <1> ERROR: NFS server is up, but the locking daemons are down
+Dec 16 11:53:23 sle12-c crmd[2823]:   notice: te_rsc_command: Initiating action 54: stop nfs-server_stop_0 on sle12-a
+Dec 16 11:53:23 sle12-c crmd[2823]:   notice: te_rsc_command: Initiating action 3: stop nfs-server_stop_0 on sle12-c (local)
+Dec 16 11:53:23 sle12-c nfsserver(nfs-server)[14944]: INFO: Stopping NFS server ...
+Dec 16 11:53:23 sle12-c nfsserver(nfs-server)[14944]: INFO: Stopping sm-notify
+Dec 16 11:53:23 sle12-c nfsserver(nfs-server)[14944]: INFO: Stopping rpc.statd
+Dec 16 11:53:23 sle12-c nfsserver(nfs-server)[14944]: INFO: NFS server stopped
+Dec 16 11:53:23 sle12-c crmd[2823]:   notice: te_rsc_command: Initiating action 55: start nfs-server_start_0 on sle12-a
+Dec 16 11:53:23 sle12-a nfsserver(nfs-server)[23255]: INFO: Stopping NFS server ...
+Dec 16 11:53:23 sle12-a nfsserver(nfs-server)[23255]: INFO: Stopping sm-notify
+Dec 16 11:53:23 sle12-a nfsserver(nfs-server)[23255]: INFO: Stopping rpc.statd
+Dec 16 11:53:23 sle12-a nfsserver(nfs-server)[23255]: INFO: NFS server stopped
+Dec 16 11:53:23 sle12-a nfsserver(nfs-server)[23320]: INFO: Starting NFS server ...
+Dec 16 11:53:23 sle12-a nfsserver(nfs-server)[23320]: INFO: Starting rpc.statd.
+Dec 16 11:53:24 sle12-a nfsserver(nfs-server)[23320]: INFO: executing sm-notify
+Dec 16 11:53:24 sle12-a nfsserver(nfs-server)[23320]: INFO: NFS server started
+Dec 16 11:53:24 sle12-a lrmd[6904]: <2>  notice: operation_finished: nfs-server_start_0:23320:stderr [ id: rpcuser: no such user ]
+Dec 16 11:53:24 sle12-a lrmd[6904]: message repeated 3 times: [   notice: operation_finished: nfs-server_start_0:23320:stderr [ id: rpcuser: no such user ]]
diff --git a/doc/website-v1/include/history-guide/resource-trace.typescript b/doc/website-v1/include/history-guide/resource-trace.typescript
new file mode 100644
index 0000000..e66ff7c
--- /dev/null
+++ b/doc/website-v1/include/history-guide/resource-trace.typescript
@@ -0,0 +1,7 @@
+# crm resource trace nfs-server monitor 0
+INFO: Trace for nfs-server:monitor is written to /var/lib/heartbeat/trace_ra/
+INFO: Trace set, restart nfs-server to trace non-monitor operations
+# crm resource cleanup nfs-server
+Cleaning up nfs-server on sle12-a
+Cleaning up nfs-server on sle12-c
+Waiting for 2 replies from the CRMd.. OK
diff --git a/doc/website-v1/include/history-guide/resource.typescript b/doc/website-v1/include/history-guide/resource.typescript
new file mode 100644
index 0000000..90f0265
--- /dev/null
+++ b/doc/website-v1/include/history-guide/resource.typescript
@@ -0,0 +1,6 @@
+crm(live)history# resource small-apache
+Apr 15 20:29:59 sle12-c crmd[1136]:   notice: te_rsc_command: Initiating action 60: stop small-apache_stop_0 on sle12-a
+Apr 15 20:29:59 sle12-a apache(small-apache)[1366]: INFO: Attempting graceful stop of apache PID 9155
+Apr 15 20:30:01 sle12-a apache(small-apache)[1366]: INFO: apache stopped.
+Apr 15 20:30:14 sle12-a apache(small-apache)[1586]: INFO: AH00558: httpd2: Could not reliably determine the server's fully qualified domain name, using 10.2.12.51. Set the 'ServerName' directive globally to suppress this message
+Apr 15 20:30:14 sle12-c crmd[1136]:   notice: te_rsc_command: Initiating action 60: start small-apache_start_0 on sle12-a
diff --git a/doc/website-v1/include/history-guide/sample-cluster.conf.crm b/doc/website-v1/include/history-guide/sample-cluster.conf.crm
new file mode 100644
index 0000000..8b44663
--- /dev/null
+++ b/doc/website-v1/include/history-guide/sample-cluster.conf.crm
@@ -0,0 +1,54 @@
+node 167906357: sle12-c
+node 167906355: sle12-a
+primitive s-libvirt stonith:external/libvirt \
+	params hostlist="sle12-a sle12-c" hypervisor_uri="qemu+ssh://hex-10.suse.de/system?keyfile=/root/.ssh/xen" reset_method=reboot \
+	op monitor interval=5m timeout=60s
+primitive p_drbd_nfs ocf:linbit:drbd \
+	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
+primitive nfs-vg LVM \
+	params volgrpname=nfs-vg
+primitive fs1 Filesystem \
+	params device="/dev/nfs-vg/fs1" directory="/srv/nfs" fstype=ext3 \
+	op monitor interval=30s
+primitive virtual-ip IPaddr2 \
+	params ip=10.2.12.100
+primitive nfs-server nfsserver \
+	params nfs_shared_infodir="/srv/nfs/state" nfs_ip=10.2.12.100 \
+	op monitor interval=30s
+primitive websrv apache \
+	params configfile="/etc/apache2/httpd.conf" \
+	op monitor interval=30
+primitive websrv-ip IPaddr2 \
+	params ip=10.2.12.101
+primitive small-apache apache \
+	params configfile="/etc/apache2/small.conf"
+group nfs-disk nfs-vg fs1
+group nfs-srv virtual-ip nfs-server
+ms ms_drbd_nfs p_drbd_nfs \
+	meta notify=true clone-max=2
+location nfs-pref virtual-ip 100: sle12-a
+location websrv-pref websrv 100: sle12-c
+colocation vg-with-drbd inf: nfs-vg ms_drbd_nfs:Master
+colocation c-nfs inf: nfs-srv nfs-disk
+colocation c-websrv inf: websrv websrv-ip
+colocation small-apache-with-virtual-ip inf: small-apache virtual-ip
+# need fs1 for the NFS server
+order o-nfs inf: nfs-disk nfs-srv
+# websrv serves requests at IP websrv-ip
+order o-websrv inf: websrv-ip websrv
+# small apache serves requests at IP virtual-ip
+order virtual-ip-before-small-apache inf: virtual-ip small-apache
+# drbd device is the nfs-vg PV
+order drbd-before-nfs-vg inf: ms_drbd_nfs:promote nfs-vg:start
+property cib-bootstrap-options: \
+	dc-version=1.1.12-ad083a8 \
+	cluster-infrastructure=corosync \
+	cluster-name=sle12-test3l-public \
+	no-quorum-policy=ignore \
+	last-lrm-refresh=1429192263
+op_defaults op-options: \
+	timeout=120s
diff --git a/doc/website-v1/include/history-guide/status-probe-fail.typescript b/doc/website-v1/include/history-guide/status-probe-fail.typescript
new file mode 100644
index 0000000..d1024e8
--- /dev/null
+++ b/doc/website-v1/include/history-guide/status-probe-fail.typescript
@@ -0,0 +1,15 @@
+# crm status
+Last updated: Tue Dec 16 11:57:04 2014
+Last change: Tue Dec 16 11:53:22 2014
+Stack: corosync
+Current DC: sle12-c (167906357) - partition with quorum
+Version: 1.1.12-ad083a8
+2 Nodes configured
+10 Resources configured
+Online: [ sle12-a sle12-c ]
+[...]
+     nfs-server	(ocf::heartbeat:nfsserver):	Started sle12-a 
+[...]
+Failed actions:
+    nfs-server_monitor_0 on sle12-c 'unknown error' (1): call=298, status=complete,
+    last-rc-change='Tue Dec 16 11:53:23 2014', queued=0ms, exec=135ms
diff --git a/doc/website-v1/include/history-guide/stonith-corosync-stopped.typescript b/doc/website-v1/include/history-guide/stonith-corosync-stopped.typescript
new file mode 100644
index 0000000..1bca5ac
--- /dev/null
+++ b/doc/website-v1/include/history-guide/stonith-corosync-stopped.typescript
@@ -0,0 +1,8 @@
+# crm history node sle12-c
+INFO: fetching new logs, please wait ...
+Dec 19 14:36:18 sle12-c corosync[29551]:   [MAIN  ] Corosync Cluster Engine ('2.3.3'): started and ready to provide service.
+Dec 19 14:36:19 sle12-c corosync[29545]: Starting Corosync Cluster Engine (corosync): [  OK  ]
+Dec 19 14:36:20 sle12-a pengine[6906]:  warning: pe_fence_node: Node sle12-c will be fenced because our peer process is no longer available
+Dec 19 14:36:20 sle12-a pengine[6906]:  warning: stage6: Scheduling Node sle12-c for STONITH
+Dec 19 14:36:20 sle12-a crmd[6907]:   notice: te_fence_node: Executing reboot fencing operation (65) on sle12-c (timeout=60000)
+Dec 19 14:36:20 sle12-a crmd[6907]:   notice: peer_update_callback: Node return implies stonith of sle12-c (action 65) completed
diff --git a/doc/website-v1/include/history-guide/transition-log.typescript b/doc/website-v1/include/history-guide/transition-log.typescript
new file mode 100644
index 0000000..eb689ec
--- /dev/null
+++ b/doc/website-v1/include/history-guide/transition-log.typescript
@@ -0,0 +1,13 @@
+crm(live)history# transition log
+INFO: retrieving information from cluster nodes, please wait ...
+Apr 15 20:30:14 sle12-c crmd[1136]:   notice: do_state_transition: State transition S_IDLE -> S_POLICY_ENGINE [ input=I_PE_CALC cause=C_FSA_INTERNAL origin=abort_transition_graph ]
+Apr 15 20:30:14 sle12-c stonithd[1132]:   notice: unpack_config: On loss of CCM Quorum: Ignore
+Apr 15 20:30:14 sle12-c pengine[1135]:   notice: unpack_config: On loss of CCM Quorum: Ignore
+Apr 15 20:30:14 sle12-c pengine[1135]:   notice: LogActions: Start   small-apache#011(sle12-a)
+Apr 15 20:30:14 sle12-c crmd[1136]:   notice: do_te_invoke: Processing graph 123 (ref=pe_calc-dc-1429122614-234) derived from /var/lib/pacemaker/pengine/pe-input-1907.bz2
+Apr 15 20:30:14 sle12-c crmd[1136]:   notice: te_rsc_command: Initiating action 60: start small-apache_start_0 on sle12-a
+Apr 15 20:30:14 sle12-c pengine[1135]:   notice: process_pe_message: Calculated Transition 123: /var/lib/pacemaker/pengine/pe-input-1907.bz2
+Apr 15 20:30:14 sle12-a stonithd[1160]:   notice: unpack_config: On loss of CCM Quorum: Ignore
+Apr 15 20:30:14 sle12-a apache(small-apache)[1586]: INFO: AH00558: httpd2: Could not reliably determine the server's fully qualified domain name, using 10.2.12.51. Set the 'ServerName' directive globally to suppress this message
+Apr 15 20:30:14 sle12-a crmd[1164]:   notice: process_lrm_event: Operation small-apache_start_0: ok (node=sle12-a, call=69, rc=0, cib-update=48, confirmed=true)
+Apr 15 20:30:15 sle12-c crmd[1136]:   notice: run_graph: Transition 123 (Complete=1, Pending=0, Fired=0, Skipped=0, Incomplete=0, Source=/var/lib/pacemaker/pengine/pe-input-1907.bz2): Complete
diff --git a/doc/website-v1/index.txt b/doc/website-v1/index.adoc
similarity index 67%
rename from doc/website-v1/index.txt
rename to doc/website-v1/index.adoc
index a2dc767..5113e92 100644
--- a/doc/website-v1/index.txt
+++ b/doc/website-v1/index.adoc
@@ -16,4 +16,9 @@ view of what your cluster is doing.
 
 For more information, see the link:/documentation[Documentation]!
 
+**Quick Links**:
 
+* http://download.opensuse.org/repositories/network:/ha-clustering:/Stable/[Stable Release Binaries]
+* http://download.opensuse.org/repositories/network:/ha-clustering:/Factory/[Development Snapshots]
+* https://github.com/ClusterLabs/crmsh/[Source Repository]
+* http://crmsh.github.io/man/[Manual]
diff --git a/doc/website-v1/installation.txt b/doc/website-v1/installation.adoc
similarity index 100%
rename from doc/website-v1/installation.txt
rename to doc/website-v1/installation.adoc
diff --git a/doc/website-v1/make-news.py b/doc/website-v1/make-news.py
index 22fb77a..f3c9073 100644
--- a/doc/website-v1/make-news.py
+++ b/doc/website-v1/make-news.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 """
-Output a combined news.txt document
+Output a combined news.adoc document
 Also write an Atom feed document
 """
 
@@ -70,8 +70,7 @@ class Entry(object):
 
     def date_obj(self):
         from dateutil import parser
-        return (parser.parse(self.date) -
-                datetime.datetime(1970, 1, 1)).total_seconds()
+        return (parser.parse(self.date))
 
     def atom_content(self):
         return escape('<pre>\n' + self.content + '\n</pre>\n')
@@ -121,8 +120,8 @@ def main():
         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: 1\n\n")
+        output.write("include::%s[]\n\n" % (e.filename))
         output.write(":leveloffset: 0\n\n")
 
         output.write("''''\n")
diff --git a/doc/website-v1/man-1.2.txt b/doc/website-v1/man-1.2.adoc
similarity index 100%
rename from doc/website-v1/man-1.2.txt
rename to doc/website-v1/man-1.2.adoc
diff --git a/doc/website-v1/man-2.0.txt b/doc/website-v1/man-2.0.adoc
similarity index 100%
rename from doc/website-v1/man-2.0.txt
rename to doc/website-v1/man-2.0.adoc
diff --git a/doc/website-v1/news.adoc b/doc/website-v1/news.adoc
new file mode 100644
index 0000000..54b1208
--- /dev/null
+++ b/doc/website-v1/news.adoc
@@ -0,0 +1,19 @@
+= News
+
+link:/news/2016-01-12-release-2_1_5[2016-01-12 10:00]
+
+:leveloffset: 1
+
+include::news/2016-01-12-release-2_1_5.adoc[]
+
+:leveloffset: 0
+
+''''
+* link:/news/2015-05-25-getting-started-jp[2015-05-25 13:30 Getting Started translated to Japanese]
+* link:/news/2015-05-13-release-2_1_4[2015-05-13 15:30 Announcing crmsh stable release 2.1.4]
+* link:/news/2015-04-10-release-2_1_3[2015-04-10 12:30 Announcing crmsh stable release 2.1.3]
+* link:/news/2015-01-26-release-2_1_2[2015-01-26 11:05 Announcing crmsh release 2.1.2]
+* link:/news/2014-10-28-release-2_1_1[2014-10-29 00:20 Announcing crmsh release 2.1.1]
+* link:/news/2014-06-30-release-2_1[2014-06-30 09:00 Announcing crmsh release 2.1]
+
+link:https://savannah.nongnu.org/news/?group_id=10890[Old News Archive]
diff --git a/doc/website-v1/news.txt b/doc/website-v1/news.txt
deleted file mode 100644
index 2c2b505..0000000
--- a/doc/website-v1/news.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-= 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.adoc
similarity index 100%
rename from doc/website-v1/news/2014-06-30-release-2_1.txt
rename to doc/website-v1/news/2014-06-30-release-2_1.adoc
diff --git a/doc/website-v1/news/2014-10-28-release-2_1_1.adoc b/doc/website-v1/news/2014-10-28-release-2_1_1.adoc
new file mode 100644
index 0000000..6b67f4f
--- /dev/null
+++ b/doc/website-v1/news/2014-10-28-release-2_1_1.adoc
@@ -0,0 +1,58 @@
+Announcing crmsh release 2.1.1
+==============================
+:Author: Kristoffer Gronlund
+:Email: kgronlund at suse.com
+:Date: 2014-10-29 00:20
+
+Today we are proud to announce the release of `crmsh` version 2.1.1!
+This version primarily fixes all known issues found since the release
+of `crmsh` 2.1 in June. We recommend that all users of crmsh upgrade
+to this version, especially if using Pacemaker 1.1.12 or newer.
+
+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.1/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.1.tar.gz
+* https://github.com/crmsh/crmsh/archive/2.1.1.zip
+
+Changes since the previous release:
+
+ - 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
+
+Thank you,
+
+Kristoffer and Dejan
diff --git a/doc/website-v1/news/2015-01-26-release-2_1_2.adoc b/doc/website-v1/news/2015-01-26-release-2_1_2.adoc
new file mode 100644
index 0000000..081bf1b
--- /dev/null
+++ b/doc/website-v1/news/2015-01-26-release-2_1_2.adoc
@@ -0,0 +1,69 @@
+Announcing crmsh release 2.1.2
+==============================
+:Author: Kristoffer Gronlund
+:Email: kgronlund at suse.com
+:Date: 2015-01-26 11:05
+
+Today we are proud to announce the release of `crmsh` version 2.1.2!
+This version primarily fixes all known issues found since the release
+of `crmsh` 2.1.1 in October. We recommend that all users of crmsh upgrade
+to this version, especially if using Pacemaker 1.1.12 or newer.
+
+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.2/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.2.tar.gz
+* https://github.com/crmsh/crmsh/archive/2.1.2.zip
+
+Changes since the previous release:
+
+ - 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
+
+Thank you,
+
+Kristoffer and Dejan
diff --git a/doc/website-v1/news/2015-04-10-release-2_1_3.adoc b/doc/website-v1/news/2015-04-10-release-2_1_3.adoc
new file mode 100644
index 0000000..c186ff0
--- /dev/null
+++ b/doc/website-v1/news/2015-04-10-release-2_1_3.adoc
@@ -0,0 +1,68 @@
+Announcing crmsh stable release 2.1.3
+=====================================
+:Author: Kristoffer Gronlund
+:Email: kgronlund at suse.com
+:Date: 2015-04-10 12:30
+
+Today we are proud to announce the release of `crmsh` version 2.1.3!
+This version fixes all known issues found since the release of `crmsh`
+2.1.2 in January. We recommend that all users of crmsh upgrade 
+to this version, especially if using Pacemaker 1.1.12 or newer.
+
+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/ClusterLabs/crmsh/blob/2.1.3/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/ClusterLabs/crmsh/archive/2.1.3.tar.gz
+* https://github.com/ClusterLabs/crmsh/archive/2.1.3.zip
+
+Changes since the previous release:
+
+ - 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
+
+Thank you,
+
+Kristoffer and Dejan
diff --git a/doc/website-v1/news/2015-05-13-release-2_1_4.adoc b/doc/website-v1/news/2015-05-13-release-2_1_4.adoc
new file mode 100644
index 0000000..31297cf
--- /dev/null
+++ b/doc/website-v1/news/2015-05-13-release-2_1_4.adoc
@@ -0,0 +1,126 @@
+Announcing crmsh stable release 2.1.4
+=====================================
+:Author: Kristoffer Gronlund
+:Email: kgronlund at suse.com
+:Date: 2015-05-13 15:30
+
+Today we are proud to announce the release of `crmsh` version 2.1.4!
+2.1.4 is a minor bug fix release with no major issues, so users
+already running 2.1.3 are mostly fine. Instead, the main reason
+for releasing 2.1.4 is as an excuse to talk about some other things
+that are happening with crmsh!
+
+The details for this release are available below.
+
+History Guide
+~~~~~~~~~~~~~
+
+Dejan has written a guide to using the crmsh history
+command. For those who are unfamiliar with the history explorer or
+want to know more about how to use it, this guide is a great
+introduction to what it does and how to use it.
+
+History is not a new crmsh feature, but, as we failed to
+advertise it and nothing works without proper marketing, it
+probably hasn't seen a very wide use. That's surely a pity and we
+hope that this gentle history guide is going to help. 
+
+So, if you use crmsh and if you need help troubleshooting
+clusters (I surely do!), take a look here:
+
+http://crmsh.github.io/history-guide/
+
+FYI, the comprehensive crmsh help also has a short description of
+the feature:
+
+........
+crm history help
+........
+
+Goes without saying: all commands are described too.
+
+If you don't use crmsh, you'll still find a lot of useful
+information in the guide, so don't skip it.
+
+Hawk Presentation
+~~~~~~~~~~~~~~~~~
+
+I presented Hawk [1] and the History Explorer interface which
+builds upon the crmsh history feature at openSUSE conf in The Hague
+earlier this month. The video of that presentation is online here:
+
+++++++++++++
+<iframe width="420" height="315" src="https://www.youtube.com/embed/mngfxzXkFLw" frameborder="0" allowfullscreen></iframe>
+++++++++++++
+
+https://www.youtube.com/watch?v=mngfxzXkFLw
+
+[1]: https://github.com/ClusterLabs/hawk
+
+
+2.2.0 Development News
+~~~~~~~~~~~~~~~~~~~~~~~
+
+While 2.1.4 is the latest stable release, I am also working on releasing
+2.2.0 which will come with a bunch of new features. I'm still working
+on some of these and not everything is in the repository yet, so
+2.2.0 is probably at least a month or so away still. I was perhaps
+a bit optimistic when I tagged RC1 back in October last year. ;)
+
+However, right now I'd like to focus on one thing that is already in
+2.2.0 and which is available if you use the development packages from
+OBS: command shorthands. This makes crmsh a lot more convenient to use
+from the command line. Basically, you can use any unambiguous subset
+of a command name to refer to that command, and crmsh will figure out
+what you mean. This may sound confusing, so an example will help with
+explaining what I mean:
+
+This is one way of showing the current cluster configuration:
+
+........
+crm configure show
+........
+
+However, now you can shorten this to the following:
+
+........
+crm cfg show
+........
+
+Other examples of shorthand are `crm rsc stop r1` or `crm st`
+for status. And of course, tab completion in bash still works for
+the shorthand variants.
+
+The examples used here are not comprehensive. crmsh is pretty clever
+at figuring out which command was intended. Download the development
+release and try it out!
+
+2.1.4 Details
+~~~~~~~~~~~~~
+
+For a complete list of changes since the previous version, please
+refer to the changelog:
+
+* https://github.com/ClusterLabs/crmsh/blob/2.1.4/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/ClusterLabs/crmsh/archive/2.1.4.tar.gz
+* https://github.com/ClusterLabs/crmsh/archive/2.1.4.zip
+
+Changes since the previous release:
+
+- Medium: hb_report: use faster zypper interface if available
+- medium: ui_configure: Wait for DC when removing running resource
+- low: schema: Don't leak PacemakerError exceptions (#93)
+- parse: Don't require trailing colon in tag definitions
+- medium: utils: Allow 1/0 as boolean values for parameters
+
+Thank you,
+
+Kristoffer and Dejan
diff --git a/doc/website-v1/news/2015-05-25-getting-started-jp.adoc b/doc/website-v1/news/2015-05-25-getting-started-jp.adoc
new file mode 100644
index 0000000..c5c6759
--- /dev/null
+++ b/doc/website-v1/news/2015-05-25-getting-started-jp.adoc
@@ -0,0 +1,17 @@
+Getting Started translated to Japanese
+======================================
+:Author: Kristoffer Gronlund
+:Email: kgronlund at suse.com
+:Date: 2015-05-25 13:30
+
+Many thanks to Motoharu Kubo at 3ware for offering to translate the
+`crmsh` documentation to Japanese!
+
+The first document to be translated is the link:/start-guide/[Getting Started] guide,
+now available in Japanese at the following location:
+
+* https://blog.3ware.co.jp/2015/05/crmsh-getting-started/
+
+Thank you,
+Kristoffer and Dejan
+
diff --git a/doc/website-v1/news/2016-01-12-release-2_1_5.adoc b/doc/website-v1/news/2016-01-12-release-2_1_5.adoc
new file mode 100644
index 0000000..93a3242
--- /dev/null
+++ b/doc/website-v1/news/2016-01-12-release-2_1_5.adoc
@@ -0,0 +1,56 @@
+Announcing crmsh stable release 2.1.5
+=====================================
+:Author: Kristoffer Gronlund
+:Email: kgronlund at suse.com
+:Date: 2016-01-12 10:00
+
+Today we are proud to announce the release of `crmsh` version 2.1.5!
+This release mainly consists of bug fixes, as well as compatibility
+with Pacemaker 1.1.14.
+
+For a complete list of changes since the previous version, please
+refer to the changelog:
+
+* https://github.com/ClusterLabs/crmsh/blob/2.1.5/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/ClusterLabs/crmsh/archive/2.1.5.tar.gz
+* https://github.com/ClusterLabs/crmsh/archive/2.1.5.zip
+
+Changes since the previous release:
+
+- medium: report: Try to load source as session if possible (bsc#927407)
+- medium: crm_gv: Wrap non-identifier names in quotes (bsc#931837)
+- medium: crm_gv: Improved quoting of non-identifier node names (bsc#931837)
+- medium: crm_pkg: Fix cluster init bug on RH-based systems
+- medium: hb_report: Collect logs from pacemaker.log
+- medium: constants: Add 'provides' meta attribute (bsc#936587)
+- high: parse: Add attributes to terminator set (bsc#940920)
+- Medium: cibconfig: skip sanity check for properties other than cib-bootstrap-options
+- medium: config: Add report_tool_options (bsc#917638)
+- low: main: Bash completion didn't handle sudo correctly
+- high: report: New detection to fix missing transitions (bnc#917131)
+- medium: report: Add pacemaker.log to find_node_log list (bsc#941734)
+- high: hb_report: Prefer pacemaker.log if it exists (bsc#941681)
+- high: report: Output format from pacemaker has changed (bsc#941681)
+- high: report: Update transition edge regexes (bsc#942906)
+- medium: report: Reintroduce empty transition pruning (bsc#943291)
+- medium: log_patterns: Remove reference to function name in log patterns (bsc#942906)
+- low: hb_report: Collect libqb version (bsc#943327)
+- high: parse: Fix crash when referencing score types by name (bsc#940194)
+- low: constants: Add meta attributes for remote nodes
+- low: ui_history: Swap from and to times if to < from
+- high: cibconfig: Do not fail on unknown pacemaker schemas (bsc#946893)
+- high: log_patterns_118: Update the correct set of log patterns (bsc#942906)
+- high: xmlutil: Order is significant in resource_set (bsc#955434)
+- high: cibconfig: Fix XML import bug for cloned groups (bsc#959895)
+
+Thank you,
+
+Kristoffer and Dejan
diff --git a/doc/website-v1/postprocess.py b/doc/website-v1/postprocess.py
index 8ae8833..9491746 100644
--- a/doc/website-v1/postprocess.py
+++ b/doc/website-v1/postprocess.py
@@ -30,7 +30,7 @@ def read_toc_data(infile, debug):
                 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))
+                    commands_data.append((3, '_'.join(info_split[2:]), info))
     toc = ''
     if len(topics_data) > 0 or len(commands_data) > 0:
         toc = '<div id="toc">\n'
diff --git a/doc/website-v1/rsctest-guide.txt b/doc/website-v1/rsctest-guide.adoc
similarity index 100%
rename from doc/website-v1/rsctest-guide.txt
rename to doc/website-v1/rsctest-guide.adoc
diff --git a/doc/website-v1/scripts.adoc b/doc/website-v1/scripts.adoc
new file mode 100644
index 0000000..e77ac65
--- /dev/null
+++ b/doc/website-v1/scripts.adoc
@@ -0,0 +1,643 @@
+= Cluster Scripts =
+:source-highlighter: pygments
+
+.Version information
+NOTE: This section applies to `crmsh 2.2+` 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 python `parallax` 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.
+
+For many scripts that only configure cluster resources or only perform
+changes on the local machine, the use of SSH is not necessary. These
+scripts can be used even if there is no way for `crmsh` to reach the
+other nodes other than through the cluster configuration.
+
+NOTE: The scripts functionality in `crmsh` has been greatly expanded
+and improved in `crmsh` 2.2. Many new scripts have been added, and in
+addition the scripts are now used as the backend for the wizards
+functionality in HAWK, the HA web interface. For more information, see
+https://github.com/ClusterLabs/hawk.
+
+== 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`, `add` and `remove` commands available
+in the `cluster` sublevel, 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.
+
+=== Common Parameters ===
+
+Which parameters a script accepts varies from script to
+script. However, there is a set of parameters that are common to all
+scripts. These parameters can be passed to any script.
+
+`nodes`::
+    List of nodes to execute the script for
+`dry_run`::
+    If set, simulate execution only
+    (default: no)
+`action`::
+    If set, only execute a single action (index, as returned by verify)
+`statefile`::
+    When single-stepping, the state is saved in the given file
+`user`::
+    Run script as the given user
+`sudo`::
+    If set, crm will prompt for a sudo password and use sudo when appropriate
+    (default: no)
+`port`::
+    Port to connect on
+`timeout`::
+    Execution timeout in seconds
+    (default: 600)
+
+=== 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. Optionally, the arguments +all+ or +names+ can be
+used. Without the +all+ flag, some scripts that are used by `crmsh` to
+implement certain commands are hidden from view. With the +names+
+flag, only a plain list of script names is printed.
+
+=== Script description ===
+
+To get more details about a script, run the `show` command. For
+example, to get more information about what the `virtual-ip` script does
+and what parameters it accepts, use the following command:
+
+.........
+# crm script
+show virtual-ip
+.........
+
+`show` will print a longer description of the script, along with a
+list of parameters divided into _steps_. Each script is divided into a
+series of steps which are performed in order. Some steps may not
+accept any parameters, but for those that do, the available parameters
+are listed here.
+
+By default, only a basic subset of the available parameters is printed
+in order to make the scripts easier to use. By passing `all` to the
+`show` command, the advanced parameters are also shown. In addition,
+there is a list of common parameters
+
+`show` 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.
+
+=== Verifying parameters ===
+
+Since a script potentially performs a series of actions and may fail
+for various reasons at any point, it is advisable to review the
+actions that a script will perform before actually running it. To do
+this, the `verify` command can be used.
+
+Pass the parameters that you would pass to `run`, and `verify` will
+check that the parameter values are OK, as well as print the sequence
+of steps that will be performed given the particular parameter values
+given.
+
+The following is an example showing how to verify the creation of a
+Virtual IP resource, using the `virtual-ip` script:
+
+..........
+# crm script
+verify virtual-ip id=my-virtual-ip ip=192.168.0.10
+..........
+
+`crmsh` will print something similar to the following output:
+
+...........
+1. Configure cluster resources
+
+	primitive my-virtual-ip ocf:heartbeat:IPaddr2
+		ip="192.168.0.10"
+		op start timeout="20" op stop timeout="20"
+		op monitor interval="10" timeout="20"
+...........
+
+In this particular case, there is only a single step, and that step
+configures a primitive resource. Other scripts may configure multiple
+resources and constraints, or may perform multiple steps in sequence.
+
+=== 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 create a Virtual IP resource using
+the `virtual-ip` script:
+
+........
+# crm script
+run virtual-ip id=my-virtual-ip ip=192.168.0.10
+........
+
+==== Single-stepping a script ====
+
+It is possible to run a script action-by-action, with manual intervention
+between actions. First of all, list the actions to perform given a
+certain set of parameter values:
+
+........
+crm script verify health
+........
+
+To execute a single action, two things need to be provided:
+
+1. The index of the action to execute (printed by `verify`)
+2. a file in which `crmsh` stores the state of execution.
+
+Note that it is entirely possible to run actions out-of-order, however
+this is unlikely to work in practice since actions often rely on the
+outcome of previous actions.
+
+The following command will execute the first action of the `health`
+script and store the output in a temporary file named `health.json`:
+
+........
+crm script run health action=1 statefile='health.json'
+........
+
+The statefile contains the script parameters and the output of
+previous steps, encoded as `json` data.
+
+To continue executing the next action in sequence, enter the next
+action index:
+
+........
+crm script run health action=2 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 ===
+
+NOTE: The implementation of cluster scripts was revised between
+`crmsh` 2.0 and `crmsh` 2.2. This section describes the revised
+cluster script format. The old format is still accepted by `crmsh`.
+
+A cluster script consists of four main sections:
+
+. The name and description of the script.
+. Any other scripts or agents included by this script, and any parameter value overrides to those provided by the included script.
+. A set of parameters accepted by the script itself, in addition to those accepted by any scripts or agents included in the script.
+. A sequence of actions which the script will perform.
+
+When the script runs, the actions defined in `main.yml` as described
+below are executed one at a time. Each action prescribes a
+modification that is applied to the cluster. Some actions work by
+calling out to scripts on each of the cluster nodes, and others apply
+only on the local node from which the script 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:
+
+cib::
+  * Applies a new CIB configuration to the cluster
+
+install::
+  * Ensures that the given list of packages is installed on all
+    cluster nodes using the system package manager.
+
+service::
+  * Manages system services using the system init tools. The argument
+    should be a space-separated list of <service>:<state> pairs.
+
+call::
+  * Run a shell command as specified in the action, either on the
+    local node on or all nodes.
+
+copy::
+  * Installs a file on the cluster nodes.
+  * Using a configuration template, install a file on the cluster
+    nodes.
+
+crm::
+  * Runs the given command using the `crm` shell. This can be used to
+    start and stop resources, for example.
+
+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 which wraps the resource agent
+`ocf:heartbeat:IPaddr2`.
+
+[source,yaml]
+----
+# The version must be exactly 2.2, and must always be
+# specified in the script. If the version is missing or
+# is less than 2.2, the script is assumed to be a legacy
+# script (specified in the format used before crmsh 2.2).
+version: 2.2
+shortdesc: Virtual IP
+category: Basic
+include:
+  - agent: ocf:heartbeat:IPaddr2
+    name: virtual-ip
+    parameters:
+      - name: id
+        type: resource
+        required: true
+      - name: ip
+        type: ip_address
+        required: true
+      - name: cidr_netmask
+        type: integer
+        required: false
+      - name: broadcast
+        type: ip_address
+        required: false
+    ops: |
+      op start timeout="20" op stop timeout="20"
+      op monitor interval="10" timeout="20"
+actions:
+  - include: virtual-ip
+----
+
+For a bigger example, here is the `apache` agent which includes
+multiple optional steps, the optional installation of packages,
+defines multiple cluster resources and potentially calls bash commands
+on each of the cluster nodes.
+
+[source,yaml]
+----
+# Copyright (C) 2009 Dejan Muhamedagic
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+category: Server
+shortdesc: Apache Webserver
+longdesc: |
+  Configure a resource group containing a virtual IP address and
+  an instance of the Apache web server.
+
+  You can optionally configure a Filesystem resource which will be
+  mounted before the web server is started.
+
+  You can also optionally configure a database resource which will
+  be started before the web server but after mounting the optional
+  filesystem.
+include:
+  - agent: ocf:heartbeat:apache
+    name: apache
+    longdesc: |
+      The Apache configuration file specified here must be available via the
+      same path on all cluster nodes, and Apache must be configured with
+      mod_status enabled.  If in doubt, try running Apache manually via
+      its init script first, and ensure http://localhost:80/server-status is
+      accessible.
+    ops: |
+      op start timeout="40"
+      op stop timeout="60"
+      op monitor interval="10" timeout="20"
+  - script: virtual-ip
+    shortdesc: The IP address configured here will start before the Apache instance.
+    parameters:
+      - name: id
+        value: "{{id}}-vip"
+  - script: filesystem
+    shortdesc: Optional filesystem mounted before the web server is started.
+    required: false
+  - script: database
+    shortdesc: Optional database started before the web server is started.
+    required: false
+parameters:
+  - name: install
+    type: boolean
+    shortdesc: Install and configure apache
+    value: false
+actions:
+  - install:
+      - apache2
+    shortdesc: Install the apache package
+    when: install
+  - service:
+      - apache: disable
+    shortdesc: Let cluster manage apache
+    when: install
+  - call: a2enmod status; true
+    shortdesc: Enable status module
+    when: install
+  - include: filesystem
+  - include: database
+  - include: virtual-ip
+  - include: apache
+  - cib: |
+      group g-{{id}}
+        {{filesystem:id}}
+        {{database:id}}
+        {{virtual-ip:id}}
+        {{id}}
+----
+
+The language for referring to parameter values in `cib` actions is
+described below.
+
+=== Command arguments ===
+
+The actions that accept a command as argument must not refer to
+commands written in python. 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)`.
+
+=== The handles language ===
+
+CIB configurations and commands can refer to the value of parameters
+in the text of the action. This is done using a custom language,
+similar to handlebars.
+
+The language accepts the following constructions:
+
+............
+{{name}} = Inserts the value of the parameter <name>
+{{script:name}} = Inserts the value of the parameter <name> from the
+                  included script named <script>.
+{{#name}} ... {{/name}} = Inserts the text between the mustasches when
+                          name is truthy.
+{{^name}} ... {{/name}} = Inserts the text between the mustasches when
+                          name is falsy.
+............
diff --git a/doc/website-v1/scripts.txt b/doc/website-v1/scripts.txt
deleted file mode 100644
index 2093093..0000000
--- a/doc/website-v1/scripts.txt
+++ /dev/null
@@ -1,445 +0,0 @@
-= 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.adoc
similarity index 99%
rename from doc/website-v1/start-guide.txt
rename to doc/website-v1/start-guide.adoc
index 5b3810b..ee034cf 100644
--- a/doc/website-v1/start-guide.txt
+++ b/doc/website-v1/start-guide.adoc
@@ -19,7 +19,7 @@ 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
+crm --version
 ........
 
 .Example cluster
diff --git a/hb_report/Makefile.am b/hb_report/Makefile.am
deleted file mode 100644
index 5a745c3..0000000
--- a/hb_report/Makefile.am
+++ /dev/null
@@ -1,25 +0,0 @@
-#
-# 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
index 7b35c98..cec33a8 100644
--- a/hb_report/ha_cf_support.sh
+++ b/hb_report/ha_cf_support.sh
@@ -1,19 +1,5 @@
- # 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
- #
+# Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# See COPYING for license information.
 
 #
 # Stack specific part (heartbeat)
diff --git a/hb_report/hb_report.in b/hb_report/hb_report.in
index 916621d..cf34857 100755
--- a/hb_report/hb_report.in
+++ b/hb_report/hb_report.in
@@ -1,25 +1,13 @@
 #!/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
- #
+# Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# See COPYING for license information.
 
 . @OCF_ROOT_DIR@/lib/heartbeat/ocf-shellfuncs
 
-HA_NOARCHBIN=@datadir@/@PACKAGE_NAME@
+prefix=@prefix@
+datarootdir=@datarootdir@
+datadir=@datadir@
+HA_NOARCHBIN=${datadir}/@PACKAGE_NAME@/hb_report
 
 . $HA_NOARCHBIN/utillib.sh
 
@@ -44,15 +32,15 @@ LOG_PATTERNS="CRIT: ERROR:"
 # Important events
 #
 # Patterns format:
-#	title	extended_regexp
+#  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
+membership crmd.*(NEW|LOST)|pcmk.*(lost|memb|LOST|MEMB):
+quorum crmd.*Updating.quorum.status|crmd.*quorum.(lost|ac?quir)
+pause Process.pause.detected
+resources lrmd.*(start|stop)
+stonith crmd.*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|Requesting.shutdown|Shutdown.complete
 "
 
 init_tmpfiles
@@ -142,7 +130,7 @@ EOF
 	exit
 }
 version() {
-	echo "@PACKAGE_NAME@: @PACKAGE_VERSION@ (@BUILD_VERSION@)"
+	echo "@PACKAGE_NAME@: @PACKAGE_VERSION@"
 	exit
 }
 #
@@ -164,7 +152,8 @@ setvarsanddefaults() {
 	# logs to collect in addition
 	# NB: they all have to be in syslog format
 	#
-	EXTRA_LOGS="/var/log/messages"
+	EXTRA_LOGS="/var/log/messages /var/log/pacemaker.log"
+	PCMK_LOG="/var/log/pacemaker.log"
 	# used only by the master
 	NO_SSH=""
 	SSH_USER=""
@@ -306,19 +295,28 @@ logmark() {
 #
 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
+		return
+	fi
+
+	if [ -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}"
+		return
 	fi
+
+	if [ -f "$PCMK_LOG" ]; then
+		echo $PCMK_LOG
+		return
+	fi
+
+	echo ${HA_DEBUGFILE:-$HA_LOGFILE}
+	[ "${HA_DEBUGFILE:-$HA_LOGFILE}" ] &&
+		debug "will try with ${HA_DEBUGFILE:-$HA_LOGFILE}"
 }
 
 #
@@ -332,8 +330,10 @@ find_decompressor() {
 		echo "gzip -dc"
 	elif echo $1 | grep -qs 'xz$'; then
 		echo "xz -dc"
-	else
+	elif file $1 | grep -qs 'text'; then
 		echo "cat"
+	else
+		echo "echo"
 	fi
 }
 #
@@ -535,12 +535,12 @@ 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"
+PCMK_LOG="$PCMK_LOG"
 USER_CLUSTER_TYPE="$USER_CLUSTER_TYPE"
 CONF="$CONF"
 B_CONF="$B_CONF"
@@ -564,11 +564,11 @@ start_slave_collector() {
 	dumpenv |
 	if [ "$node" = "$WE" ]; then
 		debug "running: $LOCAL_SUDO hb_report __slave"
-		$LOCAL_SUDO @datadir@/@PACKAGE_NAME@/hb_report __slave
+		$LOCAL_SUDO ${HA_NOARCHBIN}/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"
+			"$SUDO ${HA_NOARCHBIN}/hb_report __slave"
 	fi | (cd $WORKDIR && tar xf -)
 }
 
@@ -742,7 +742,7 @@ getconfigurations() {
 #
 sys_info() {
 	cluster_info
-	@datadir@/@PACKAGE_NAME@/hb_report -V # our info
+	${HA_NOARCHBIN}/hb_report -V # our info
 	echo "resource-agents: `grep 'Build version:' @OCF_ROOT_DIR@/lib/heartbeat/ocf-shellfuncs`"
 	crm_info
 	pkg_versions $PACKAGES
@@ -1049,10 +1049,12 @@ pickcompress() {
 }
 # get the right part of the log
 getlog() {
-	local cnt
 	local outf
 	outf=$WORKDIR/$HALOG_F
 
+	# collect journal from systemd
+	collect_journal $FROM_TIME $TO_TIME $WORKDIR/$JOURNAL_F
+
 	if [ "$HA_LOG" ]; then  # log provided by the user?
 		[ -f "$HA_LOG" ] || {  # not present
 			is_collector ||  # warning if not on slave
@@ -1062,8 +1064,6 @@ getlog() {
 	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
@@ -1073,10 +1073,6 @@ getlog() {
 		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"
@@ -1285,7 +1281,7 @@ 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
+resource-agents cluster-glue libglue2 ldirectord libqb0
 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
@@ -1412,7 +1408,7 @@ 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
+	sleep 2
 fi
 
 #
diff --git a/hb_report/openais_conf_support.sh b/hb_report/openais_conf_support.sh
index b96d1aa..69e0e4b 100644
--- a/hb_report/openais_conf_support.sh
+++ b/hb_report/openais_conf_support.sh
@@ -1,19 +1,5 @@
- # 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
- #
+# Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# See COPYING for license information.
 
 #
 # Stack specific part (openais)
diff --git a/hb_report/utillib.sh b/hb_report/utillib.sh
index 0fcab80..ff54df8 100644
--- a/hb_report/utillib.sh
+++ b/hb_report/utillib.sh
@@ -1,19 +1,5 @@
- # 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
- #
+# Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# See COPYING for license information.
 
 #
 # figure out the cluster type, depending on the process list
@@ -131,6 +117,7 @@ findmsg() {
 	favourites="ha-*"
 	mark=$1
 	log=""
+
 	for d in $syslogdirs; do
 		[ -d $d ] || continue
 		log=`grep -l -e "$mark" $d/$favourites` && break
@@ -138,6 +125,7 @@ findmsg() {
 		log=`grep -l -e "$mark" $d/*` && break
 		test "$log" && break
 	done 2>/dev/null
+
 	[ "$log" ] &&
 		ls -t $log | tr '\n' ' '
 	[ "$log" ] &&
@@ -387,6 +375,10 @@ fetchpkg_zypper() {
 	local pkg
 	debug "get debuginfo packages using zypper: $@"
 	zypper -qn ref > /dev/null
+	# use --ignore-unknown if available, much faster
+	# (2 is zypper exit code for syntax/usage)
+	zypper -qn --ignore-unknown install -C $@ >/dev/null
+	[ $? -ne 2 ] && return
 	for pkg in $@; do
 		zypper -qn install -C $pkg >/dev/null
 	done
diff --git a/modules/Makefile.am b/modules/Makefile.am
deleted file mode 100644
index f190be1..0000000
--- a/modules/Makefile.am
+++ /dev/null
@@ -1,81 +0,0 @@
-#
-# 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/cache.py b/modules/cache.py
index 53f4a9b..493e755 100644
--- a/modules/cache.py
+++ b/modules/cache.py
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 import time
 
diff --git a/modules/cibconfig.py b/modules/cibconfig.py
index ceeb68d..fa3b87f 100644
--- a/modules/cibconfig.py
+++ b/modules/cibconfig.py
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 import copy
 from lxml import etree
@@ -22,45 +8,46 @@ 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
+from collections import defaultdict
+from . import config
+from . import options
+from . import constants
+from . import tmpfiles
+from .parse import CliParser
+from . import clidisplay
+from .cibstatus import cib_status
+from . import idmgmt
+from .ra import get_ra, get_properties_list, get_pe_meta
+from . 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
+from . 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, is_related, check_id_ref
+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
+from . import cibverify
 
 
 def show_unrecognized_elems(cib_elem):
@@ -74,7 +61,7 @@ def show_unrecognized_elems(cib_elem):
         if is_defaults(topnode) or topnode.tag == "fencing-topology":
             continue
         for c in topnode.iterchildren():
-            if not c.tag in cib_object_map:
+            if c.tag not in cib_object_map:
                 common_warn("unrecognized CIB element %s" % c.tag)
                 rc = False
     return rc
@@ -293,43 +280,38 @@ class CibObjectSet(object):
         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:
+            s = self._pre_edit(s)
+            filehash = hash(s)
+            tmp = str2tmp(s)
+            if not tmp:
+                return False
+            while not rc:
+                if edit_file(tmp) != 0:
+                    break
+                s = open(tmp).read()
+                if hash(s) != filehash:
+                    ok = self.save(self._post_edit(s))
+                    if not ok and config.core.force:
+                        common_err("Save failed and --force is set, " +
+                                   "aborting edit to avoid infinite loop")
+                    elif not ok and ask("Edit or discard changes (yes to edit, no to discard)?"):
+                        continue
+                rc = True
             os.unlink(tmp)
-        except OSError:
-            pass
+        except OSError, e:
+            common_debug("unlink(%s) failure: %s" % (tmp, e))
+        except IOError, msg:
+            common_err(msg)
         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()
+        with clidisplay.nopretty():
+            s = self.repr()
         # don't allow edit if one or more elements were not
         # found
         if not self.search_rc:
@@ -349,9 +331,8 @@ class CibObjectSet(object):
         return self.save(outp)
 
     def filter(self, filter):
-        clidisplay.disable_pretty()
-        s = self.repr(format=-1)
-        clidisplay.enable_pretty()
+        with clidisplay.nopretty():
+            s = self.repr(format=-1)
         # don't allow filter if one or more elements were not
         # found
         if not self.search_rc:
@@ -363,9 +344,8 @@ class CibObjectSet(object):
         if not f:
             return False
         rc = True
-        clidisplay.disable_pretty()
-        s = self.repr()
-        clidisplay.enable_pretty()
+        with clidisplay.nopretty():
+            s = self.repr()
         if s:
             f.write(s)
             f.write('\n')
@@ -417,9 +397,8 @@ class CibObjectSet(object):
 
     def show(self):
         s = self.repr()
-        if not s:
-            return self.search_rc
-        page_string(s)
+        if s:
+            page_string(s)
         return self.search_rc
 
     def import_file(self, method, fname):
@@ -431,7 +410,7 @@ class CibObjectSet(object):
         f = self._open_url(fname)
         if not f:
             return False
-        s = ''.join(f)
+        s = f.read()
         if f != sys.stdin:
             f.close()
         return self.save(s, no_remove=True, method=method)
@@ -474,22 +453,14 @@ class CibObjectSet(object):
             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
+            for p in r_node.xpath("./instance_attributes/nvpair"):
+                name, value = p.get("name"), p.get("value")
+                if value is None:
+                    continue
+                # don't fail if the meta-data doesn't contain the
+                # expected attributes
+                if name in ra_params and ra_params[name].get("unique") == "1":
+                    clash_dict[(ra_class, ra_provider, ra_type, name, value)].append(ra_id)
             return
         # we check the whole CIB for clashes as a clash may originate between
         # an object already committed and a new one
@@ -498,7 +469,7 @@ class CibObjectSet(object):
                          if o.obj_type == "primitive"])
         if not check_set:
             return 0
-        clash_dict = {}
+        clash_dict = defaultdict(list)
         for obj in set_obj_all.obj_set:
             node = obj.node
             if is_primitive(node):
@@ -519,32 +490,10 @@ class CibObjectSet(object):
         Test objects for sanity. This is about semantics.
         '''
         rc = self.__check_unique_clash(set_obj_all)
-        for obj in self.obj_set:
+        for obj in sorted(self.obj_set, key=lambda x: x.obj_id):
             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):
     '''
@@ -556,10 +505,8 @@ class CibObjectSetCli(CibObjectSet):
         CibObjectSet.__init__(self, *args)
 
     def repr_nopretty(self, format=1):
-        clidisplay.disable_pretty()
-        s = self.repr(format=format)
-        clidisplay.enable_pretty()
-        return s
+        with clidisplay.nopretty():
+            return self.repr(format=format)
 
     def repr(self, format=1):
         "Return a string containing cli format of all objects."
@@ -601,9 +548,7 @@ class CibObjectSetCli(CibObjectSet):
         coming from edit). The original CIB is preserved and no
         changes are made.
         '''
-        edit_d = {}
-        id_set = oset()
-        del_set = oset()
+        diff = CibDiff(self)
         rc = True
         err_buf.start_tmp_lineno()
         cp = CliParser()
@@ -611,32 +556,17 @@ class CibObjectSetCli(CibObjectSet):
             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
+                rc = rc and diff.add(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)
+        rc = diff.apply(cib_factory, mode='cli', no_remove=no_remove, method=method)
         if not rc:
             self._initialize()
         return rc
@@ -670,29 +600,12 @@ class CibObjectSetRaw(CibObjectSet):
         if not show_unrecognized_elems(cib_elem):
             return False
         rc = True
-        id_set = oset()
-        del_set = oset()
-        edit_d = {}
+        diff = CibDiff(self)
         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
+            rc = diff.add(node)
         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)
+        rc = diff.apply(cib_factory, mode='xml', no_remove=no_remove, method=method)
         if not rc:
             self._initialize()
         return rc
@@ -700,9 +613,8 @@ class CibObjectSetRaw(CibObjectSet):
     def verify(self):
         if not self.obj_set:
             return True
-        clidisplay.disable_pretty()
-        cib = self.repr(format=-1)
-        clidisplay.enable_pretty()
+        with clidisplay.nopretty():
+            cib = self.repr(format=-1)
         rc = cibverify.verify(cib)
 
         if rc not in (0, 1):
@@ -789,11 +701,7 @@ def resolve_idref(node):
             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)
+    check_id_ref(cib_factory.get_cib(), id_ref)
     return id_ref
 
 
@@ -810,7 +718,9 @@ def resolve_references(node):
         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)
+        # TODO: This always refers to a resource ATM.
+        # Handle case where it may refer to a node name?
+        obj = cib_factory.find_resource(child_id)
         common_debug("resolve_references: %s -> %s" % (child_id, obj))
         if obj is not None:
             newnode = copy.deepcopy(obj.node)
@@ -898,6 +808,7 @@ def parse_cli_to_xml(cli, oldnode=None, validation=None):
         return None, None, None
     return postprocess_cli(node, oldnode)
 
+
 #
 # cib element classes (CibObject the parent class)
 #
@@ -909,7 +820,7 @@ class CibObject(object):
     set_names = {}
 
     def __init__(self, xml_obj_type):
-        if not xml_obj_type in cib_object_map:
+        if xml_obj_type not in cib_object_map:
             unsupported_err(xml_obj_type)
             return
         self.obj_type = cib_object_map[xml_obj_type][0]
@@ -940,16 +851,11 @@ class CibObject(object):
                                 len(self.children))
 
     def _repr_cli_xml(self, format):
-        if format < 0:
-            clidisplay.disable_pretty()
-        try:
+        with clidisplay.nopretty(format < 0):
             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:
@@ -990,9 +896,7 @@ class CibObject(object):
         if self.nocli:
             return self._repr_cli_xml(format)
         l = []
-        if format < 0:
-            clidisplay.disable_pretty()
-        try:
+        with clidisplay.nopretty(format < 0):
             head_s = self._repr_cli_head(format)
             # everybody must have a head
             if not head_s:
@@ -1010,9 +914,6 @@ class CibObject(object):
                 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):
         '''
@@ -1022,7 +923,7 @@ class CibObject(object):
         also show rule expressions if found
         '''
 
-        has_nvpairs = len(node.xpath('.//nvpair')) > 0
+        # has_nvpairs = len(node.xpath('.//nvpair')) > 0
         idref = node.get('id-ref')
 
         # don't skip empty sets: skipping these breaks
@@ -1166,9 +1067,8 @@ class CibObject(object):
         '''
         if self.node is None:
             return True
-        clidisplay.disable_pretty()
-        cli_text = self.repr_cli(format=0)
-        clidisplay.enable_pretty()
+        with clidisplay.nopretty():
+            cli_text = self.repr_cli(format=0)
         if not cli_text:
             common_debug("validation failed: %s" % (etree.tostring(self.node)))
             return False
@@ -1191,7 +1091,7 @@ class CibObject(object):
         Check if all operation attributes are supported by the
         schema.
         '''
-        rc = True
+        rc = 0
         op_id = op_node.get("name")
         for name in op_node.keys():
             vals = schema.rng_attr_values(op_node.tag, name)
@@ -1201,14 +1101,14 @@ class CibObject(object):
             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
+                rc = 1
         return rc
 
     def _check_ops_attributes(self):
         '''
         Check if operation attributes settings are valid.
         '''
-        rc = True
+        rc = 0
         if self.node is None:
             return rc
         for op_node in self.node.xpath("operations/op"):
@@ -1239,6 +1139,11 @@ class CibObject(object):
         else:
             return self
 
+    def meta_attributes(self, name):
+        "Returns all meta attribute values with the given name"
+        v = self.node.xpath('./meta_attributes/nvpair[@name="%s"]/@value' % (name))
+        return v
+
     def find_child_in_node(self, child):
         for c in self.node.iterchildren():
             if c.tag == child.obj_type and \
@@ -1663,6 +1568,28 @@ class CibContainer(CibObject):
             child_rsc.repr_gv(sg_obj, from_grp=True)
 
 
+def _check_if_constraint_ref_is_child(obj):
+    """
+    Used by check_sanity for constraints to verify
+    that referenced resources are not children in
+    a container.
+    """
+    rc = 0
+    for rscid in obj._referenced_resources():
+        tgt = cib_factory.find_object(rscid)
+        if not tgt:
+            common_warn("%s: resource %s does not exist" % (obj.obj_id, rscid))
+            rc = 1
+        elif tgt.parent and tgt.parent.obj_type == "group":
+            if obj.obj_type == "colocation":
+                common_warn("%s: resource %s is grouped, constraints should apply to the group" % (obj.obj_id, rscid))
+                rc = 1
+        elif tgt.parent and tgt.parent.obj_type in constants.container_tags:
+            common_warn("%s: resource %s ambiguous, apply constraints to container" % (obj.obj_id, rscid))
+            rc = 1
+    return rc
+
+
 class CibLocation(CibObject):
     '''
     Location constraint.
@@ -1733,8 +1660,15 @@ class CibLocation(CibObject):
                 if uname and uname.lower() not in ids:
                     common_warn("%s: referenced node %s does not exist" % (self.obj_id, uname))
                     rc = 1
+        rc2 = _check_if_constraint_ref_is_child(self)
+        if rc2 > rc:
+            rc = rc2
         return rc
 
+    def _referenced_resources(self):
+        ret = self.node.xpath('.//resource_set/resource_ref/@id')
+        return ret or [self.node.get("rsc")]
+
     def repr_gv(self, gv_obj, from_grp=False):
         '''
         What to do with the location constraint?
@@ -1747,13 +1681,14 @@ class CibLocation(CibObject):
             score_n = self.node.findall("rule")[0]
             exp = self.node.xpath("rule/expression")[0]
             pref_node = exp.get("value")
-        else:
+        if pref_node is None:
             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)
+        if rsc_id is not None:
+            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):
@@ -1870,6 +1805,23 @@ class CibSimpleConstraint(CibObject):
                 self.node.get("first"),
                 self.node.get("then")])
 
+    def _referenced_resources(self):
+        ret = self.node.xpath('.//resource_set/resource_ref/@id')
+        if ret:
+            return ret
+        if self.obj_type == "order":
+            return [self.node.get("first"), self.node.get("then")]
+        elif self.obj_type == "colocation":
+            return [self.node.get("rsc"), self.node.get("with-rsc")]
+        elif self.node.get("rsc"):
+            return [self.node.get("rsc")]
+
+    def check_sanity(self):
+        if self.node is None:
+            common_err("%s: no xml (strange)" % self.obj_id)
+            return utils.get_check_rc()
+        return _check_if_constraint_ref_is_child(self)
+
 
 class CibRscTicket(CibSimpleConstraint):
     '''
@@ -1919,6 +1871,12 @@ class CibProperty(CibObject):
             return utils.get_check_rc()
         l = []
         if self.obj_type == "property":
+            # don't check property sets which are not
+            # "cib-bootstrap-options", they are probably used by
+            # some resource agents such as mysql to store RA
+            # specific state
+            if self.obj_id != cib_object_map[self.xml_obj_type][3]:
+                return 0
             l = get_properties_list()
             l += constants.extra_cluster_properties
         elif self.obj_type == "op_defaults":
@@ -1962,7 +1920,10 @@ class CibFencingOrder(CibObject):
         s = clidisplay.keyword(self.obj_type)
         d = odict()
         for c in self.node.iterchildren("fencing-level"):
-            target = c.get("target")
+            if "target-attribute" in c.attrib:
+                target = (c.get("target-attribute"), c.get("target-value"))
+            else:
+                target = c.get("target")
             if target not in d:
                 d[target] = {}
             d[target][c.get("index")] = c.get("devices")
@@ -1976,7 +1937,13 @@ class CibFencingOrder(CibObject):
             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]))
+
+        def fmt_target(tgt):
+            if isinstance(tgt, tuple):
+                return "attr:%s=%s" % tgt
+            else:
+                return tgt + ":"
+        return cli_format([s] + ["%s %s" % (fmt_target(x), ' '.join(dd[x]))
                                  for x in dd.keys()],
                           break_lines=(format > 0))
 
@@ -1992,7 +1959,7 @@ class CibFencingOrder(CibObject):
             return utils.get_check_rc()
         rc = 0
         nl = self.node.findall("fencing-level")
-        for target in [x.get("target") for x in nl]:
+        for target in [x.get("target") for x in nl if x.get("target") is not None]:
             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
@@ -2043,11 +2010,10 @@ class CibTag(CibObject):
     '''
 
     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)
+        return ' '.join([clidisplay.keyword(self.obj_type),
+                         clidisplay.id(self.obj_id)] +
+                        [clidisplay.rscref(c.get('id'))
+                         for c in self.node.iterchildren() if not is_comment(c)])
 
 
 #
@@ -2062,10 +2028,10 @@ cib_piped = "cibadmin -p"
 
 def get_default_timeout():
     t = cib_factory.get_op_default("timeout")
-    if t:
+    if t is not None:
         return t
     t = cib_factory.get_property("default-action-timeout")
-    if t:
+    if t is not None:
         return t
     try:
         return get_pe_meta().param_default("default-action-timeout")
@@ -2116,7 +2082,121 @@ def can_migrate(node):
     return 'true' in node.xpath('.//nvpair[@name="allow-migrate"]/@value')
 
 
-cib_upgrade = "cibadmin --upgrade --force"
+class CibDiff(object):
+    '''
+    Represents a cib edit order.
+    Is complicated by the fact that
+    nodes and resources can have
+    colliding ids.
+
+    Can carry changes either as CLI objects
+    or as XML statements.
+    '''
+    def __init__(self, objset):
+        self.objset = objset
+        self._node_set = oset()
+        self._nodes = {}
+        self._rsc_set = oset()
+        self._resources = {}
+
+    def add(self, item):
+        obj_id = id_for_node(item)
+        is_node = item.tag == 'node'
+        if obj_id is None:
+            common_err("element %s has no id!" %
+                       etree.tostring(item, pretty_print=True))
+            return False
+        elif is_node and obj_id in self._node_set:
+            common_err("Duplicate node: %s" % (obj_id))
+            return False
+        elif not is_node and obj_id in self._rsc_set:
+            common_err("Duplicate resource: %s" % (obj_id))
+            return False
+        elif is_node:
+            self._node_set.add(obj_id)
+            self._nodes[obj_id] = item
+        else:
+            self._rsc_set.add(obj_id)
+            self._resources[obj_id] = item
+        return True
+
+    def _obj_type(self, nid):
+        for obj in self.objset.all_set:
+            if obj.obj_id == nid:
+                return obj.obj_type
+        return None
+
+    def _is_node(self, nid):
+        for obj in self.objset.all_set:
+            if obj.obj_id == nid and obj.obj_type == 'node':
+                return True
+        return False
+
+    def _is_resource(self, nid):
+        for obj in self.objset.all_set:
+            if obj.obj_id == nid and obj.obj_type != 'node':
+                return True
+        return False
+
+    def _obj_nodes(self):
+        return oset([n for n in self.objset.obj_ids
+                     if self._is_node(n)])
+
+    def _obj_resources(self):
+        return oset([n for n in self.objset.obj_ids
+                     if self._is_resource(n)])
+
+    def _is_edit_valid(self, id_set, existing):
+        '''
+        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.objset.locked_ids
+        rscstat = RscState()
+        if not_allowed:
+            common_err("Elements %s already exist" %
+                       ', '.join(list(not_allowed)))
+            rc = False
+        delete_set = existing - 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
+
+    def apply(self, factory, mode='cli', no_remove=False, method='replace'):
+        rc = True
+
+        edited_nodes = self._nodes.copy()
+        edited_resources = self._resources.copy()
+
+        def calc_sets(input_set, existing):
+            rc = True
+            if not no_remove:
+                rc = self._is_edit_valid(input_set, existing)
+                del_set = existing - (input_set)
+            else:
+                del_set = oset()
+            mk_set = (input_set) - existing
+            upd_set = (input_set) & existing
+            return rc, mk_set, upd_set, del_set
+
+        if not rc:
+            return rc
+
+        for e, s, existing in ((edited_nodes, self._node_set, self._obj_nodes()),
+                               (edited_resources, self._rsc_set, self._obj_resources())):
+            rc, mk, upd, rm = calc_sets(s, existing)
+            if not rc:
+                return rc
+            rc = cib_factory.set_update(e, mk, upd, rm, upd_type=mode, method=method)
+            if not rc:
+                return rc
+        return rc
 
 
 class CibFactory(object):
@@ -2132,8 +2212,6 @@ class CibFactory(object):
         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):
@@ -2154,7 +2232,7 @@ class CibFactory(object):
     #
 
     def _check_parent(self, obj, parent):
-        if not obj in parent.children:
+        if obj not in parent.children:
             common_err("object %s does not reference its child %s" %
                        (parent.obj_id, obj.obj_id))
             return False
@@ -2206,9 +2284,8 @@ class CibFactory(object):
         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
+        if not schema.is_supported(schema_st):
+            common_warn("schema %s is not supported by the shell" % schema_st)
         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())
@@ -2226,7 +2303,7 @@ class CibFactory(object):
             # 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)
+            common_err("Schema %s conflicts with current configuration" % schema_st)
             return 4
         self.cib_attrs["validate-with"] = schema_st
         self.new_schema = True
@@ -2245,18 +2322,24 @@ class CibFactory(object):
         '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):
+        # if no schema is configured, just assume that it validates
+        if not validator or schema.is_supported(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.'
+    def upgrade_validate_with(self, force=False):
+        """Upgrade the CIB.
+
+        Requires the force argument to be set if
+        validate-with is configured to anything other than
+        0.6.
+        """
         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
+            return ext_cmd("cibadmin --upgrade --force") == 0
 
     def _import_cib(self, cib_elem):
         'Parse the current CIB (from cibadmin -Q).'
@@ -2264,8 +2347,7 @@ class CibFactory(object):
         if self.cib_elem is None:
             return False
         if not self.is_cib_supported():
-            self.reset()
-            return False
+            common_warn("CIB schema is not supported by the shell")
         self._get_cib_attributes(self.cib_elem)
         schema.init_schema(self.cib_elem)
         return True
@@ -2356,11 +2438,11 @@ class CibFactory(object):
             for obj in self.remove_queue:
                 obj._dump_state()
 
-    def commit(self, force=False):
+    def commit(self, force=False, replace=False):
         'Commit the configuration to the CIB.'
         if not self.is_cib_sane():
             return False
-        if cibadmin_can_patch():
+        if not replace and cibadmin_can_patch():
             rc = self._patch_cib(force)
         else:
             rc = self._replace_cib(force)
@@ -2580,7 +2662,7 @@ class CibFactory(object):
         # 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)
+            obj.node = self.find_xml_node(obj.xml_obj_type, obj.obj_id)
             self._update_links(obj)
         idmgmt.pop_state()
         return self.check_structure()
@@ -2607,9 +2689,10 @@ class CibFactory(object):
 
     def find_objects(self, obj_id):
         "Find objects for id (can be a wildcard-glob)."
+        def matchfn(x):
+            return x and fnmatch.fnmatch(x, obj_id)
         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):
@@ -2627,9 +2710,34 @@ class CibFactory(object):
         if objs is None:
             return None
         if len(objs) > 0:
+            for obj in objs:
+                if obj.obj_type != 'node':
+                    return obj
             return objs[0]
         return None
 
+    def find_resource(self, obj_id):
+        if not self.is_cib_sane():
+            return None
+        objs = self.find_objects(obj_id)
+        if objs is None:
+            return None
+        for obj in objs:
+            if obj.obj_type != 'node':
+                return obj
+        return None
+
+    def find_node(self, obj_id):
+        if not self.is_cib_sane():
+            return None
+        objs = self.find_objects(obj_id)
+        if objs is None:
+            return None
+        for obj in objs:
+            if obj.obj_type == 'node':
+                return obj
+        return None
+
     #
     # tab completion functions
     #
@@ -2641,6 +2749,10 @@ class CibFactory(object):
         "List of object types (for completion)"
         return list(set([x.obj_type for x in self.cib_objects]))
 
+    def tag_list(self):
+        "List of tags (for completion)"
+        return list(set([x.obj_id for x in self.cib_objects if x.obj_type == "tag"]))
+
     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"]
@@ -2661,8 +2773,8 @@ class CibFactory(object):
 
     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"]
+        return sorted([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)."
@@ -2687,18 +2799,17 @@ class CibFactory(object):
     #
     # 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":
+    def find_container_child(self, node):
+        "Find an object which may be the child in a container."
+        for obj in reversed(self.cib_objects):
+            if node.tag == "fencing-topology" and obj.xml_obj_type == "fencing-topology":
                 return obj
-            if node.get("id") == obj.obj_id:
+            if node.tag == obj.node.tag and 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."
+    def find_xml_node(self, tag, id, strict=True):
+        "Find a xml node of this type with this id."
         try:
             if tag in constants.defaults_tags:
                 expr = '//%s/meta_attributes[@id="%s"]' % (tag, id)
@@ -2728,7 +2839,7 @@ class CibFactory(object):
             return False
         rc = True
         for obj_id in args:
-            obj = self.find_object(obj_id)
+            obj = self.find_resource(obj_id)
             if not obj:
                 no_object_err(obj_id)
                 rc = False
@@ -2808,7 +2919,7 @@ class CibFactory(object):
         id to reference.
         '''
         self.id_refs[id_ref] = attr_list_type
-        obj = self.find_object(id_ref)
+        obj = self.find_resource(id_ref)
         if obj:
             nodes = obj.node.xpath(".//%s" % attr_list_type)
             if len(nodes) > 1:
@@ -2818,11 +2929,7 @@ class CibFactory(object):
                 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)
+        check_id_ref(self.cib_elem, id_ref)
         return id_ref
 
     def _get_attr_value(self, obj_type, attr):
@@ -2854,6 +2961,10 @@ class CibFactory(object):
     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))
+        existing = self.find_object(obj_id)
+        if existing and [obj_type, existing.obj_type].count("node") != 1:
+            common_error("Cannot create %s:%s: Found existing %s:%s" % (obj_id, obj_type, obj_id, existing.obj_type))
+            return None
         xml_obj_type = backtrans.get(obj_type)
         v = cib_object_map.get(xml_obj_type)
         if v is None:
@@ -2882,7 +2993,7 @@ class CibFactory(object):
         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')]
+            matches = [cib_factory.find_resource(o) for o in mt.node.xpath('./obj_ref/@id')]
             ret += [m for m in matches if m is not None]
         return ret
 
@@ -2900,6 +3011,12 @@ class CibFactory(object):
                 obj_set |= oset(self.get_elems_on_type(spec))
             elif spec.startswith("tag:"):
                 obj_set |= oset(self.get_elems_on_tag(spec))
+            elif spec.startswith("related:"):
+                name = spec[len("related:"):]
+                obj_set |= oset(self.find_objects(name) or [])
+                obj = self.find_object(name)
+                if obj is not None:
+                    obj_set |= oset(self.related_elements(obj))
             else:
                 objs = self.find_objects(spec) or []
                 for obj in objs:
@@ -2927,7 +3044,7 @@ class CibFactory(object):
         rc = True
         constraint_id = node.get("id")
         for obj_id in referenced_resources(node):
-            if not self.find_object(obj_id):
+            if not self.find_resource(obj_id):
                 constraint_norefobj_err(constraint_id, obj_id)
                 rc = False
         return rc
@@ -2965,7 +3082,7 @@ class CibFactory(object):
 
     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)
+        child = self.find_resource(child_id)
         if not child:
             no_object_err(child_id)
             return False
@@ -2976,7 +3093,7 @@ class CibFactory(object):
         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:
+        if child.obj_type not 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
@@ -3034,7 +3151,7 @@ class CibFactory(object):
         '''Add an op to a primitive.'''
         # does the referenced primitive exist
         rsc_id = node.get('rsc')
-        rsc_obj = self.find_object(rsc_id)
+        rsc_obj = self.find_resource(rsc_id)
         if not rsc_obj:
             no_object_err(rsc_id)
             return None
@@ -3067,7 +3184,7 @@ class CibFactory(object):
         if obj_type == "op":
             return self.add_op(elem)
         if obj_type == "node":
-            obj = self.find_object(obj_id)
+            obj = self.find_node(obj_id)
             # make an exception and allow updating nodes
             if obj:
                 self.merge_from_cli(obj, elem)
@@ -3138,17 +3255,32 @@ class CibFactory(object):
         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))
+        common_debug("_cli_set_update: mk=%s, upd=%s, del=%s" % (mk_set, upd_set, del_set))
         test_l = []
 
         def obj_is_container(x):
-            obj = self.find_object(x)
+            obj = self.find_resource(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)]
+        def obj_is_constraint(x):
+            obj = self.find_resource(x)
+            return obj and is_constraint(obj.node)
+
+        del_constraints = []
+        del_containers = []
+        del_objs = []
+        for x in del_set:
+            if obj_is_constraint(x):
+                del_constraints.append(x)
+            elif obj_is_container(x):
+                del_containers.append(x)
+            else:
+                del_objs.append(x)
 
-        # delete containers first in case objects are moved elsewhere
+        # delete constraints and containers first in case objects are moved elsewhere
+        if not self.delete(*del_constraints):
+            common_debug("delete %s failed" % (list(del_set)))
+            return False
         if not self.delete(*del_containers):
             common_debug("delete %s failed" % (list(del_set)))
             return False
@@ -3162,7 +3294,10 @@ class CibFactory(object):
             test_l.append(obj)
 
         for id in upd_set:
-            obj = self.find_object(id)
+            if edit_d[id].tag == 'node':
+                obj = self.find_node(id)
+            else:
+                obj = self.find_resource(id)
             if not obj:
                 common_debug("%s not found!" % (id))
                 return False
@@ -3175,7 +3310,8 @@ class CibFactory(object):
                              (obj, etree.tostring(node), method))
                 return False
             test_l.append(obj)
-        if not self.delete(*del_objs):
+
+        if not self.delete(*reversed(del_objs)):
             common_debug("delete %s failed" % (list(del_set)))
             return False
         rc = True
@@ -3201,7 +3337,10 @@ class CibFactory(object):
                 return False
             test_l.append(obj)
         for id in upd_set:
-            obj = self.find_object(id)
+            if edit_d[id].tag == 'node':
+                obj = self.find_node(id)
+            else:
+                obj = self.find_resource(id)
             if not obj:
                 return False
             if not self.update_from_node(obj, edit_d[id]):
@@ -3244,7 +3383,7 @@ class CibFactory(object):
         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 = [self.find_resource(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
@@ -3296,7 +3435,7 @@ class CibFactory(object):
         return True
 
     def test_element(self, obj):
-        if not obj.xml_obj_type in constants.defaults_tags:
+        if obj.xml_obj_type not in constants.defaults_tags:
             if not self._verify_element(obj):
                 return False
         if utils.is_check_always() and obj.check_sanity() > 1:
@@ -3313,7 +3452,7 @@ class CibFactory(object):
             return
         for c in obj.node.iterchildren():
             if is_child_rsc(c):
-                child = self.find_object_for_node(c)
+                child = self.find_container_child(c)
                 if not child:
                     missing_obj_err(c)
                     continue
@@ -3350,9 +3489,12 @@ class CibFactory(object):
         if obj_type not in constants.container_tags:
             return True
 
-        for c in node.iterchildren('primitive'):
+        # bsc#959895: also process cloned groups
+        for c in node.iterchildren():
+            if c.tag not in ('primitive', 'group'):
+                continue
             pid = c.get('id')
-            child_obj = self.find_object(pid)
+            child_obj = self.find_resource(pid)
             if child_obj is None:
                 child_obj = self.create_from_node(copy.deepcopy(c))
                 if not child_obj:
@@ -3411,15 +3553,17 @@ class CibFactory(object):
                 err_buf.info("constraint %s updated" % str(c_obj))
 
     def related_constraints(self, obj):
+        def related_constraint(obj2):
+            return is_constraint(obj2.node) and rsc_constraint(obj.obj_id, obj2.node)
         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
+        return [x for x in self.cib_objects if related_constraint(x)]
+
+    def related_elements(self, obj):
+        "Both constraints, groups, tags, ..."
+        if not is_resource(obj.node):
+            return []
+        return [x for x in self.cib_objects if is_related(obj.obj_id, x.node)]
 
     def _redirect_children_constraints(self, obj):
         '''
@@ -3536,6 +3680,10 @@ class CibFactory(object):
         rename_id(obj.node, old_id, new_id)
         obj.obj_id = new_id
         idmgmt.rename(old_id, new_id)
+        # FIXME: (bnc#901543)
+        # for each child node; if id starts with "%(old_id)s-" and
+        # is not referenced by anything, change that id as well?
+        # otherwise inner ids will resemble old name, not new
         obj.set_updated()
 
     def erase(self):
@@ -3547,9 +3695,7 @@ class CibFactory(object):
         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"]:
+        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
diff --git a/modules/cibstatus.py b/modules/cibstatus.py
index f857a72..8034b5e 100644
--- a/modules/cibstatus.py
+++ b/modules/cibstatus.py
@@ -1,29 +1,15 @@
 # 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
-#
+# See COPYING for license information.
 
 import os
 from lxml import etree
-import tmpfiles
+from . 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
+from .utils import ext_cmd, show_dot_graph, page_string
+from .msg import common_err, common_info, common_warn
+from . import xmlutil
+from . import utils
+from . import config
 
 
 def get_tag_by_id(node, tag, id):
diff --git a/modules/cibverify.py b/modules/cibverify.py
index 9eb7f50..3d907c0 100644
--- a/modules/cibverify.py
+++ b/modules/cibverify.py
@@ -1,23 +1,9 @@
 # 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
-#
+# See COPYING for license information.
 
 import re
-import utils
-from msg import err_buf
+from . import utils
+from .msg import err_buf
 
 
 cib_verify = "crm_verify --verbose -p"
@@ -35,9 +21,8 @@ def _prettify(line, indent=0):
 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)
+            err_buf.error(_prettify(line, 0))
         else:
-            err_buf.writemsg(line)
+            err_buf.writemsg(_prettify(line, 7))
     return rc
diff --git a/modules/clidisplay.py b/modules/clidisplay.py
index 9e812e6..f41ad70 100644
--- a/modules/clidisplay.py
+++ b/modules/clidisplay.py
@@ -1,25 +1,11 @@
 # 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
-#
+# See COPYING for license information.
 
 """
 Display output for various syntax elements.
 """
 
-import config
+from . import config
 
 
 # Enable colors/upcasing
@@ -36,12 +22,25 @@ def disable_pretty():
     _pretty = False
 
 
+class nopretty(object):
+    def __init__(self, cond=True):
+        self.cond = cond
+
+    def __enter__(self):
+        if self.cond:
+            disable_pretty()
+
+    def __exit__(self, type, value, traceback):
+        if self.cond:
+            enable_pretty()
+
+
 def colors_enabled():
     return 'color' in config.color.style and _pretty
 
 
 def _colorize(s, colors):
-    if colors_enabled():
+    if s and colors_enabled():
         return ''.join(('${%s}' % clr.upper()) for clr in colors) + s + '${NORMAL}'
     return s
 
diff --git a/modules/cliformat.py b/modules/cliformat.py
index 12f13c5..106325b 100644
--- a/modules/cliformat.py
+++ b/modules/cliformat.py
@@ -1,24 +1,10 @@
 # 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
-#
+# See COPYING for license information.
 
-import constants
-import clidisplay
-import utils
-import xmlutil
+from . import constants
+from . import clidisplay
+from . import utils
+from . import xmlutil
 
 
 #
@@ -77,7 +63,7 @@ def cli_operations(node, break_lines=True):
 
 def cli_nvpair(nvp):
     'Converts an nvpair tag or a (name, value) pair to CLI syntax'
-    from cibconfig import cib_factory
+    from .cibconfig import cib_factory
     nodeid = nvp.get('id')
     idref = nvp.get('id-ref')
     name = nvp.get('name')
@@ -104,7 +90,7 @@ def nvpairs2list(node, add_id=False):
     long and therefore obscure the relevant content. For some
     elements, however, they are included (e.g. properties).
     '''
-    import xmlbuilder
+    from . import xmlbuilder
 
     ret = []
     if 'id-ref' in node:
@@ -223,7 +209,7 @@ def cli_exprs(node):
 
 
 def cli_rule(node):
-    from cibconfig import cib_factory
+    from .cibconfig import cib_factory
     s = []
     node_id = node.get("id")
     if node_id and cib_factory.is_id_refd(node.tag, node_id):
@@ -400,8 +386,8 @@ def cli_acl_spec2_format(xml_spec, v):
 
 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('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:
diff --git a/modules/cmd_status.py b/modules/cmd_status.py
index 9edbea5..f8a8b10 100644
--- a/modules/cmd_status.py
+++ b/modules/cmd_status.py
@@ -1,25 +1,69 @@
 # 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
-#
+# See COPYING for license information.
 
-import utils
+import re
+import clidisplay
+from . import utils
 
 _crm_mon = None
 
+_WARNS = ['pending',
+          'complete',
+          'Timed Out',
+          'NOT SUPPORTED',
+          'Error',
+          'Not installed',
+          r'UNKNOWN\!',
+          'Stopped',
+          'standby']
+_OKS = ['Online', 'online', 'ok', 'master', 'Started', 'Master', 'Slave']
+_ERRORS = ['not running',
+           'unknown error',
+           'invalid parameter',
+           'unimplemented feature',
+           'insufficient privileges',
+           'not installed',
+           'not configured',
+           'not running',
+           r'master \(failed\)',
+           'OCF_SIGNAL',
+           'OCF_NOT_SUPPORTED',
+           'OCF_TIMEOUT',
+           'OCF_OTHER_ERROR',
+           'OCF_DEGRADED',
+           'OCF_DEGRADED_MASTER',
+           'unknown',
+           'Unknown',
+           'OFFLINE',
+           'Failed actions']
+
+
+class CrmMonFilter(object):
+    _OK = re.compile(r'(%s)' % '|'.join(_OKS))
+    _WARNS = re.compile(r'(%s)' % '|'.join(_WARNS))
+    _ERROR = re.compile(r'(%s)' % ('|'.join(_ERRORS)))
+    _NODES = re.compile(r'(\d+ Nodes configured)')
+    _RESOURCES = re.compile(r'(\d+ Resources configured)')
+
+    _RESOURCE = re.compile(r'(\S+)(\s+)\((\S+:\S+)\):')
+    _GROUP = re.compile(r'(Resource Group|Clone Set): (\S+)')
+
+    def _filter(self, line):
+        line = self._RESOURCE.sub("%s%s(%s):" % (clidisplay.help_header(r'\1'),
+                                                 r'\2',
+                                                 r'\3'), line)
+        line = self._NODES.sub(clidisplay.help_header(r'\1'), line)
+        line = self._RESOURCES.sub(clidisplay.help_header(r'\1'), line)
+        line = self._GROUP.sub(r'\1: ' + clidisplay.help_header(r'\2'), line)
+        line = self._WARNS.sub(clidisplay.warn(r'\1'), line)
+        line = self._OK.sub(clidisplay.ok(r'\1'), line)
+        line = self._ERROR.sub(clidisplay.error(r'\1'), line)
+        return line
+
+    def __call__(self, text):
+        return '\n'.join([self._filter(line) for line in text.splitlines()]) + '\n'
+
 
 def crm_mon(opts=''):
     """
@@ -63,11 +107,14 @@ def cmd_status(args):
         "noheaders": "-D",
         "detail": "-R",
         "brief": "-b",
+        "full": "-ncrft",
     }
     extra = ' '.join(opts.get(arg, arg) for arg in args)
+    if not args:
+        extra = "-r"
     rc, s = crm_mon(extra)
     if rc != 0:
         raise IOError("crm_mon (rc=%d): %s" % (rc, s))
 
-    utils.page_string(s)
+    utils.page_string(CrmMonFilter()(s))
     return True
diff --git a/modules/command.py b/modules/command.py
index dd722d0..45b115a 100644
--- a/modules/command.py
+++ b/modules/command.py
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 # - Base class for UI levels
 # - Decorators and other helper functions for the UI
@@ -21,9 +7,9 @@
 #   inside the functions.
 
 import inspect
-import help as help_module
-import ui_utils
-from msg import common_debug
+from . import help as help_module
+from . import ui_utils
+from .msg import common_debug
 
 
 def name(n):
@@ -85,7 +71,7 @@ def level(level_class):
 def help(doc):
     '''
     Use to set a help text for a command or level
-    which isn't documented in crm.8.txt.
+    which isn't documented in crm.8.adoc.
 
     The first line of the doc string will be used as
     the short help, the rest will be used as the full
@@ -204,6 +190,40 @@ def _help_completer(args, context):
     return help_module.list_help_topics() + context.current_level().get_completions()
 
 
+def fuzzy_get(items, s):
+    """
+    Finds s in items using a fuzzy
+    matching algorithm:
+
+    1. if exact match, return value
+    2. if unique prefix, return value
+    3. if unique prefix substring, return value
+    """
+    found = items.get(s)
+    if found:
+        return found
+    import re
+
+    def fuzzy_match(rx):
+        matcher = re.compile(rx, re.I)
+        matches = [c
+                   for m, c in items.iteritems()
+                   if matcher.match(m)]
+        if len(matches) == 1:
+            return matches[0]
+        return None
+
+    # prefix match
+    m = fuzzy_match(s + '.*')
+    if m:
+        return m
+    # substring match
+    m = fuzzy_match('.*'.join(s) + '.*')
+    if m:
+        return m
+    return None
+
+
 class UI(object):
     '''
     Base class for all ui levels.
@@ -263,10 +283,14 @@ at the current level.
         if context.previous_level():
             out = ['..']
         out += context.current_level().get_completions()
-        for i, o in enumerate(out):
+        i = 0
+        for o in out:
+            if o.startswith('-') or o.startswith('_'):
+                continue
             print '%-16s' % (o),
             if ((i - 2) % 3) == 0:
                 print ''
+            i += 1
         print ''
 
     @help('''Navigate the level structure
@@ -350,14 +374,18 @@ Examples:
         '''
         Returns child info for the given name, or None
         if the child is not found.
+
+        This tries very hard to find a matching child:
+        If none is found, a fuzzy matcher is used to
+        pick a close match
         '''
-        return self._children.get(child)
+        return fuzzy_get(self._children, child)
 
     def is_sublevel(self, child):
         '''
         True if the given name is a sublevel of this level
         '''
-        sub = self._children.get(child)
+        sub = self.get_child(child)
         return sub and sub.type == 'level'
 
     @classmethod
diff --git a/modules/completers.py b/modules/completers.py
index bcd5ab9..91d7891 100644
--- a/modules/completers.py
+++ b/modules/completers.py
@@ -1,23 +1,9 @@
 # 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
-#
+# See COPYING for license information.
 
 # Helper completers
 
-import xmlutil
+from . import xmlutil
 
 
 def choice(lst):
diff --git a/modules/config.py b/modules/config.py
index 4722ccd..25eaea7 100644
--- a/modules/config.py
+++ b/modules/config.py
@@ -1,32 +1,43 @@
 # 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
-#
+# See COPYING for license information.
 '''
 Holds user-configurable options.
 '''
 
 import os
 import re
-import ConfigParser
-import userdir
+try:
+    import ConfigParser
+except ImportError:
+    import configparser as ConfigParser
+from . import userdir
 
 
 _SYSTEMWIDE = '/etc/crm/crm.conf'
 _PERUSER = os.getenv("CRM_CONFIG_FILE") or os.path.join(userdir.CONFIG_HOME, 'crm.conf')
 
+_PATHLIST = {
+    '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'),
+    'wwwdir': ('/srv/www', '/var/www')
+}
+
+
+def make_path(path):
+    """input: path containing %(?)s-statements
+    output: path with no such statements"""
+    m = re.match(r'\%\(([^\)]+)\)(.+)', path)
+    if m:
+        t = m.group(1)
+        for dd in _PATHLIST[t]:
+            if os.path.isdir(path % {t: dd}):
+                return path % {t: dd}
+        return path % {t: _PATHLIST[t][0]}
+    return path
+
 
 # opt_ classes
 # members: default, completions, validate()
@@ -38,21 +49,35 @@ class opt_program(object):
             self.default = os.getenv(envvar)
         else:
             for prog in proglist:
-                if self._is_program(prog):
-                    self.default = prog
+                p = self._find_program(prog)
+                if p is not None:
+                    self.default = p
                     break
         self.completions = proglist
 
-    def _is_program(self, prog):
+    def _find_program(self, prog):
         """Is this program available?"""
-        for p in os.getenv("PATH").split(os.pathsep):
-            filename = os.path.join(p, prog)
+        paths = os.getenv("PATH").split(os.pathsep)
+        paths.extend(['/usr/bin', '/usr/sbin', '/bin', '/sbin'])
+        if prog.startswith('/'):
+            filename = make_path(prog)
             if os.path.isfile(filename) and os.access(filename, os.X_OK):
-                return True
-        return False
+                return filename
+        elif prog.startswith('%'):
+            prog = make_path(prog)
+            for p in paths:
+                filename = os.path.join(p, prog)
+                if os.path.isfile(filename) and os.access(filename, os.X_OK):
+                    return filename
+        else:
+            for p in paths:
+                filename = make_path(os.path.join(p, prog))
+                if os.path.isfile(filename) and os.access(filename, os.X_OK):
+                    return filename
+        return None
 
     def validate(self, prog):
-        if not self._is_program(prog):
+        if self._find_program(prog) is None:
             raise ValueError("%s does not exist or is not a program" % prog)
 
     def get(self, value):
@@ -96,7 +121,7 @@ class opt_multichoice(object):
     def validate(self, val):
         vals = [x.strip() for x in val.split(',')]
         for otype in vals:
-            if not otype in self.completions:
+            if otype not in self.completions:
                 raise ValueError("%s not in %s" % (val, ', '.join(self.completions)))
 
     def get(self, value):
@@ -123,28 +148,9 @@ class opt_boolean(object):
 
 
 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.default = make_path(path)
         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):
@@ -171,6 +177,18 @@ class opt_color(object):
         return [s.rstrip(',') for s in value.split(' ')] or ['normal']
 
 
+class opt_list(object):
+    def __init__(self, deflist):
+        self.default = ' '.join(deflist)
+        self.completions = deflist
+
+    def validate(self, val):
+        pass
+
+    def get(self, value):
+        return [s.rstrip(',') for s in value.split(' ')]
+
+
 DEFAULTS = {
     'core': {
         'editor': opt_program('EDITOR', ('vim', 'vi', 'emacs', 'nano')),
@@ -189,6 +207,7 @@ DEFAULTS = {
         'dotty': opt_program('', ('dotty',)),
         'dot': opt_program('', ('dot',)),
         'ignore_missing_metadata': opt_boolean('no'),
+        'report_tool_options': opt_string('')
     },
     'path': {
         'sharedir': opt_dir('%(datadir)s/crmsh'),
@@ -201,10 +220,11 @@ DEFAULTS = {
         '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')
+        'nagios_plugins': opt_dir('%(libdir)s/nagios/plugins'),
+        'hawk_wizards': opt_dir('%(wwwdir)s/hawk/config/wizard'),
     },
     'color': {
-        'style': opt_multichoice('color', ('plain', 'color', 'uppercase')),
+        'style': opt_multichoice('color', ('plain', 'color-always', 'color', 'uppercase')),
         'error': opt_color('red bold'),
         'ok': opt_color('green bold'),
         'warn': opt_color('yellow bold'),
@@ -271,11 +291,14 @@ class _Configuration(object):
             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 ''
+        try:
+            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 ''
+        except ConfigParser.NoOptionError as e:
+            raise ValueError(e)
 
     def get(self, section, name, raw=False):
         if raw:
@@ -396,16 +419,15 @@ color = _Section('color')
 
 
 def load_version():
-    version, build = 'dev', 'unknown'
+    version = 'dev'
     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
+    return version
 
-VERSION, BUILD_VERSION = load_version()
-CRM_VERSION = "%s (Build %s)" % (VERSION, BUILD_VERSION)
+VERSION = load_version()
+CRM_VERSION = str(VERSION)
diff --git a/modules/constants.py b/modules/constants.py
index 68fba46..0902492 100644
--- a/modules/constants.py
+++ b/modules/constants.py
@@ -1,23 +1,80 @@
 # 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
-#
+# See COPYING for license information.
 
-from ordereddict import odict
+from .ordereddict import odict
 
 
+# A list of all keywords introduced in the
+# CIB language.
+keywords = {
+    "node": "element",
+    "primitive": "element",
+    "resource": "element",
+    "group": "element",
+    "clone": "element",
+    "ms": "element",
+    "master": "element",
+    "location": "element",
+    "colocation": "element",
+    "collocation": "element",
+    "order": "element",
+    "rsc_ticket": "element",
+    "rsc_template": "element",
+    "property": "element",
+    "rsc_defaults": "element",
+    "op_defaults": "element",
+    "acl_target": "element",
+    "acl_group": "element",
+    "user": "element",
+    "role": "element",
+    "fencing_topology": "element",
+    "fencing-topology": "element",
+    "tag": "element",
+    "monitor": "element",
+    "params": "subelement",
+    "meta": "subelement",
+    "attributes": "subelement",
+    "utilization": "subelement",
+    "operations": "subelement",
+    "op": "subelement",
+    "rule": "subelement",
+    "inf": "value",
+    "INFINITY": "value",
+    "and": "op",
+    "or": "op",
+    "lt": "op",
+    "gt": "op",
+    "lte": "op",
+    "gte": "op",
+    "eq": "op",
+    "ne": "op",
+    "defined": "op",
+    "not_defined": "op",
+    "in_range": "op",
+    "in": "op",
+    "date_spec": "op",
+    "spec": "op",
+    "date": "value",
+    "yes": "value",
+    "no": "value",
+    "true": "value",
+    "false": "value",
+    "on": "value",
+    "off": "value",
+    "normal": "value",
+    "member": "value",
+    "ping": "value",
+    "remote": "value",
+    "start": "value",
+    "stop": "value",
+    "Mandatory": "value",
+    "Optional": "value",
+    "Serialize": "value",
+    "ref": "value",
+    "xpath": "value",
+    "xml": "element",
+}
+
 cib_cli_map = {
     "node": "node",
     "primitive": "primitive",
@@ -149,6 +206,7 @@ rsc_meta_attributes = (
     "migration-threshold", "priority", "multiple-active",
     "failure-timeout", "resource-stickiness", "target-role",
     "restart-type", "description", "remote-node", "requires",
+    "provides", "remote-port", "remote-addr", "remote-connect-timeout"
 )
 group_meta_attributes = ("container", )
 clone_meta_attributes = (
@@ -201,16 +259,19 @@ graph = {
     "node": {
         "style": "bold",
         "shape": "box",
-        "color": "blue",
+        "color": "#7ac142",
     },
     "primitive": {
-        "fillcolor": "lightgrey",
-        "style": "filled",
+        "fillcolor": "#e4e5e6",
+        "color": "#b9b9b9",
+        "shape": "box",
+        "style": "rounded,filled",
     },
     "rsc_template": {
-        "fillcolor": "lightgrey",
-        "color": "mediumpurple",
-        "style": "filled",
+        "fillcolor": "#ffd457",
+        "color": "#b9b9b9",
+        "shape": "box",
+        "style": "rounded,filled,dashed",
     },
     "class:stonith": {
         "shape": "box",
@@ -221,14 +282,14 @@ graph = {
         "dir": "none",
     },
     "clone": {
-        "color": "red",
+        "color": "#ec008c",
     },
     "ms": {
-        "color": "maroon",
+        "color": "#f8981d",
     },
     "group": {
-        "color": "blue",
-        "group": "blue",
+        "color": "#00aeef",
+        "group": "#00aeef",
         "labelloc": "b",
         "labeljust": "r",
         "labelfontsize": "12",
@@ -237,7 +298,7 @@ graph = {
         "style": "dotted",
     },
     "template:edge": {
-        "color": "grey64",
+        "color": "#b9b9b9",
         "style": "dotted",
         "arrowtail": "open",
         "dir": "back",
@@ -254,12 +315,6 @@ simulate_programs = {
     "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",
@@ -273,17 +328,4 @@ extra_cluster_properties = ("dc-version",
                             "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
index c2a8045..cba061f 100644
--- a/modules/corosync.py
+++ b/modules/corosync.py
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 '''
 Functions that abstract creating and editing the corosync.conf
 configuration file, and also the corosync-* utilities.
@@ -21,10 +7,10 @@ configuration file, and also the corosync-* utilities.
 
 import os
 import re
-import utils
-import tmpfiles
 import socket
-from msg import err_buf, common_debug
+from . import utils
+from . import tmpfiles
+from .msg import err_buf, common_debug
 
 
 def conf():
@@ -35,6 +21,11 @@ def is_corosync_stack():
     return utils.cluster_stack() == 'corosync'
 
 
+def check_tools():
+    return all(utils.is_program(p)
+               for p in ['corosync-cfgtool', 'corosync-quorumtool', 'corosync-cmapctl'])
+
+
 def cfgtool(*args):
     return utils.get_stdout(['corosync-cfgtool'] + list(args), shell=False)
 
@@ -288,30 +279,7 @@ 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
+    return utils.cluster_copy_file(conf(), nodes)
 
 
 def pull_configuration(from_node):
@@ -344,74 +312,19 @@ def pull_configuration(from_node):
         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)
+    if checksum:
+        utils.remote_checksum(local_path, nodes, this_node)
     elif len(nodes) == 1:
-        _diff_this(pssh, local_path, nodes, this_node)
+        utils.remote_diff_this(local_path, nodes, this_node)
     elif this_node in nodes:
         nodes.remove(this_node)
-        _diff_this(pssh, local_path, nodes, this_node)
+        utils.remote_diff_this(local_path, nodes, this_node)
     elif len(nodes):
-        _diff(pssh, local_path, nodes)
+        utils.remote_diff(local_path, nodes)
 
 
 def next_nodeid(parser):
diff --git a/modules/crm_gv.py b/modules/crm_gv.py
index f27d4a9..af00847 100644
--- a/modules/crm_gv.py
+++ b/modules/crm_gv.py
@@ -1,25 +1,12 @@
 # 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
+# See COPYING for license information.
+
+import re
+from . import config
+from . import tmpfiles
+from . import utils
+from .msg import common_err
+from .ordereddict import odict
 
 # graphviz stuff
 
@@ -29,6 +16,12 @@ def _attr_str(attr_d):
                      for k, v in attr_d.iteritems()])
 
 
+def _quoted(name):
+    if re.match('^[0-9_]', name):
+        return '"%s"' % (name)
+    return name
+
+
 class Gv(object):
     '''
     graph.
@@ -75,7 +68,7 @@ class Gv(object):
             self.norank_nodes.append(id)
 
     def my_edge(self, e):
-        return [self.gv_id(x) for x in e]
+        return [self.gv_id(x) for x in e if x is not None]
 
     def new_edge(self, e):
         ne = self.my_edge(e)
@@ -95,7 +88,7 @@ class Gv(object):
         self.edge_attrs[e_id][attr_n] = attr_v
 
     def edge_str(self, e_id):
-        e_s = self.EDGEOP.join(self.edges[e_id])
+        e_s = self.EDGEOP.join(_quoted(x) for x in self.edges[e_id])
         if e_id < len(self.edge_attrs):
             return('%s [%s]' % (e_s, _attr_str(self.edge_attrs[e_id])))
         else:
@@ -105,7 +98,7 @@ class Gv(object):
         attrs = 'style="invis"'
         if node in self.norank_nodes:
             attrs = '%s,constraint="false"' % attrs
-        return '%s [%s];' % (self.EDGEOP.join([tn, node]), attrs)
+        return '%s [%s];' % (self.EDGEOP.join([_quoted(tn), _quoted(node)]), attrs)
 
     def invisible_edges(self):
         '''
@@ -145,7 +138,7 @@ class Gv(object):
             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.append('\t%s [%s];' % (_quoted(n), attr_s))
         l += self.invisible_edges()
         l.append(self.footer())
         return l
diff --git a/modules/crm_pssh.py b/modules/crm_pssh.py
old mode 100755
new mode 100644
index bbfdc81..64c65e9
--- a/modules/crm_pssh.py
+++ b/modules/crm_pssh.py
@@ -12,70 +12,31 @@ 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 parallax.manager import Manager, FatalError
+from parallax.task import Task
+from parallax import Options
 
-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
+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):
+def parse_args(outdir, errdir, 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
+    opts = Options()
+    opts.timeout = int(t)
+    opts.quiet = True
+    opts.inline = False
+    opts.outdir = outdir
+    opts.errdir = errdir
+    return opts
 
 
 def get_output(dir, host):
@@ -115,10 +76,6 @@ def do_pssh(l, opts):
         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 = ""
@@ -128,24 +85,31 @@ def do_pssh(l, opts):
                '-o', 'PasswordAuthentication=no',
                '-o', 'SendEnv=PSSH_NODENUM',
                '-o', 'StrictHostKeyChecking=no']
-        if opts.options:
+        if hasattr(opts, 'options'):
             for opt in opts.options:
                 cmd += ['-o', opt]
         if user:
             cmd += ['-l', user]
         if port:
             cmd += ['-p', port]
-        if opts.extra:
+        if hasattr(opts, 'extra'):
             cmd.extend(opts.extra)
         if cmdline:
             cmd.append(cmdline)
         hosts.append(host)
-        t = Task(host, port, user, cmd, opts, stdin)
+        t = Task(host, port, user, cmd,
+                 stdin=opts.input_stream,
+                 verbose=opts.verbose,
+                 quiet=opts.quiet,
+                 print_out=opts.print_out,
+                 inline=opts.inline,
+                 inline_stdout=opts.inline_stdout,
+                 default_user=opts.default_user)
         manager.add_task(t)
     try:
         return manager.run()  # returns a list of exit codes
     except FatalError:
-        common_err("pssh to nodes failed")
+        common_err("SSH to nodes failed")
         show_output(opts.errdir, hosts, "stderr")
         return False
 
@@ -163,7 +127,7 @@ def examine_outcome(l, opts, statuses):
         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):
+    # elif any(x==255 for x in statuses):
     for status in statuses:
         if status == 255:
             common_warn("ssh processes failed")
@@ -187,7 +151,7 @@ def next_loglines(a, outdir, errdir):
                      (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))
+        opts = parse_args(outdir, errdir)
         l.append([node, cmdline])
     statuses = do_pssh(l, opts)
     if statuses:
@@ -209,8 +173,8 @@ def next_peinputs(node_pe_l, outdir, errdir):
         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))
+        cmdline = "tar -C %s -chf - %s" % (dir, ' '.join(red_pe_l))
+        opts = parse_args(outdir, errdir)
         l.append([node, cmdline])
     if not l:
         # is this a failure?
@@ -231,7 +195,7 @@ def do_pssh_cmd(cmd, node_l, outdir, errdir, timeout=20000):
         l.append([node, cmd])
     if not l:
         return True
-    opts, args = parse_args(make_pssh_opts(outdir, errdir), t=str(int(timeout/1000)))
+    opts = parse_args(outdir, errdir, t=int(timeout/1000))
     return do_pssh(l, opts)
 
 # vim:ts=4:sw=4:et:
diff --git a/modules/handles.py b/modules/handles.py
new file mode 100644
index 0000000..c1edde7
--- /dev/null
+++ b/modules/handles.py
@@ -0,0 +1,128 @@
+# Copyright (C) 2015 Kristoffer Gronlund <kgronlund at suse.com>
+# See COPYING for license information.
+
+import re
+
+
+_head_re = re.compile(r'\{\{(\#|\^)?([A-Za-z0-9\#\$:_-]+)\}\}')
+
+
+class value(object):
+    """
+    An object that is indexable in mustasches,
+    but also evaluates to a value itself.
+    """
+    def __init__(self, obj, value):
+        self.value = value
+        self.obj = obj
+        self.get = obj.get
+
+    def __call__(self):
+        return self.value
+
+    def __repr__(self):
+        return "handles.value(%s, %s)" % (repr(self.obj), repr(self.value))
+
+    def __str__(self):
+        return "handles.value(%s, %s)" % (repr(self.obj), repr(self.value))
+
+
+def _join(d1, d2):
+    d = d1.copy()
+    d.update(d2)
+    return d
+
+
+def _resolve(path, context, strict):
+    for values in context:
+        r = path
+        p = values
+        while r and p is not None:
+            p, r = p.get(r[0]), r[1:]
+        if strict and r:
+            continue
+        if callable(p):
+            p = p()
+        if p is not None:
+            return p
+    if strict:
+        raise ValueError("Not set: %s" % (':'.join(path)))
+    return None
+
+
+def _push(path, value, context):
+    root = {}
+    leaf = root
+    for x in path[:-1]:
+        leaf = {}
+        root[x] = leaf
+    leaf[path[-1]] = value
+    ret = [root] + context
+    return ret
+
+
+def _textify(obj):
+    if obj is None:
+        return ''
+    elif obj is True:
+        return 'true'
+    elif obj is False:
+        return 'false'
+    else:
+        return str(obj)
+
+
+def _parse(template, context, strict):
+    ret = ""
+    while template:
+        head = _head_re.search(template)
+        if head is None:
+            ret += template
+            break
+        istart, iend, prefix, key = head.start(0), head.end(0), head.group(1), head.group(2)
+        if istart > 0:
+            ret += template[:istart]
+        path, block, invert = key.split(':'), prefix == '#', prefix == '^'
+        if not path:
+            raise ValueError("empty {{}} block found")
+        obj = _resolve(path, context, strict)
+        if block or invert:
+            tailtag = '{{/%s}}' % (key)
+            tailidx = iend + template[head.end(0):].find(tailtag)
+            if tailidx < iend:
+                raise ValueError("Unclosed conditional: %s" % head.group(0))
+            iend = tailidx + len(tailtag)
+            body = template[head.end(0):tailidx]
+            if body.startswith('\n') and (not ret or ret.endswith('\n')):
+                ret = ret[:-1]
+            if block:
+                if obj in (None, False):
+                    pass
+                elif isinstance(obj, tuple) or isinstance(obj, list):
+                    for it in obj:
+                        ret += _parse(body, _push(path, it, context), strict)
+                else:
+                    ret += _parse(body, context, strict)
+            elif not obj:
+                ret += _parse(body, _push(path, "", context), strict)
+            if ret.endswith('\n') and template[iend:].startswith('\n'):
+                iend += 1
+        elif obj is not None:
+            ret += _textify(obj)
+        template = template[iend:]
+    return ret
+
+
+def parse(template, values, strict=False):
+    """
+    Takes as input a template string and a dict
+    of values, and replaces the following:
+    {{object:key}} = look up key in object and insert value
+    {{object}} = insert value if not None or False.
+    {{#object}} ... {{/object}} = if object is a dict or value, process text. if object
+    is a list, process text for each item in the list
+    (can't nest these for items with the same name)
+    {{^object}} ... {{/object}} = if object is falsy, process text.
+    If a path evaluates to a callable, the callable will be invoked to get the value.
+    """
+    return _parse(template, [values], strict)
diff --git a/modules/help.py b/modules/help.py
index 49b04ba..6ec8b84 100644
--- a/modules/help.py
+++ b/modules/help.py
@@ -1,24 +1,10 @@
 # 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
-#
+# See COPYING for license information.
 
 '''
 The commands exposed by this module all
-get their data from the doc/crm.8.txt text
+get their data from the doc/crm.8.adoc text
 file. In that file, there are help for
  - topics
  - levels
@@ -41,11 +27,11 @@ Help for the level itself is like this:
 
 import os
 import re
-from utils import page_string
-from msg import common_err
-import config
-import clidisplay
-from ordereddict import odict
+from .utils import page_string
+from .msg import common_err
+from . import config
+from . import clidisplay
+from .ordereddict import odict
 
 
 class HelpFilter(object):
@@ -124,7 +110,7 @@ class HelpEntry(object):
         return str(self)
 
 
-HELP_FILE = os.path.join(config.path.sharedir, 'crm.8.txt')
+HELP_FILE = os.path.join(config.path.sharedir, 'crm.8.adoc')
 
 _DEFAULT = HelpEntry('No help available', long_help='', alias_for=None, generated=True)
 _REFERENCE_RE = re.compile(r'<<[^,]+,(.+)>>')
@@ -144,6 +130,8 @@ _TOPICS["Topics"] = HelpEntry("Available help topics", generated=True)
 def _titleline(title, desc, suffix=''):
     return '%-16s %s\n' % (('`%s`' % (title)) + suffix, desc)
 
+_hidden_commands = ('up', 'cd', 'help', 'quit', 'ls')
+
 
 def help_overview():
     '''
@@ -162,13 +150,11 @@ def help_overview():
             s += '\t' + _titleline(title, command.short)
     s += "\n"
 
-    hidden_commands = ('up', 'cd', 'help', 'quit', 'ls')
-
-    for title, level in _LEVELS.iteritems():
+    for title, level in sorted(_LEVELS.iteritems(), key=lambda x: x[0]):
         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:
+            for cmdname, cmd in sorted(_COMMANDS[title].iteritems(), key=lambda x: x[0]):
+                if cmdname in _hidden_commands or cmdname.startswith('_'):
                     continue
                 if not cmd.is_alias():
                     s += '\t\t' + _titleline(cmdname, cmd.short)
@@ -206,7 +192,8 @@ def help_level(level):
     Returns a help entry for a given level.
     '''
     _load_help()
-    return _LEVELS.get(level, _DEFAULT)
+    from .command import fuzzy_get
+    return fuzzy_get(_LEVELS, level) or _DEFAULT
 
 
 def help_command(level, command):
@@ -214,10 +201,11 @@ def help_command(level, command):
     Returns a help entry for a given command
     '''
     _load_help()
-    lvlhelp = _COMMANDS.get(level)
+    from .command import fuzzy_get
+    lvlhelp = fuzzy_get(_COMMANDS, level)
     if not lvlhelp:
         raise ValueError("Undocumented topic '%s'" % (level))
-    cmdhelp = lvlhelp.get(command)
+    cmdhelp = fuzzy_get(lvlhelp, command)
     if not cmdhelp:
         raise ValueError("Undocumented topic '%s' in '%s'" % (command, level))
     return cmdhelp
@@ -228,11 +216,13 @@ def _is_help_topic(arg):
 
 
 def _is_command(level, command):
-    return level in _COMMANDS and command in _COMMANDS[level]
+    from .command import fuzzy_get
+    return level in _COMMANDS and fuzzy_get(_COMMANDS[level], command)
 
 
 def _is_level(level):
-    return level in _LEVELS
+    from .command import fuzzy_get
+    return fuzzy_get(_LEVELS, level)
 
 
 def help_contextual(context, subject, subtopic):
@@ -244,10 +234,6 @@ def help_contextual(context, subject, subtopic):
         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:
@@ -256,7 +242,11 @@ def help_contextual(context, subject, subtopic):
         return help_command(context, subject)
     if _is_level(subject):
         return help_level(subject)
-    raise ValueError("Undocumented topic '%s'" % (subject))
+    from .command import fuzzy_get
+    t = fuzzy_get(_TOPICS, subject.lower())
+    if t:
+        return t
+    raise ValueError("No help found for '%s'! 'overview' lists all help entries" % (subject))
 
 
 def add_help(entry, topic=None, level=None, command=None):
@@ -285,7 +275,7 @@ def add_help(entry, topic=None, level=None, command=None):
 
 def _load_help():
     '''
-    Lazily load and parse crm.8.txt.
+    Lazily load and parse crm.8.adoc.
     '''
     global _LOADED
     if _LOADED:
@@ -342,8 +332,14 @@ def _load_help():
         for lvlname, level in _LEVELS.iteritems():
             if lvlname in _COMMANDS:
                 level.long += "\n\nCommands:\n"
-                for cmdname, cmd in _COMMANDS[lvlname].iteritems():
+                for cmdname, cmd in sorted(_COMMANDS[lvlname].iteritems(), key=lambda x: x[0]):
+                    if cmdname in _hidden_commands or cmdname.startswith('_'):
+                        continue
                     level.long += "\t" + _titleline(cmdname, cmd.short)
+                level.long += "\n"
+                for cmdname, cmd in sorted(_COMMANDS[lvlname].iteritems(), key=lambda x: x[0]):
+                    if cmdname in _hidden_commands:
+                        level.long += "\t" + _titleline(cmdname, cmd.short)
 
     def fixup_root_commands():
         "root commands appear as levels"
@@ -374,9 +370,14 @@ def _load_help():
                     add_help_for_alias(lvl.name, info.name, alias)
                 if info.level:
                     add_aliases_for_level(info.level)
-        from ui_root import Root
+        from .ui_root import Root
         add_aliases_for_level(Root)
 
+    def fixup_topics():
+        "fix entries for topics and overview"
+        _TOPICS["Overview"] = help_overview()
+        _TOPICS["Topics"] = help_topics()
+
     try:
         name = os.getenv("CRM_HELP_FILE") or HELP_FILE
         helpfile = open(name, 'r')
@@ -397,6 +398,7 @@ def _load_help():
         append_cmdinfos()
         fixup_root_commands()
         fixup_help_aliases()
+        fixup_topics()
     except IOError, msg:
         common_err("Help text not found! %s" % (msg))
 
diff --git a/modules/report.py b/modules/history.py
similarity index 77%
rename from modules/report.py
rename to modules/history.py
index 7bff564..f7cd0a0 100644
--- a/modules/report.py
+++ b/modules/history.py
@@ -1,44 +1,29 @@
 # 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
-#
+# See COPYING for license information.
 
 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
-
+from . import config
+from . import constants
+from . 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, 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, get_stdout, quote
+from .utils import make_datetime_naive, datetime_to_timestamp
+
+_HAS_PARALLAX = False
 try:
-    from crm_pssh import next_loglines, next_peinputs
+    from .crm_pssh import next_loglines, next_peinputs
+    _HAS_PARALLAX = True
 except:
-    _NO_PSSH = True
+    pass
 
 
 YEAR = None
@@ -68,8 +53,11 @@ 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)))
+    year = time.strftime("%Y", time.localtime(ts))
+    if YEAR is not None:
+        t = (" (ts: %s)" % (ts)) if ts is not None else ""
+        common_debug("history: setting year to %s%s" % (year, t))
+    YEAR = year
 
 
 def make_time(t):
@@ -80,31 +68,59 @@ def make_time(t):
     if t is None:
         return None
     elif isinstance(t, datetime.datetime):
-        return convert_dt(t)
+        return datetime_to_timestamp(t)
     return t
 
 
+_syslog2node_formats = (re.compile(r'^[a-zA-Z]{2,4} \d{1,2} \d{2}:\d{2}:\d{2}\s+(?:\[\d+\])?\s*([\S]+)'),
+                        re.compile(r'^\d{4}-\d{2}-\d{2}T\S+\s+(?:\[\d+\])?\s*([\S]+)'))
+
+
 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
+    fmt1, fmt2 = _syslog2node_formats
+    m = fmt1.match(s)
+    if m:
+        if YEAR is None:
+            set_year()
+        tstr = ' '.join([YEAR] + s.split()[0:3])
+        return datetime_to_timestamp(parse_time(tstr))
+
+    m = fmt2.match(s)
+    if m:
+        tstr = s.split()[0]
+        return datetime_to_timestamp(parse_time(tstr))
+
+    common_debug("malformed line: %s" % s)
+    return None
 
 
 def syslog2node(s):
-    '''Get the node from a syslog line.'''
+    '''
+    Get the node from a syslog line.
+
+    old format:
+    Aug 14 11:07:04 <node> ...
+    new format:
+    Aug 14 11:07:04 [<PID>] <node> ...
+    RFC5424:
+    <TS> <node> ...
+    RFC5424 (2):
+    <TS> [<PID>] <node> ...
+    '''
+
+    fmt1, fmt2 = _syslog2node_formats
+    m = fmt1.search(s)
+    if m:
+        return m.group(1)
+
+    m = fmt2.search(s)
+    if m:
+        return m.group(1)
+
     try:
         # strptime defaults year to 1900 (sigh)
         time.strptime(' '.join(s.split()[0:3]),
@@ -233,7 +249,9 @@ def filter_log(sl, log_l):
     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)]
+    ret = [x for x in sl if is_our_log(x, node_l)]
+    common_debug("filter_log: %s in, %s out" % (len(sl), len(ret)))
+    return ret
 
 
 def first_log_lines(log_l):
@@ -260,26 +278,13 @@ def last_log_lines(log_l):
     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):
+    def __init__(self, log_l, from_dt, to_dt):
         self.log_l = log_l
-        self.central_log = central_log
         self.f = {}
         self.startpos = {}
         self.endpos = {}
@@ -301,13 +306,9 @@ class LogSyslog(object):
             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)
+        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):
         '''
@@ -323,25 +324,28 @@ class LogSyslog(object):
             start = log_seek(f, self.from_ts)
             end = log_seek(f, self.to_ts, to_end=True)
             if start == -1 or end == -1:
+                common_debug("%s is a bad log" % (log))
                 bad_logs.append(log)
             else:
+                common_debug("%s start=%s, end=%s" % (log, start, end))
                 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):
+    def get_match_line(self, f, relist):
         '''
-        Get first line from f that matches re_s, but is not
-        behind endpos[f].
+        Get first line from f that matches one of
+        the REs in relist, but is not behind endpos[f].
+        if relist is empty, return all lines
         '''
         while f.tell() < self.endpos[f]:
             fpos = f.tell()
             s = f.readline().rstrip()
             if not s:
                 continue
-            if not patt or patt.search(s):
+            if not relist or any(r.search(s) for r in relist):
                 return s, fpos
         return '', -1
 
@@ -354,27 +358,15 @@ class LogSyslog(object):
             l.append(s)
         return l
 
-    def search_logs(self, log_l, re_s=''):
+    def search_logs(self, log_l, relist):
         '''
-        Search logs for re_s and sort by time.
+        Search logs for any of the regexps in relist.
         '''
-        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]
+        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 = [self.get_match_line(x, relist)[0] for x in fl]
         top_line_ts = []
         rm_idx_l = []
         # calculate time stamps for head lines
@@ -387,12 +379,12 @@ class LogSyslog(object):
         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]))
+        common_debug("search in %s" % ", ".join(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)
+            return [top_line[0]] + self.single_log_list(fl[0], relist)
         # search through multiple logs, merge sorted by time
         l = []
         first = 0
@@ -408,7 +400,7 @@ class LogSyslog(object):
             if not top_line[first]:
                 break
             l.append(top_line[first])
-            top_line[first] = self.get_match_line(fl[first], patt)[0]
+            top_line[first] = self.get_match_line(fl[first], relist)[0]
             if not top_line[first]:
                 top_line_ts[first] = time.time()
             else:
@@ -419,27 +411,20 @@ class LogSyslog(object):
         '''
         Return a list of log messages which
         match one of the regexes in re_l.
+        if re_l is an empty list, return all lines.
         '''
-        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)
+        log_l = log_l or self.log_l
+        return filter_log(self.search_logs(log_l, re_l), log_l)
 
 
 def human_date(dt):
     'Some human date representation. Date defaults to now.'
     if not dt:
-        dt = datetime.datetime.now()
+        dt = make_datetime_naive(datetime.datetime.now())
+    # here, dt is in UTC. Convert to localtime:
+    localdt = datetime.datetime.fromtimestamp(datetime_to_timestamp(dt))
     # drop microseconds
-    return re.sub("[.].*", "", "%s %s" % (dt.date(), dt.time()))
+    return re.sub("[.].*", "", "%s %s" % (localdt.date(), localdt.time()))
 
 
 def is_log(p):
@@ -460,7 +445,7 @@ def read_log_info(log):
         logf, pos = s.split()
         return logf, int(pos)
     except:
-        warn_once("hb_report too old, you need to update cluster-glue")
+        warn_once("crm report too old, you need to update cluster-glue")
         return '', -1
 
 
@@ -499,32 +484,79 @@ def run_graph_msg_actions(msg):
         s = s[r.end():]
 
 
-def extract_pe_file(msg):
+def get_pe_file_num_from_msg(msg):
+    """
+    Get PE file name and number from log message
+    Returns: (file, num)
+    """
     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]
+        return ("", "-1")
+    return (msg_a[-1], get_pe_num(msg_a[-1]))
+
+
+def transition_start_re(number_re):
+    """
+    Return regular expression matching transition start.
+    number_re can be a specific transition or a regexp matching
+    any transition number.
+    The resulting RE has groups
+    1: transition number
+    2: full path of pe file
+    3: pe file number
+    """
+    m1 = "crmd.*Processing graph ([0-9]+).*derived from (.*/pe-[^-]+-(%s)[.]bz2)" % (number_re)
+    m2 = "pengine.*Transition ([0-9]+):.*([^ ]*/pe-[^-]+-(%s)[.]bz2)" % (number_re)
+    try:
+        return re.compile("(?:%s)|(?:%s)" % (m1, m2))
+    except re.error, e:
+        common_debug("RE compilation failed: %s" % (e))
+        raise ValueError("Error in search expression")
+
+
+def transition_end_re(number_re):
+    """
+    Return RE matching transition end.
+    See transition_start_re for more details.
+    """
+    try:
+        return re.compile("crmd.*Transition ([0-9]+).*Source=(.*/pe-[^-]+-(%s)[.]bz2).:.*(Stopped|Complete|Terminated)" % (number_re))
+    except re.error, e:
+        common_debug("RE compilation failed: %s" % (e))
+        raise ValueError("Error in search expression")
+
+
+def find_transition_end(trnum, messages):
+    """
+    Find the end of the given transition in the list of messages
+    """
+    matcher = transition_end_re(trnum)
+    for msg in messages:
+        if matcher.search(msg):
+            return msg
+    matcher = transition_start_re(str(int(trnum) + 1))
+    for msg in messages:
+        if matcher.search(msg):
+            return msg
+    return None
 
 
-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)
+def find_transition_end_msg(transition_start_msg, trans_msg_l):
+    """
+    Given the start of a transition log message, find
+    and return the end of the transition log messages.
+    """
+    pe_file, pe_num = get_pe_file_num_from_msg(transition_start_msg)
     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
+    return find_transition_end(pe_num, trans_msg_l) or ""
 
 
 def trans_str(node, pe_file):
-    '''Convert node,pe_file to transition sting.'''
+    '''Convert node,pe_file to transition string.'''
     return "%s:%s" % (node, os.path.basename(pe_file).replace(".bz2", ""))
 
 
@@ -539,29 +571,28 @@ 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 __init__(self, start_msg, end_msg):
+        self.start_msg = start_msg
+        self.end_msg = end_msg
+        self.tags = set()
+        self.pe_file, self.pe_num = get_pe_file_num_from_msg(start_msg)
+        self.dc = syslog2node(start_msg)
+        self.start_ts = syslog_ts(start_msg)
+        if end_msg:
+            self.end_ts = syslog_ts(end_msg)
+        else:
+            common_warn("end of transition %s not found in logs (transition not complete yet?)" % self)
+            self.end_ts = datetime_to_timestamp(datetime.datetime(2525, 1, 1))
 
     def __str__(self):
-        return trans_str(self.dc, self.pe_file)
+        return self.get_node_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 get_node_file(self):
+        return trans_str(self.dc, self.pe_file)
 
     def actions_count(self):
-        if self.run_msg:
-            act_d = run_graph_msg_actions(self.run_msg)
+        if self.end_msg:
+            act_d = run_graph_msg_actions(self.end_msg)
             return sum(act_d.values())
         else:
             return -1
@@ -571,9 +602,9 @@ class Transition(object):
 
     def transition_info(self):
         print "Transition %s (%s -" % (self, shorttime(self.start_ts)),
-        if self.run_msg:
+        if self.end_msg:
             print "%s):" % shorttime(self.end_ts)
-            act_d = run_graph_msg_actions(self.run_msg)
+            act_d = run_graph_msg_actions(self.end_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)
@@ -627,7 +658,6 @@ class Report(object):
         self.from_dt = None
         self.to_dt = None
         self.log_l = []
-        self.central_log = None
         self.setnodes = []  # optional
         # derived
         self.loc = None
@@ -635,7 +665,7 @@ class Report(object):
         self.nodecolor = {}
         self.logobj = None
         self.desc = None
-        self.peinputs_l = []
+        self._transitions = []
         self.cibgrp_d = {}
         self.cibcln_d = {}
         self.cibrsc_l = []
@@ -646,7 +676,7 @@ class Report(object):
         self.detail = 0
         self.log_filter_out = []
         self.log_filter_out_re = []
-        # change_origin may be CH_SRC, CH_TIME, CH_UPD
+        # change_origin may be 0, CH_SRC, CH_TIME, CH_UPD
         # depending on the change_origin, we update our attributes
         self.change_origin = CH_SRC
         set_year()
@@ -664,7 +694,7 @@ class Report(object):
         return self.node_l
 
     def peinputs_list(self):
-        return [x.pe_num for x in self.peinputs_l]
+        return [x.pe_num for x in self._transitions]
 
     def session_subcmd_list(self):
         return ["save", "load", "pack", "delete", "list", "update"]
@@ -688,6 +718,9 @@ class Report(object):
         elif bfname.endswith(".tar.gz"):  # hmm, must be ancient
             loc = tarball.replace(".tar.gz", "")
             tar_unpack_option = "z"
+        elif bfname.endswith(".tar.xz"):
+            loc = tarball.replace(".tar.xz", "")
+            tar_unpack_option = "J"
         else:
             self.error("this doesn't look like a report tarball")
             return None
@@ -703,11 +736,9 @@ class Report(object):
             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:
+            rc, tf_loc = get_stdout("tar -t%s < %s 2> /dev/null | head -1" % (tar_unpack_option, quote(bfname)))
+            if os.path.abspath(tf_loc) != os.path.abspath(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)
@@ -766,7 +797,7 @@ class Report(object):
 
     def is_live_recent(self):
         '''
-        Look at the last live hb_report. If it's recent enough,
+        Look at the last live report. If it's recent enough,
         return True.
         '''
         try:
@@ -777,7 +808,7 @@ class Report(object):
 
     def is_live_very_recent(self):
         '''
-        Look at the last live hb_report. If it's recent enough,
+        Look at the last live report. If it's recent enough,
         return True.
         '''
         return (time.time() - self.last_live_update) <= self.short_live_recent
@@ -790,26 +821,11 @@ class Report(object):
 
     def find_node_log(self, node):
         p = os.path.join(self.loc, node)
-        for lf in ("ha-log.txt", "messages", "journal.log"):
+        for lf in ("ha-log.txt", "messages", "journal.log", "pacemaker.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 = []
@@ -849,7 +865,7 @@ class Report(object):
     def read_new_log(self, node):
         '''
         Get a list of log lines.
-        The log is put in self.outdir/node by pssh.
+        The log is put in self.outdir/node by parallax.
         '''
         if not os.path.isdir(self.outdir):
             return []
@@ -892,7 +908,7 @@ class Report(object):
                 continue
             pe_l = []
             for new_t_obj in self.list_transitions(log_l, future_pe=True):
-                self.new_peinput(new_t_obj)
+                self._new_transition(new_t_obj)
                 pe_l.append(new_t_obj.pe_file)
             if pe_l:
                 node_pe_l.append([node, pe_l])
@@ -921,8 +937,12 @@ class Report(object):
         Update or create live report.
         '''
         d = self._live_loc()
+
+        created_now = False
+
+        # Create live report if it doesn't exist
         if not d or not os.path.isdir(d):
-            return self.get_live_report()
+            created_now, d = True, self.get_live_report()
         if not self.loc:
             # the live report is there, but we were just invoked
             self.loc = d
@@ -931,7 +951,7 @@ class Report(object):
             # 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 _HAS_PARALLAX:
                 if not acquire_lock(self.report_cache_dir):
                     return None
                 rc = self.update_live_report()
@@ -940,13 +960,22 @@ class Report(object):
                     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()
+                warn_once("parallax library not installed, slow live updates ahead")
+        if not created_now:
+            return self.get_live_report()
+        return self.loc
 
     def new_live_report(self):
         '''
-        Run hb_report to get logs now.
+        Run the report command to get logs now.
         '''
+        from . import ui_report
+
+        extcmd = ui_report.report_tool()
+        if extcmd is None:
+            self.error("No reporting tool found")
+            return None
+
         d = self._live_loc()
         rmdir_r(d)
         tarball = "%s.tar.bz2" % d
@@ -958,18 +987,19 @@ class Report(object):
             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,
+        common_info("Retrieving information from cluster nodes, please wait...")
+        rc = pipe_cmd_nosudo("%s -Z -Q -f '%s' %s %s %s %s" %
+                             (extcmd,
                               self.from_dt.ctime(),
                               to_option,
                               nodes_option,
+                              str(config.core.report_tool_options),
                               d))
         if rc != 0:
             if os.path.isfile(tarball):
-                self.warn("hb_report thinks it failed, proceeding anyway")
+                self.warn("report thinks it failed, proceeding anyway")
             else:
-                self.error("hb_report failed")
+                self.error("report failed")
                 return None
         self.last_live_update = time.time()
         return self.unpack_report(tarball)
@@ -998,7 +1028,7 @@ class Report(object):
             refresh = from_dt and top_dt > from_dt
         if refresh:
             self.set_change_origin(CH_UPD)
-            self.refresh_source(force=True)
+            return self.refresh_source(force=True)
         else:
             self.set_change_origin(CH_TIME)
             self.report_setup()
@@ -1025,8 +1055,7 @@ class Report(object):
     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.
+        resource list, groups). If "live" then use cibadmin.
         '''
         cib_elem = None
         cib_f = self.get_cib_loc()
@@ -1054,13 +1083,13 @@ class Report(object):
                 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))
+    def _new_transition(self, transition):
+        t_obj = self.find_transition(transition.get_node_file())
         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)
+            common_debug("duplicate %s, replacing older PE file" % transition)
+            self._transitions.remove(t_obj)
+        common_debug("appending new PE %s" % transition)
+        self._transitions.append(transition)
 
     def set_node_colors(self):
         i = 0
@@ -1069,25 +1098,24 @@ class Report(object):
             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)]
+        te_invoke_patt = transition_start_re("[0-9]+")
+        return (x for x in msg_l if te_invoke_patt.search(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:
+        trans_re_l = (transition_start_re("[0-9]+"), transition_end_re("[0-9]+"))
+        if msg_l is None:
             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)]
+            return (x for x in msg_l if trans_re_l[0].search(x) or trans_re_l[1].search(x))
 
     def is_empty_transition(self, t0, t1):
-        num_actions = t1.actions_count()
         if not (t0 and t1):
-            return num_actions == 0
+            return False
         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
+        if not (os.path.isfile(old_pe_l_file) or os.path.isfile(new_pe_l_file)):
+            return True
+        num_actions = t1.actions_count()
         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:
@@ -1115,86 +1143,84 @@ class Report(object):
         '''
         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)
+            transition_end_msg = find_transition_end_msg(msg, trans_msg_l)
+            t_obj = Transition(msg, transition_end_msg)
             if self.is_empty_transition(prev_transition, t_obj):
                 common_debug("skipping empty transition (%s)" % t_obj)
                 continue
+            self._set_transition_tags(t_obj)
             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:
+    def _report_setup_source(self):
+        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
-        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.node_l = self.get_nodes()
+        self.set_node_colors()
+        self.log_l = self.find_logs()
+        self.read_cib()
+
+    def _report_setup_update(self):
+        l = self.get_nodes()
+        if self.node_l != l:
+            self.node_l = l
             self.set_node_colors()
             self.log_l = self.find_logs()
-            self.find_central_log()
             self.read_cib()
+
+    def report_setup(self):
+        if self.change_origin == 0:
+            return False
+        if not self.loc:
+            return False
+
+        if self.change_origin == CH_SRC:
+            self._report_setup_source()
         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._report_setup_update()
+
+        self.logobj = LogSyslog(self.log_l,
                                 self.from_dt,
                                 self.to_dt)
+
         if self.change_origin != CH_UPD:
             common_debug("getting transitions from logs")
-            self.peinputs_l = []
+            self._transitions = []
             for new_t_obj in self.list_transitions():
-                self.new_peinput(new_t_obj)
+                self._new_transition(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
+        Unpack a report tarball.
+        For "live", create an ad-hoc report and unpack it
         somewhere in the cache area.
         Parse the period.
         '''
@@ -1232,12 +1258,12 @@ class Report(object):
         including current detail level
         '''
         cib_f = None
-        if self.source != "live" or self.central_log:
+        if self.source != "live":
             cib_f = self.get_cib_loc()
         if is_pcmk_118(cib_f=cib_f):
-            from log_patterns_118 import log_patterns
+            from .log_patterns_118 import log_patterns
         else:
-            from log_patterns import log_patterns
+            from .log_patterns import log_patterns
         if type not in log_patterns:
             common_error("%s not featured in log patterns" % type)
             return None
@@ -1290,12 +1316,14 @@ class Report(object):
         '''
         Print log lines, either matched by re_l or all.
         '''
+        def process(r):
+            return re.compile(r) if isinstance(r, basestring) else r
         if not log_l:
             log_l = self.log_l
-        if not self.central_log and not log_l:
+        if not log_l:
             self.error("no logs found")
             return
-        self.display_logs(self.logobj.get_matches(re_l, log_l))
+        self.display_logs(self.logobj.get_matches([process(r) for r in re_l], log_l))
 
     def get_source(self):
         return self.source
@@ -1318,11 +1346,15 @@ class Report(object):
         output'''
         max_output = 20
         s = ""
-        if len(self.peinputs_l) > max_output:
+        if len(self._transitions) > 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 fmt(t):
+            if 'error' in t.tags:
+                return self._str_nodecolor(t.dc, t.pe_num) + "*"
+            return self._str_nodecolor(t.dc, t.pe_num)
+
+        return "%s%s" % (s, ' '.join([fmt(x) for x in self._transitions[-max_output:]]))
 
     def get_rpt_dt(self, dt, whence):
         '''
@@ -1338,7 +1370,8 @@ class Report(object):
             elif whence == "bottom":
                 myts = max([syslog_ts(x) for x in last_log_lines(self.log_l)])
             if myts:
-                return datetime.datetime.fromtimestamp(myts)
+                import dateutil.tz
+                return make_datetime_naive(datetime.datetime.fromtimestamp(myts).replace(tzinfo=dateutil.tz.tzlocal()))
             common_debug("No log lines with timestamps found in report")
         except Exception, e:
             common_debug("Error: %s" % (e))
@@ -1374,6 +1407,8 @@ class Report(object):
         '''
         Show all events.
         '''
+        if not self.prepare_source():
+            return False
         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) + \
@@ -1382,11 +1417,11 @@ class Report(object):
         if not all_re_l:
             self.error("no resources or nodes found")
             return False
-        self.show_logs(re_l=all_re_l)
+        return 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:
+    def find_transition(self, t_str):
+        for t_obj in self._transitions:
+            if t_obj.get_node_file() == t_str:
                 return t_obj
         return None
 
@@ -1396,7 +1431,7 @@ class Report(object):
         '''
         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))
+        t_obj = self.find_transition(rpt_pe2t_str(rpt_pe_file))
         if not t_obj:
             common_err("%s: transition not found" % rpt_pe_file)
             return False
@@ -1410,6 +1445,36 @@ class Report(object):
         self.logobj.set_log_timeframe(self.from_dt, self.to_dt)
         return True
 
+    def show_transition_tags(self, rpt_pe_file):
+        '''
+        prints the tags for the transition
+        '''
+        t_obj = self.find_transition(rpt_pe2t_str(rpt_pe_file))
+        if not t_obj:
+            common_err("%s: transition not found" % rpt_pe_file)
+            return False
+        for tag in t_obj.tags:
+            print tag
+        return True
+
+    def _set_transition_tags(self, transition):
+        # limit the log scope temporarily
+        self.logobj.set_log_timeframe(transition.start_ts, transition.end_ts)
+
+        # search log, match regexes to tags
+        regexes = [
+            re.compile(r"(error|unclean)", re.I),
+            re.compile(r"crmd.*notice:\s+Operation\s+([^:]+):\s+(?!ok)"),
+        ]
+
+        for l in self.logobj.get_matches(regexes):
+            for rx in regexes:
+                m = rx.search(l)
+                if m:
+                    transition.tags.add(m.group(1).lower())
+
+        self.logobj.set_log_timeframe(self.from_dt, self.to_dt)
+
     def resource(self, *args):
         '''
         Show resource relevant logs.
@@ -1468,10 +1533,8 @@ class Report(object):
                 return False
             self.show_logs(log_l=l)
 
-    pe_details_header = \
-      "Date       Start    End       Filename      Client     User       Origin"
-    pe_details_separator = \
-      "====       =====    ===       ========      ======     ====       ======"
+    pe_details_header = "Date       Start    End       Filename      Client     User       Origin"
+    pe_details_separator = "====       =====    ===       ========      ======     ====       ======"
 
     def pe_detail_format(self, t_obj):
         l = [
@@ -1494,8 +1557,8 @@ class Report(object):
                 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)]
+        l = [long and self.pe_detail_format(t_obj) or self.pe_report_path(t_obj)
+             for t_obj in self._transitions if pe_file_in_range(t_obj.pe_file, a)]
         if long:
             l = [self.pe_details_header, self.pe_details_separator] + l
         return l
diff --git a/modules/idmgmt.py b/modules/idmgmt.py
index 24f988f..9bc348c 100644
--- a/modules/idmgmt.py
+++ b/modules/idmgmt.py
@@ -1,24 +1,10 @@
 # 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
+# See COPYING for license information.
+
+from . import constants
 import copy
-from msg import common_error, id_used_err
-import xmlutil
+from .msg import common_error, id_used_err
+from . import xmlutil
 
 
 '''
diff --git a/modules/log_patterns.py b/modules/log_patterns.py
index 12fe469..9513dca 100644
--- a/modules/log_patterns.py
+++ b/modules/log_patterns.py
@@ -1,4 +1,5 @@
 # Copyright (C) 2011 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# See COPYING for license information.
 #
 # log pattern specification
 #
@@ -20,58 +21,58 @@
 # [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:",
-		),
-	),
+    "resource": (
+        (  # detail 0
+            "lrmd.*%% (?:start|stop|promote|demote|migrate)",
+            "lrmd.*RA output: .%%:.*:stderr",
+            "lrmd.*WARN: Managed %%:.*exited",
+            "lrmd.*WARN: .* %% .*timed out$",
+            "crmd.*LRM operation %%_(?:start|stop|promote|demote|migrate)_.*confirmed=true",
+            "crmd.*LRM operation %%_.*Timed Out",
+            "[(]%%[)][[]",
+        ),
+        (  # detail 1
+            "lrmd.*%% (?:probe|notify)",
+            "lrmd.*Managed %%:.*exited",
+        ),
+    ),
+    "node": (
+        (  # detail 0
+            " %% .*Corosync.Cluster.Engine",
+            " %% .*Executive.Service.RELEASE",
+            " %% .*Requesting.shutdown",
+            " %% .*Shutdown.complete",
+            " %% .*Configuration.validated..Starting.heartbeat",
+            "pengine.*Scheduling Node %% for STONITH",
+            "crmd.* of %% failed",
+            "stonith-ng.*host '%%'",
+            "Exec.*on %% ",
+            "Node %% will be fenced",
+            "stonith-ng.*for %% timed",
+            "stonith-ng.*can not fence %%:",
+            "stonithd.*Succeeded.*node %%:",
+            "(?:lost|memb): %% ",
+            "crmd.*(?:NEW|LOST):.* %% ",
+            "Node return implies stonith of %% ",
+        ),
+        (  # detail 1
+        ),
+    ),
+    "quorum": (
+        (  # detail 0
+            "crmd.*Updating.quorum.status",
+            "crmd.*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
index 30834a9..25dad77 100644
--- a/modules/log_patterns_118.py
+++ b/modules/log_patterns_118.py
@@ -1,4 +1,5 @@
 # Copyright (C) 2012 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# See COPYING for license information.
 #
 # log pattern specification (for pacemaker v1.1.8)
 #
@@ -20,56 +21,56 @@
 # [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):",
-		),
-	),
+    "resource": (
+        (  # detail 0
+            "crmd.*Initiating.*%%_(?:start|stop|promote|demote|migrate)_",
+            "lrmd.*operation_finished: %%_",
+            "crmd.*LRM operation %%_(?:start|stop|promote|demote|migrate)_.*confirmed=true",
+            "crmd.*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.*Node %% will be fenced",
+            "crmd.*for %% failed",
+            "stonith-ng.*host '%%'",
+            "Exec.*on %% ",
+            "Node %% will be fenced",
+            "stonith-ng.*on %% for.*timed out",
+            "stonith-ng.*can not fence %%:",
+            "stonithd.*Succeeded.*node %%:",
+            "(?:lost|memb): %% ",
+            "crmd.*(?:NEW|LOST):.* %% ",
+            "Node return implies stonith of %% ",
+        ),
+        (  # detail 1
+        ),
+    ),
+    "quorum": (
+        (  # detail 0
+            "crmd.*Updating.quorum.status",
+            "crmd.*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
index d0ab271..5672c3f 100644
--- a/modules/main.py
+++ b/modules/main.py
@@ -1,37 +1,22 @@
 # 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
-#
+# See COPYING for license information.
 
 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
+from . import config
+from . import options
+from . import constants
+from .msg import err_buf, common_err
+from . import clidisplay
+from . import term
+from . import utils
+from . import userdir
 
-import ui_root
-import ui_context
+from . import ui_root
+from . import ui_context
 
 
 random.seed()
@@ -55,7 +40,7 @@ def load_rc(context, rcfile):
         try:
             if not context.run(inp):
                 raise ValueError("Error in RC file: " + rcfile)
-        except ValueError, msg:
+        except ValueError as msg:
             common_err(msg)
     f.close()
     sys.stdin = save_stdin
@@ -76,8 +61,9 @@ def exit_handler():
 # prefer the user set PATH
 def envsetup():
     mybinpath = os.path.dirname(sys.argv[0])
+    path = os.environ["PATH"].split(':')
     for p in mybinpath, config.path.crm_daemon_dir:
-        if p not in os.environ["PATH"].split(':'):
+        if p not in path:
             os.environ['PATH'] = "%s:%s" % (os.environ['PATH'], p)
 
 
@@ -92,76 +78,63 @@ def cib_prompt():
     return shadow
 
 
+def make_option_parser():
+    from optparse import OptionParser
+    parser = OptionParser(usage="""%prog [-h|--help] [OPTIONS] [SUBCOMMAND ARGS...]
+or %prog help SUBCOMMAND
+
+For a list of available subcommands, use %prog help.
+
+Use %prog without arguments for an interactive session.
+Call a subcommand directly for a "single-shot" use.
+Call %prog with a level name as argument to start an interactive
+session from that level.
+
+See the crm(8) man page or call %prog help for more details.""",
+                          version="%prog " + config.CRM_VERSION)
+    parser.disable_interspersed_args()
+    parser.add_option("-f", "--file", dest="filename", metavar="FILE",
+                      help="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).")
+    parser.add_option("-c", "--cib", dest="cib", metavar="CIB",
+                      help="Start the session using the given shadow CIB file. " +
+                      "Equivalent to `cib use <CIB>`.")
+    parser.add_option("-D", "--display", dest="display", metavar="OUTPUT_TYPE",
+                      help="Choose one of the output options: plain, color-always, color, or uppercase. " +
+                      "The default is color if the terminal emulation supports colors, " +
+                      "else plain.")
+    parser.add_option("-F", "--force", action="store_true", default=False, dest="force",
+                      help="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.")
+    parser.add_option("-n", "--no", action="store_true", default=False, dest="ask_no",
+                      help="Automatically answer no when prompted")
+    parser.add_option("-w", "--wait", action="store_true", default=False, dest="wait",
+                      help="Make crm wait for the cluster transition to finish " +
+                      "(for the changes to take effect) after each processed line.")
+    parser.add_option("-H", "--history", dest="history", metavar="DIR|FILE|SESSION",
+                      help="A directory or file containing a cluster report to load " +
+                      "into history, or the name of a previously saved history session.")
+    parser.add_option("-d", "--debug", action="store_true", default=False, dest="debug",
+                      help="Print verbose debugging information.")
+    parser.add_option("-R", "--regression-tests", action="store_true", default=False,
+                      dest="regression_tests",
+                      help="Enables extra verbose trace logging used by the regression " +
+                      "tests. Logs all external calls made by crmsh.")
+    parser.add_option("--scriptdir", dest="scriptdir", metavar="DIR",
+                      help="Extra directory where crm looks for cluster scripts, or a list " +
+                      "of directories separated by semi-colons (e.g. /dir1;/dir2;etc.).")
+    parser.add_option("-X", dest="profile", metavar="PROFILE",
+                      help="Collect profiling data and save in PROFILE.")
+    return parser
+
+
+option_parser = make_option_parser()
+
+
 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.
-    """
+    option_parser.print_usage(file=(sys.stderr if rc != 0 else sys.stdout))
     sys.exit(rc)
 
 
@@ -199,9 +172,11 @@ def add_quotes(args):
     return l
 
 
-def do_work(context, user_args):
-    compatibility_setup()
-
+def handle_noninteractive_use(context, user_args):
+    """
+    returns: either a status code of 0 or 1, or
+    None to indicate that nothing was done here.
+    """
     if options.shadow:
         if not context.run("cib use " + options.shadow):
             return 1
@@ -227,31 +202,51 @@ def do_work(context, user_args):
                 err_buf.reset_lineno(-1)
         else:
             return 1
+    return None
+
+
+def render_prompt(context):
+    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
+    return rendered_prompt
 
+
+def setup_context(context):
     if options.input_file and options.input_file != "-":
         try:
             sys.stdin = open(options.input_file)
-        except IOError, msg:
+        except IOError as msg:
             common_err(msg)
             usage(2)
 
     if options.interactive and not options.batch:
         context.setup_readline()
 
+
+def main_input_loop(context, user_args):
+    """
+    Main input loop for crmsh. Parses input
+    line by line.
+    """
+    compatibility_setup()
+    rc = handle_noninteractive_use(context, user_args)
+    if rc is not None:
+        return rc
+
+    setup_context(context)
+
     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)
+            inp = utils.multi_input(render_prompt(context))
             if inp is None:
                 if options.interactive:
                     rc = 0
@@ -259,12 +254,12 @@ def do_work(context, user_args):
             try:
                 if not context.run(inp):
                     rc = 1
-            except ValueError, msg:
+            except ValueError as msg:
                 rc = 1
                 common_err(msg)
         except KeyboardInterrupt:
             if options.interactive and not options.batch:
-                print "Ctrl-C, leaving"
+                print("Ctrl-C, leaving")
             context.quit(1)
     return rc
 
@@ -276,14 +271,13 @@ def compgen():
 
     options.shell_completion = True
 
-    #point = int(args[0])
+    # 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()
+    # remove [*]crm from commandline
+    idx = line.find('crm')
+    if idx >= 0:
+        line = line[idx+3:].lstrip()
 
     options.interactive = False
     ui = ui_root.Root()
@@ -292,59 +286,33 @@ def compgen():
     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:]
+            print(w[idx+1:])
     else:
         for w in context.complete(line):
-            print w
+            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)
+    opts, args = option_parser.parse_args()
+    config.core.debug = "yes" if opts.debug else config.core.debug
+    options.profile = opts.profile or options.profile
+    options.regression_tests = opts.regression_tests or options.regression_tests
+    config.color.style = opts.display or config.color.style
+    config.core.force = opts.force or config.core.force
+    if opts.filename:
+        err_buf.reset_lineno()
+        options.input_file, options.batch, options.interactive = opts.filename, True, False
+    options.history = opts.history or options.history
+    config.core.wait = opts.wait or config.core.wait
+    options.shadow = opts.cib or options.shadow
+    options.scriptdir = opts.scriptdir or options.scriptdir
+    options.ask_no = opts.ask_no
+    return args
 
 
 def profile_run(context, user_args):
     import cProfile
-    cProfile.runctx('do_work(context, user_args)',
+    cProfile.runctx('main_input_loop(context, user_args)',
                     globals(),
                     {'context': context, 'user_args': user_args},
                     filename=options.profile)
@@ -354,7 +322,7 @@ def profile_run(context, user_args):
         stats_cmd = "; ".join(['import pstats',
                                's = pstats.Stats("%s")' % options.profile,
                                's.sort_stats("cumulative").print_stats()'])
-        print "python -c '%s' | less" % (stats_cmd)
+        print("python -c '%s' | less" % (stats_cmd))
     return 0
 
 
@@ -376,14 +344,18 @@ def run():
             err_buf.reset_lineno()
             options.batch = True
         user_args = parse_options()
+        term._init()
         if options.profile:
             return profile_run(context, user_args)
         else:
-            return do_work(context, user_args)
+            return main_input_loop(context, user_args)
     except KeyboardInterrupt:
-        print "Ctrl-C, leaving"
+        print("Ctrl-C, leaving")
         sys.exit(1)
-    except ValueError, e:
+    except ValueError as e:
+        if config.core.debug:
+            import traceback
+            traceback.print_exc()
         common_err(str(e))
 
 # vim:ts=4:sw=4:et:
diff --git a/modules/msg.py b/modules/msg.py
index c216c15..71a6cdf 100644
--- a/modules/msg.py
+++ b/modules/msg.py
@@ -1,25 +1,11 @@
 # 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
-#
+# See COPYING for license information.
 
 import sys
 from lxml import etree
-import config
-import clidisplay
-import options
+from . import config
+from . import clidisplay
+from . import options
 
 ERR_STREAM = sys.stderr
 
@@ -30,7 +16,7 @@ class ErrorBuffer(object):
     '''
     def __init__(self):
         try:
-            import term
+            from . import term
             self._term = term
         except:
             self._term = None
@@ -252,9 +238,9 @@ def cib_no_elem_err(el_name):
 
 
 def cib_ver_unsupported_err(validator, rel):
-    err_buf.error("CIB not supported: validator '%s', release '%s'" %
+    err_buf.error("Unsupported CIB: validator '%s', release '%s'" %
                   (validator, rel))
-    err_buf.error("You may try the upgrade command")
+    err_buf.error("To upgrade an old (<1.0) schema, use the upgrade command.")
 
 
 def update_err(obj_id, cibadm_opt, xml, rc):
diff --git a/modules/options.py b/modules/options.py
index 4769021..4c6509d 100644
--- a/modules/options.py
+++ b/modules/options.py
@@ -1,26 +1,13 @@
 # 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
-#
+# See COPYING for license information.
 '''
 Session-only options (not saved).
 '''
 
 interactive = False
 batch = False
+ask_no = False
 regression_tests = False
 profile = ""
 history = "live"
diff --git a/modules/pacemaker.py b/modules/pacemaker.py
index 521374d..891f386 100644
--- a/modules/pacemaker.py
+++ b/modules/pacemaker.py
@@ -1,24 +1,9 @@
 # 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
-#
+# See COPYING for license information.
 
 import os
 import tempfile
 import copy
-import re
 from lxml import etree
 
 
@@ -34,27 +19,21 @@ def get_validate_name(cib_elem):
 
 
 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
+    return "rng"
 
 
 def get_schema_filename(validate_name):
-    if re.match(r"pacemaker-\d+\.\d+", validate_name):
+    if not validate_name.endswith('.rng'):
         return "%s.rng" % (validate_name)
-    return None
+    return validate_name
 
 
 def read_schema_local(validate_name, file_path):
     try:
-        f = open(file_path)
-        schema = f.read()
+        with open(file_path) as f:
+            return f.read()
     except IOError, msg:
-        raise PacemakerError("Cannot read the schema file: " + str(msg))
-
-    f.close()
-    return schema
+        raise PacemakerError("Cannot read schema file '%s': %s" % (file_path, msg))
 
 
 def delete_dir(dir_path):
@@ -145,11 +124,6 @@ class Schema(object):
 
         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:
@@ -351,9 +325,6 @@ class RngSchema(Schema):
         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
@@ -382,7 +353,6 @@ class RngSchema(Schema):
             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):
diff --git a/modules/parse.py b/modules/parse.py
index 3633682..4809c30 100644
--- a/modules/parse.py
+++ b/modules/parse.py
@@ -1,31 +1,18 @@
 # 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
-#
+# Copyright (C) 2013-2015 Kristoffer Gronlund <kgronlund at suse.com>
+# See COPYING for license information.
 
 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
+from . import constants
+from .ra import disambiguate_ra_type, ra_type_validate
+from . 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
+from . import xmlbuilder
+from . import xmlutil
 
 
 class ParseError(Exception):
@@ -327,15 +314,16 @@ class RuleParser(BaseParser):
     _UNARYOP_RE = re.compile(r'(%s)$' % ('|'.join(constants.unary_ops)), re.IGNORECASE)
     _BINOP_RE = None
 
-    _TERMINATORS = ('params', 'meta', 'utilization', 'operations', 'op', 'rule')
+    _TERMINATORS = ('params', 'meta', 'utilization', 'operations', 'op', 'rule', 'attributes')
 
-    def match_attr_list(self, name, tag):
+    def match_attr_list(self, name, tag, allow_empty=True):
         """
-        matches <name> [$id=<id>] [<score>:] <n>=<v> <n>=<v> ... | $id-ref=<id-ref>
+        matches [$id=<id>] [<score>:] <n>=<v> <n>=<v> ... | $id-ref=<id-ref>
+        if matchname is False, matches:
+        <n>=<v> <n>=<v> ...
         """
-        from cibconfig import cib_factory
+        from .cibconfig import cib_factory
 
-        self.match(name)
         xmlid = None
         if self.try_match_idspec():
             if self.matched(1) == '$id-ref':
@@ -350,21 +338,32 @@ class RuleParser(BaseParser):
             score = self.matched(1)
         rules = self.match_rules()
         values = self.match_nvpairs(minpairs=0)
+        if (allow_empty, xmlid, score, len(rules), len(values)) == (False, None, None, 0, 0):
+            return None
         return xmlbuilder.attributes(tag, rules, values, xmlid=xmlid, score=score)
 
-    def match_attr_lists(self, name_map):
+    def match_attr_lists(self, name_map, implicit_initial=None):
         """
         generator which matches attr_lists
         name_map: maps CLI name to XML name
         """
-        while self.try_match('|'.join(name_map.keys())):
+        to_match = '|'.join(name_map.keys())
+        if self.try_match(to_match):
+            name = self.matched(0).lower()
+            yield self.match_attr_list(name, name_map[name])
+        elif implicit_initial is not None:
+            attrs = self.match_attr_list(implicit_initial,
+                                         name_map[implicit_initial],
+                                         allow_empty=False)
+            if attrs is not None:
+                yield attrs
+        while self.try_match(to_match):
             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
+        from .cibconfig import cib_factory
 
         rules = []
         while self.try_match('rule'):
@@ -496,7 +495,7 @@ class RuleParser(BaseParser):
 
     def validate_score(self, score, noattr=False):
         if not noattr and score in olist(constants.score_types):
-            return constants.score_types[score.lower()]
+            return ["score", 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]
@@ -509,6 +508,43 @@ class RuleParser(BaseParser):
         else:
             return ['score-attribute', score]
 
+    def match_arguments(self, out, name_map, implicit_initial=None):
+        """
+        [<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
+
+        implicit_initial: when matching attr lists, if none match at first
+        parse an implicit initial token and then continue.
+        This is so for example: primitive foo Dummy state=1 is accepted when
+        params is the implicit initial.
+        """
+        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]
+        initial = True
+        while self.has_tokens():
+            t = self.current_token().lower()
+            if t in names:
+                initial = False
+                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)
+            elif initial:
+                initial = False
+                for attr_list in self.match_attr_lists(name_map,
+                                                       implicit_initial=implicit_initial):
+                    out.append(attr_list)
+            else:
+                break
+
 
 class NodeParser(RuleParser):
     _UNAME_RE = re.compile(r'([^:]+)(:(normal|member|ping|remote))?$', re.IGNORECASE)
@@ -536,9 +572,9 @@ class NodeParser(RuleParser):
         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)
+        self.match_arguments(out, {'attributes': 'instance_attributes',
+                                   'utilization': 'utilization'},
+                             implicit_initial='attributes')
         return out
 
 
@@ -591,7 +627,7 @@ class ResourceParser(RuleParser):
         out.append(node)
 
     def match_operations(self, out, match_id):
-        from cibconfig import cib_factory
+        from .cibconfig import cib_factory
 
         def is_op():
             return self.has_tokens() and self.current_token().lower() == 'op'
@@ -615,28 +651,6 @@ class ResourceParser(RuleParser):
         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)
 
@@ -668,7 +682,7 @@ class ResourceParser(RuleParser):
                                    'meta': 'meta_attributes',
                                    'utilization': 'utilization',
                                    'operations': 'operations',
-                                   'op': 'op'})
+                                   'op': 'op'}, implicit_initial='params')
         return out
 
     parse_primitive = _primitive_or_template
@@ -684,7 +698,7 @@ class ResourceParser(RuleParser):
         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'})
+                                   'meta': 'meta_attributes'}, implicit_initial='params')
         out.append(child)
         return out
 
@@ -710,7 +724,8 @@ class ResourceParser(RuleParser):
             children.append(child)
         xmlbuilder.maybe_set(out, 'description', self.try_match_description())
         self.match_arguments(out, {'params': 'instance_attributes',
-                                   'meta': 'meta_attributes'})
+                                   'meta': 'meta_attributes'},
+                             implicit_initial='params')
         for child in children:
             xmlbuilder.child(out, 'crmsh-ref', id=child)
         return out
@@ -727,8 +742,8 @@ class ConstraintParser(RuleParser):
 
     def parse_location(self):
         """
-        location <id> rsc [[$]<attribute>=<value>] <score>: <node>
-        location <id> rsc [[$]<attribute>=<value>] <rule> [<rule> ...]
+        location <id> <rsc> [[$]<attribute>=<value>] <score>: <node>
+        location <id> <rsc> [[$]<attribute>=<value>] <rule> [<rule> ...]
         rsc :: /<rsc-pattern>/
             | { <rsc-set> }
             | <rsc>
@@ -858,37 +873,27 @@ class ConstraintParser(RuleParser):
         parser = ResourceSet(suffix_type, tokens, self)
         return simple, parser.parse()
 
+    def _fmt(self, info, name):
+        if info[1]:
+            return [[name, info[0]], [name + '-' + info[2], info[1]]]
+        return [[name, info[0]]]
+
+    def _split_setref(self, typename, classifier):
+            rsc, typ = self.match_split()
+            typ, t = classifier(typ)
+            if typ and not t:
+                self.err("Invalid %s '%s' for '%s'" % (typename, typ, rsc))
+            return rsc, typ, t
+
     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')
+        ret = self._fmt(self._split_setref('role', self.validation.classify_role), 'rsc')
         if count == 2:
-            ret += fmt(rsc_role(), 'with-rsc')
+            ret += self._fmt(self._split_setref('role', self.validation.classify_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')
+        ret = self._fmt(self._split_setref('action', self.validation.classify_action), 'first')
+        return ret + self._fmt(self._split_setref('action', self.validation.classify_action), 'then')
 
 
 class OpParser(BaseParser):
@@ -923,7 +928,7 @@ class PropertyParser(RuleParser):
         return ('property', 'rsc_defaults', 'op_defaults')
 
     def parse(self, cmd):
-        from cibconfig import cib_factory
+        from .cibconfig import cib_factory
 
         setmap = {'property': 'cluster_property_set',
                   'rsc_defaults': 'meta_attributes',
@@ -956,9 +961,22 @@ class FencingOrderParser(BaseParser):
     <fencing-topology>
     <fencing-level id=<id> target=<text> index=<+int> devices="\w,\w..."/>
     </fencing-topology>
+
+    new:
+
+    from 1.1.14 on, target can be a node attribute value mapping:
+
+    attr:<name>=<value> maps to XML:
+
+    <fencing-topology>
+    <fencing-level id=<id> target-attribute=<text> target-value=<text>
+                   index=<+int> devices="\w,\w..."/>
+    </fencing-topology>
+
     """
 
-    _TARGET_RE = re.compile(r'([^:]+):$')
+    _TARGET_RE = re.compile(r'([\w=-]+):$')
+    _TARGET_ATTR_RE = re.compile(r'attr:([\w-]+)=([\w-]+)$')
 
     def can_parse(self):
         return ('fencing-topology', 'fencing_topology')
@@ -971,7 +989,9 @@ class FencingOrderParser(BaseParser):
         # (target, devices)
         raw_levels = []
         while self.has_tokens():
-            if self.try_match(self._TARGET_RE):
+            if self.try_match(self._TARGET_ATTR_RE):
+                target = (self.matched(1), self.matched(2))
+            elif self.try_match(self._TARGET_RE):
                 target = self.matched(1)
             else:
                 raw_levels.append((target, self.match_any()))
@@ -982,7 +1002,7 @@ class FencingOrderParser(BaseParser):
     def _postprocess_levels(self, raw_levels):
         from collections import defaultdict
         from itertools import repeat
-        from cibconfig import cib_factory
+        from .cibconfig import cib_factory
         if raw_levels[0][0] == "@@":
             def node_levels():
                 for node in cib_factory.node_id_list():
@@ -990,16 +1010,26 @@ class FencingOrderParser(BaseParser):
                         yield node, devices
             lvl_generator = node_levels
         else:
-            lvl_generator = lambda: raw_levels
+            def wrap_levels():
+                return raw_levels
+            lvl_generator = wrap_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
+            if isinstance(target, tuple):
+                c = xmlbuilder.child(out, 'fencing-level',
+                                     index=str(targets[target[0]]),
+                                     devices=devices)
+                c.set('target-attribute', target[0])
+                c.set('target-value', target[1])
+                targets[target[0]] += 1
+            else:
+                xmlbuilder.child(out, 'fencing-level',
+                                 target=target,
+                                 index=str(targets[target]),
+                                 devices=devices)
+                targets[target] += 1
 
         return out
 
@@ -1011,7 +1041,7 @@ class TagParser(BaseParser):
       ...
     </tag>
     """
-    _TAG_RE = re.compile(r"([^:]+):$")
+    _TAG_RE = re.compile(r"([a-zA-Z_][^\s:]*):?$")
 
     def can_parse(self):
         return ('tag',)
@@ -1019,7 +1049,7 @@ class TagParser(BaseParser):
     def parse(self, cmd):
         self.begin(cmd, min_args=2)
         self.match('tag')
-        self.match(self._TAG_RE)
+        self.match(self._TAG_RE, errmsg="Expected tag name")
         out = xmlbuilder.new('tag', id=self.matched(1))
         while self.has_tokens():
             e = xmlbuilder.new('obj_ref', id=self.match_resource())
@@ -1552,7 +1582,7 @@ class CliParser(object):
         '''
         if isinstance(s, unicode):
             try:
-                s = s.encode('ascii')
+                s = s.encode('ascii', errors='xmlcharrefreplace')
             except Exception, e:
                 common_err(e)
                 return False
diff --git a/modules/ra.py b/modules/ra.py
index 137f7e9..fa7867c 100644
--- a/modules/ra.py
+++ b/modules/ra.py
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 import os
 import subprocess
@@ -21,16 +7,16 @@ 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
+from . import cache
+from . import constants
+from . import config
+from . import options
+from . import userdir
+from . 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)
@@ -42,9 +28,6 @@ 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.
@@ -84,9 +67,6 @@ 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":
@@ -114,7 +94,7 @@ class RaOS(object):
 
     def classes(self):
         'List of classes.'
-        return "heartbeat lsb nagios ocf stonith".split()
+        return "heartbeat lsb nagios ocf stonith systemd".split()
 
     def types(self, ra_class="ocf", ra_provider=""):
         'List of types for a class.'
@@ -125,39 +105,49 @@ class RaOS(object):
         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)
+            l = self._stonith_types()
         elif ra_class == "nagios":
-            l = os_types_list("%s/check_*" % config.path.nagios_plugins)
-            l = [x.replace("check_", "") for x in l]
+            l = [x.replace("check_", "")
+                 for x in os_types_list("%s/check_*" % config.path.nagios_plugins)]
+        elif ra_class == "systemd":
+            l = self._systemd_types()
         l = list(set(l))
         l.sort()
         return l
 
+    def _stonith_types(self):
+        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)
+
+    def _systemd_types(self):
+        l = []
+        rc, lines = stdout2list("systemctl list-unit-files --full")
+        if rc != 0:
+            return l
+        t = re.compile(r'^(.+)\.service')
+        for line in lines:
+            m = t.search(line)
+            if m:
+                l.append(m.group(1))
+        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))
+        # TODO: check rc
         return l
 
     def meta(self, ra_class, ra_type, ra_provider):
@@ -196,8 +186,13 @@ def can_use_lrmadmin():
         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))
+    if v_this < v_min:
+        return False
+    if userdir.getuser() not in ("root", config.path.crm_daemon_user):
+        return False
+    if not (is_program(lrmadmin_prog) and is_process("lrmd")):
+        return False
+    return utils.ext_cmd(">/dev/null 2>&1 %s -C" % lrmadmin_prog) == 0
 
 
 def crm_resource_support():
@@ -205,16 +200,16 @@ def crm_resource_support():
     return s != ""
 
 
+ at utils.memoize
 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
+        common_debug("Using crm_resource for agent discovery")
+        return RaCrmResource()
+    if can_use_lrmadmin():
+        common_debug("Using lrmd for agent discovery")
+        return RaLrmd()
+    common_debug("Using OS for agent discovery")
+    return RaOS()
 
 
 def ra_classes():
@@ -245,13 +240,11 @@ def ra_providers_all(ra_class="ocf"):
     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)
+    ocf = os.path.join(os.environ["OCF_ROOT"], "resource.d")
+    if os.path.isdir(ocf):
+        return cache.store(id, sorted([s for s in os.listdir(ocf)
+                                       if os.path.isdir(os.path.join(ocf, s))]))
+    return []
 
 
 def ra_types(ra_class="ocf", ra_provider=""):
@@ -273,43 +266,37 @@ def ra_types(ra_class="ocf", ra_provider=""):
     return cache.store(id, list)
 
 
+ at utils.memoize
 def get_pe_meta():
-    if not constants.pe_metadata:
-        constants.pe_metadata = RAInfo("pengine", "metadata")
-    return constants.pe_metadata
+    return RAInfo("pengine", "metadata")
 
 
+ at utils.memoize
 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
+    info = RAInfo("crmd", "metadata")
+    info.exclude_from_completion(constants.crmd_metadata_do_not_complete)
+    return info
 
 
+ at utils.memoize
 def get_stonithd_meta():
-    if not constants.stonithd_metadata:
-        constants.stonithd_metadata = RAInfo("stonithd", "metadata")
-    return constants.stonithd_metadata
+    return RAInfo("stonithd", "metadata")
 
 
+ at utils.memoize
 def get_cib_meta():
-    if not constants.cib_metadata:
-        constants.cib_metadata = RAInfo("cib", "metadata")
-    return constants.cib_metadata
+    return RAInfo("cib", "metadata")
 
 
+ at utils.memoize
 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
+    meta = copy.deepcopy(get_crmd_meta())
+    meta.add_ra_params(get_pe_meta())
+    meta.add_ra_params(get_cib_meta())
+    return meta
 
 
+ at utils.memoize
 def get_properties_list():
     try:
         return get_properties_meta().params().keys()
@@ -426,15 +413,11 @@ class RAInfo(object):
             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")
+        if meta is None:
+            if not config.core.ignore_missing_metadata:
+                self.error("got no meta-data, does this RA exist?")
             return None
+        self.ra_elem = meta
         try:
             assert self.ra_elem.tag == 'resource-agent'
         except Exception:
@@ -489,8 +472,7 @@ class RAInfo(object):
             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]
+                if c.get("name") and c.get("name") not in self.excluded_from_completion]
 
     def actions(self):
         '''
@@ -622,7 +604,7 @@ class RAInfo(object):
             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))
+                common_warn("%s: action '%s' not found in Resource Agent meta-data" % (id, op))
                 rc |= 1
             if "interval" in n_ops[op]:
                 v = n_ops[op]["interval"]
@@ -657,6 +639,7 @@ class RAInfo(object):
     def meta(self):
         '''
         RA meta-data as raw xml.
+        Returns an etree xml object.
         '''
         sid = "ra_meta-%s" % self.ra_string()
         if cache.is_cached(sid):
@@ -667,8 +650,13 @@ class RAInfo(object):
             l = ra_if().meta(self.ra_class, self.ra_type, self.ra_provider)
         if not l:
             return None
+        try:
+            xml = etree.fromstring('\n'.join(l))
+        except Exception:
+            self.error("Cannot parse meta-data XML")
+            return None
         self.debug("read and cached meta-data")
-        return cache.store(sid, l)
+        return cache.store(sid, xml)
 
     def meta_pretty(self):
         '''
@@ -723,16 +711,14 @@ class RAInfo(object):
         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)
+        l = [head]
         longdesc = get_nodes_text(n, "longdesc")
         if longdesc:
-            longdesc = self.ra_tab + longdesc.replace("\n", "\n" + self.ra_tab) + '\n'
-            l.append(longdesc)
+            l.append(self.ra_tab + longdesc.replace("\n", "\n" + self.ra_tab) + '\n')
         return '\n'.join(l)
 
     def meta_parameter(self, param):
@@ -780,7 +766,16 @@ class RAInfo(object):
 
 
 def get_ra(r):
-    return RAInfo(r.get("class"), r.get("type"), r.get("provider"))
+    """
+    Argument is either an xml resource tag with class, provider and type attributes,
+    or a CLI style class:provider:type string.
+    """
+    if isinstance(r, basestring):
+        cls, provider, type = disambiguate_ra_type(r)
+    else:
+        cls, provider, type = r.get('class'), r.get('provider'), r.get('type')
+    # note order of arguments!
+    return RAInfo(cls, type, provider)
 
 
 #
@@ -833,4 +828,55 @@ def disambiguate_ra_type(s):
     pr = pick_provider(ra_providers(tp, cl)) if cl == 'ocf' else ''
     return cl, pr, tp
 
+
+def can_validate_agent(agent):
+    if utils.getuser() != 'root':
+        return False
+    if isinstance(agent, basestring):
+        c, p, t = disambiguate_ra_type(agent)
+        if c != "ocf":
+            return False
+        agent = RAInfo(c, t, p)
+        if agent.mk_ra_node() is None:
+            return False
+    if len(agent.ra_elem.xpath('.//actions/action[@name="validate-all"]')) < 1:
+        return False
+    return True
+
+
+def validate_agent(agentname, params):
+    """
+    Call the validate-all action on the agent, given
+    the parameter hash params.
+    agent: either a c:p:t agent name, or an RAInfo instance
+    params: a hash of agent parameters
+    Returns: (rc, out)
+    """
+    if not can_validate_agent(agentname):
+        return (-1, "")
+    if isinstance(agentname, basestring):
+        c, p, t = disambiguate_ra_type(agentname)
+        if c != "ocf":
+            raise ValueError("Only OCF agents are supported by this command")
+        agent = RAInfo(c, t, p)
+        if agent.mk_ra_node() is None:
+            return (-1, "")
+    else:
+        agent = agentname
+    if len(agent.ra_elem.xpath('.//actions/action[@name="validate-all"]')) < 1:
+        raise ValueError("validate-all action not supported by agent")
+
+    my_env = os.environ.copy()
+    my_env["OCF_ROOT"] = config.path.ocf_root
+    for k, v in params.iteritems():
+        my_env["OCF_RESKEY_" + k] = v
+    cmd = [os.path.join(config.path.ocf_root, "resource.d", agent.ra_provider, agent.ra_type), "validate-all"]
+    if options.regression_tests:
+        print ".EXT", " ".join(cmd)
+    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=my_env)
+    out, _ = p.communicate()
+    p.wait()
+    return p.returncode, out
+
+
 # vim:ts=4:sw=4:et:
diff --git a/modules/rsctest.py b/modules/rsctest.py
index dd167e1..587897d 100644
--- a/modules/rsctest.py
+++ b/modules/rsctest.py
@@ -1,25 +1,11 @@
 # 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
-#
+# See COPYING for license information.
 
 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
+from .msg import common_err, common_debug, common_warn, common_info
+from .utils import rmdir_r, quote, this_node, ext_cmd
+from .xmlutil import get_topmost_rsc, get_op_timeout, get_child_nvset_node, is_ms, is_cloned
 
 
 #
@@ -147,7 +133,7 @@ class RADriver(object):
         '''
         Execute an operation.
         '''
-        from crm_pssh import show_output
+        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")
@@ -166,11 +152,10 @@ class RADriver(object):
         '''defined in subclasses'''
         pass
 
-    def runop(self, op, nodes=None):
+    def runop(self, op, nodes=None, local_only=False):
         '''
         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
@@ -182,12 +167,16 @@ class RADriver(object):
             # 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
+        if local_only:
+            self.ec_l[this_node()] = ext_cmd(cmd)
+        else:
+            from .crm_pssh import do_pssh_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):
@@ -305,6 +294,31 @@ class RALSB(RADriver):
         return cmd
 
 
+class RASystemd(RADriver):
+    '''
+    Execute operations on systemd resources.
+    '''
+
+    # Error codes are meaningless for systemd...
+    SYSD_OK = 0
+    SYSD_ERR_GENERIC = 1
+    SYSD_NOT_RUNNING = 3
+
+    def __init__(self, *args):
+        RADriver.__init__(self, *args)
+        self.ec_ok = self.SYSD_OK
+        self.ec_stopped = self.SYSD_NOT_RUNNING
+        self.ec_master = self.unused
+
+    def set_rscenv(self, op):
+        RADriver.set_rscenv(self, op)
+
+    def exec_cmd(self, op):
+        op = "status" if op == "monitor" else op
+        cmd = "systemctl %s %s.service" % (op, self.ra_type)
+        return cmd
+
+
 class RAStonith(RADriver):
     '''
     Execute operations on Stonith resources.
@@ -355,7 +369,8 @@ class RAStonith(RADriver):
 ra_driver = {
     "ocf": RAOCF,
     "lsb": RALSB,
-    "stonith": RAStonith
+    "stonith": RAStonith,
+    "systemd": RASystemd
 }
 
 
@@ -416,9 +431,9 @@ def test_resources(resources, nodes, all_nodes):
         return True
 
     try:
-        import crm_pssh
+        from . import crm_pssh
     except ImportError:
-        common_err("pssh not installed, rsctest can not be executed")
+        common_err("Parallax SSH not installed, rsctest can not be executed")
         return False
     if not check_test_support(resources):
         return False
@@ -430,4 +445,31 @@ def test_resources(resources, nodes, all_nodes):
         rc |= test_node(node)
     return rc
 
+
+def call_resource(rsc, cmd, nodes, local_only):
+    """
+    Calls the given operation on the resource.
+    local_only: Only performs the call locally (don't use SSH).
+    """
+    ra_class = rsc.get("class")
+    if ra_class not in ra_driver:
+        common_err("Calling '%s' for resource not supported" % (cmd))
+        return False
+    d = ra_driver[ra_class](rsc, [])
+
+    from . import ra
+    agent = ra.get_ra(rsc)
+    actions = agent.actions().keys() + ['meta-data', 'validate-all']
+
+    if cmd not in actions:
+        common_err("action '%s' not supported by %s" % (cmd, ra.name))
+        return False
+    d.runop(cmd, nodes, local_only=local_only)
+    for node in nodes:
+        ok = d.is_ok(node)
+        if not ok:
+            common_err("%s failed with rc=%d on %s" %
+                       (cmd, d.op_status(node), node))
+    return all(d.is_ok(node) for node in nodes)
+
 # vim:ts=4:sw=4:et:
diff --git a/modules/schema.py b/modules/schema.py
index 879f859..df3f127 100644
--- a/modules/schema.py
+++ b/modules/schema.py
@@ -1,23 +1,27 @@
 # 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
+# See COPYING for license information.
+
+from . import config
+import re
+from .pacemaker import CrmSchema, PacemakerError
+from .msg import common_err
+
+
+def is_supported(name):
+    """
+    Check if the given name is a supported schema name
+    A short form is also accepted where the prefix
+    pacemaker- is implied.
+
+    Revision: The pacemaker schema version now
+    changes too often for a strict check to make sense.
+    Lets just check look for schemas we know we don't
+    support.
+    """
+    name = re.match(r'pacemaker-(\d+\.\d+)$', name)
+    if name:
+        return float(name.group(1)) > 0.9
+    return True
 
 
 def get_attrs(schema, name):
@@ -87,7 +91,10 @@ def _load_schema(cib):
 
 def init_schema(cib):
     global _crm_schema
-    _crm_schema = _load_schema(cib)
+    try:
+        _crm_schema = _load_schema(cib)
+    except PacemakerError, msg:
+        common_err(msg)
     reset()
 
 
diff --git a/modules/scripts.py b/modules/scripts.py
index 95fea0b..c3bb4ca 100644
--- a/modules/scripts.py
+++ b/modules/scripts.py
@@ -1,56 +1,1055 @@
-# 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
-#
+# Copyright (C) 2015 Kristoffer Gronlund <kgronlund at suse.com>
+# See COPYING for license information.
 
-import sys
-import time
-import random
 import os
-import shutil
-import getpass
+import re
 import subprocess
-import config
-import options
-from msg import err_buf
-import userdir
+import getpass
+import time
+import shutil
+import socket
+import random
+from copy import deepcopy
+from glob import glob
+from lxml import etree
 
 try:
-    from psshlib import api as pssh
-    has_pssh = True
+    import json
 except ImportError:
-    has_pssh = False
+    import simplejson as json
 
-import utils
 
 try:
-    import json
+    import parallax
+    has_parallax = True
 except ImportError:
-    import simplejson as json
+    has_parallax = False
 
 
-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'))
+from . import config
+from . import handles
+from . import options
+from . import userdir
+from . import utils
+from .msg import err_buf, common_debug
+
+
+_script_cache = None
+_script_version = 2.2
+_strict_handles = False
+
+_action_shortdescs = {
+    'cib': 'Configure cluster resources',
+    'install': 'Install packages',
+    'service': 'Manage system services',
+    'call': 'Run command on nodes',
+    'copy': 'Install file on nodes',
+    'crm': 'Run crm command',
+    'collect': 'Collect data from nodes',
+    'verify': 'Verify collected data',
+    'apply': 'Apply changes to nodes',
+    'apply_local': 'Apply changes to cluster'
+}
+
+
+class Text(object):
+    """
+    Idea: Replace all fields that may contain
+    references to data with Text objects, that
+    lazily resolve when asked to.
+    Context needed is the script in which this
+    Text resolves. What we do is that we install
+    the parameter values in the script, so we can
+    get it from here.
+
+    This can also then be responsible for the
+    various kinds of output cleanup/formatting
+    (desc, cib, etc)
+    """
+    DESC = 1
+    CIB = 2
+    SHORTDESC = 3
+
+    @staticmethod
+    def shortdesc(script, text):
+        return Text(script, text, type=Text.SHORTDESC)
+
+    @staticmethod
+    def desc(script, text):
+        return Text(script, text, type=Text.DESC)
+
+    @staticmethod
+    def cib(script, text):
+        return Text(script, text, type=Text.CIB)
+
+    @staticmethod
+    def isa(obj):
+        return isinstance(obj, basestring) or isinstance(obj, Text)
+
+    def __init__(self, script, text, type=None):
+        self.script = script
+        if isinstance(text, Text):
+            self.text = text.text
+        else:
+            self.text = text
+        self.type = type
+
+    def _parse(self):
+        val = self.text
+        if val in (True, False):
+            return "true" if val else "false"
+        if not isinstance(val, basestring):
+            return str(val)
+        return handles.parse(val, self.script.get('__values__', {})).strip()
+
+    def __repr__(self):
+        return repr(self.text)
+
+    def __str__(self):
+        if self.type == self.DESC:
+            return format_desc(self._parse())
+        elif self.type == self.SHORTDESC:
+            return self._parse()
+        elif self.type == self.CIB:
+            return format_cib(self._parse())
+        return self._parse()
+
+    def __eq__(self, obj):
+        return str(self) == str(obj)
+
+
+def _strip(desc):
+    if desc is None:
+        return None
+    return desc.strip()
+
+
+def format_desc(desc):
+    import textwrap
+    return '\n\n'.join([textwrap.fill(para) for para in desc.split('\n\n') if para.strip()])
+
+
+def format_cib(text):
+    text = re.sub(r'[ ]+', ' ', text)
+    text = re.sub(r'\n[ \t\f\v]+', '\n\t', text)
+    i = 0
+    while True:
+        i = text.find('\n\t\n')
+        if i < 0:
+            break
+        text = text[:i] + text[i+2:]
+    return text
+
+
+def space_cib(text):
+    """
+    After merging CIB commands, space separate lines out
+    """
+    return re.sub(r'\n([^\t])', r'\n\n\1', re.sub(r'[\n\r]+', r'\n', text))
+
+
+class Actions(object):
+    """
+    Each method in this class handles a particular action.
+    """
+    @staticmethod
+    def _parse(script, action):
+        """
+        action: action data (dict)
+        params: flat list of parameter values
+        values: processed list of parameter values (for handles.parse)
+
+        Converts {'cib': "primitive..."} into {"name": "cib", "value": "primitive..."}
+        Each action has two values: "value" may be a non-textual object
+        depending on the type of action. "text" is visual context to display
+        to a user (so a cleaned up CIB, or the list of packages to install)
+        """
+        name = action['name']
+        action['value'] = action[name]
+        del action[name]
+        action['text'] = ''
+        value = action['value']
+        if name == 'install':
+            if Text.isa(value):
+                action['value'] = str(value).split()
+            action['text'] = ' '.join(action['value'])
+        # service takes a list of objects with a single key;
+        # mapping service: state
+        # the text field will be converted to lines where
+        # each line is <service> -> <state>
+        elif name == 'service':
+            if Text.isa(value):
+                value = [dict([v.split(':', 1)]) for v in str(value).split()]
+                action['value'] = value
+
+            def arrow(v):
+                return ' -> '.join(x.items()[0])
+            action['text'] = '\n'.join([arrow(x) for x in value])
+        elif name == 'cib' or name == 'crm':
+            action['text'] = str(Text.cib(script, value))
+            action['value'] = _remove_empty_lines(action['text'])
+        elif name == 'call':
+            action['value'] = Text(script, value)
+        elif name == 'copy':
+            action['value'] = Text(script, value)
+            action['template'] = _make_boolean(action.get('template', False))
+            action['to'] = Text(script, action.get('to', action['value']))
+            action['text'] = "%s -> %s" % (action['value'], action['to'])
+
+        if 'shortdesc' not in action:
+            action['shortdesc'] = _action_shortdescs.get(name, '')
+        else:
+            action['shortdesc'] = Text.shortdesc(script, action['shortdesc'])
+        if 'longdesc' not in action:
+            action['longdesc'] = ''
+        else:
+            action['longdesc'] = Text.desc(script, action['longdesc'])
+        if 'when' in action:
+            when = action['when']
+            if re.search(r'\{\{.*\}\}', when):
+                action['when'] = Text(script, when)
+            elif when:
+                action['when'] = Text(script, '{{%s}}' % (when))
+            else:
+                del action['when']
+
+    @staticmethod
+    def _mergeable(action):
+        return action['name'] in ('cib', 'crm', 'install', 'service')
+
+    @staticmethod
+    def _merge(into, new):
+        """
+        Merge neighbour actions.
+        Note: When this is called, all text values
+        should already be "reduced", that is, any
+        variable references already resolved.
+        """
+        if into.get('nodes') != new.get('nodes'):
+            return False
+        if into['name'] in ('cib', 'crm'):
+            into['value'] = '\n'.join([str(into['value']), str(new['value'])])
+            into['text'] = space_cib('\n'.join([str(into['text']), str(new['text'])]))
+        elif into['name'] == 'service':
+            into['value'].extend(new['value'])
+            into['text'] = '\n'.join([str(into['text']), str(new['text'])])
+        elif into['name'] == 'install':
+            into['value'].extend(new['value'])
+            into['text'] = ' '.join([str(into['text']), str(new['text'])])
+        if new['shortdesc']:
+            newd = str(new['shortdesc'])
+            if newd != str(into['shortdesc']):
+                into['shortdesc'] = _strip(newd)
+        if new['longdesc']:
+            newd = str(new['longdesc'])
+            if newd != str(into['longdesc']):
+                into['longdesc'] = newd
+        return True
+
+    @staticmethod
+    def _needs_sudo(action):
+        if action['name'] == 'call' and action.get('sudo'):
+            return True
+        return action['name'] in ('apply', 'apply_local', 'install', 'service')
+
+    def __init__(self, run, action):
+        self._run = run
+        self._action = action
+        self._value = action['value']
+        if not isinstance(self._value, list):
+            self._value = str(self._value)
+        self._text = str(action['text'])
+        self._nodes = str(action.get('nodes', ''))
+
+    def collect(self):
+        "input: shell command"
+        self._run.run_command(self._nodes or 'all', self._value, True)
+        self._run.record_json()
+
+    def validate(self):
+        "input: shell command"
+        self._run.run_command(None, self._value, True)
+        self._run.validate_json()
+
+    def apply(self):
+        "input: shell command"
+        self._run.run_command(self._nodes or 'all', self._value, True)
+        self._run.record_json()
+
+    def apply_local(self):
+        "input: shell command"
+        self._run.run_command(None, self._value, True)
+        self._run.record_json()
+
+    def report(self):
+        "input: shell command"
+        self._run.run_command(None, self._value, False)
+        self._run.report_result()
+
+    def call(self):
+        """
+        input: shell command / script
+
+        TODO: actually allow script here
+        """
+        self._run.call(self._nodes, self._value)
+
+    def copy(self):
+        """
+        copy: <from>
+        to: <path>
+        template: true|false
+
+        TODO: FIXME: Verify that it works...
+        TODO: FIXME: Error handling
+        """
+        if not os.path.exists(self._value):
+            raise ValueError("File not found: %s" % (self._value))
+        if self._action['template']:
+            fn = self._run.str2tmp(str(Text.cib(self._run.script, open(self._value).read())))
+            self._value = fn
+        self._run.copy_file(self._nodes, self._value, str(self._action['to']))
+
+    def _crm_do(self, act):
+        fn = self._run.str2tmp(_join_script_lines(self._value))
+        if config.core.debug:
+            args = '-d --wait --no'
+        else:
+            args = '--wait --no'
+        if self._action.get('force'):
+            args = args + ' --force'
+        self._run.call(None, 'crm %s %s %s' % (args, act, fn))
+
+    def crm(self):
+        """
+        input: crm command sequence
+        """
+        return self._crm_do('-f')
+
+    def cib(self):
+        "input: cli configuration script"
+        return self._crm_do('configure load update')
+
+    def install(self):
+        """
+        input: list of packages
+        or: map of <os>: <packages>
+        """
+        self._run.execute_shell(self._nodes or 'all', '''#!/usr/bin/env python
+import crm_script
+import crm_init
+
+crm_init.install_packages(%s)
+crm_script.exit_ok(True)
+        ''' % (self._value))
+
+    def service(self):
+        values = []
+        for s in self._value:
+            for v in s.iteritems():
+                values.append(v)
+        services = "\n".join([('crm_script.service%s' % repr(v)) for v in values])
+        self._run.execute_shell(self._nodes or 'all', '''#!/usr/bin/env python
+import crm_script
+import crm_init
+
+%s
+crm_script.exit_ok(True)
+''' % (services))
+
+    def include(self):
+        """
+        Treated differently: at parse time,
+        the include actions should disappear
+        and be replaced with actions generated
+        from the include. Either from an included
+        script, or a cib generated from an agent
+        include.
+        """
+
+_actions = dict([(n, getattr(Actions, n)) for n in dir(Actions) if not n.startswith('_')])
+
+
+def _find_action(action):
+    """return name of action for action"""
+    for a in _actions.keys():
+        if a in action:
+            return a
+    return None
+
+
+def _make_options(params):
+    "Setup parallax options."
+    opts = parallax.Options()
+    opts.inline = True
+    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 _parse_yaml(scriptname, scriptfile):
+    data = None
+    try:
+        import yaml
+        with open(scriptfile) as f:
+            data = yaml.load(f)
+            if isinstance(data, list):
+                data = data[0]
+    except ImportError as e:
+        raise ValueError("Failed to load yaml module: %s" % (e))
+    except Exception as e:
+        raise ValueError("Failed to parse script main: %s" % (e))
+
+    if data:
+        ver = data.get('version')
+        if ver is None or str(ver) != str(_script_version):
+            data = _upgrade_yaml(data)
+
+    if 'parameters' in data:
+        data['steps'] = [{'parameters': data['parameters']}]
+        del data['parameters']
+    elif 'steps' not in data:
+        data['steps'] = []
+    data['name'] = scriptname
+    data['dir'] = os.path.dirname(scriptfile)
+    return data
+
+
+def _rename(obj, key, to):
+    if key in obj:
+        obj[to] = obj[key]
+        del obj[key]
+
+
+def _upgrade_yaml(data):
+    """
+    Upgrade a parsed yaml document from
+    an older version.
+    """
+    if 'version' in data and data['version'] > _script_version:
+        raise ValueError("Unknown version (expected < %s, got %s)" % (_script_version, data['version']))
+
+    data['version'] = _script_version
+    data['category'] = data.get('category', 'Legacy')
+    _rename(data, 'name', 'shortdesc')
+    _rename(data, 'description', 'longdesc')
+
+    data['actions'] = data.get('steps', [])
+    paramstep = {'parameters': data.get('parameters', [])}
+    data['steps'] = [paramstep]
+    if 'parameters' in data:
+        del data['parameters']
+
+    for p in paramstep['parameters']:
+        _rename(p, 'description', 'shortdesc')
+        _rename(p, 'default', 'value')
+        if 'required' not in p:
+            p['required'] = 'value' not in p
+
+    for action in data['actions']:
+        _rename(action, 'name', 'shortdesc')
+
+    return data
+
+
+_hawk_template_cache = {}
+
+
+def _parse_hawk_template(workflow, name, type, step, actions):
+    """
+    Convert a hawk template into steps + a cib action
+    """
+    path = os.path.join(os.path.dirname(workflow), '../templates', type + '.xml')
+    if path in _hawk_template_cache:
+        xml = _hawk_template_cache[path]
+    elif os.path.isfile(path):
+        xml = etree.parse(path).getroot()
+        common_debug("Found matching template: %s" % (path))
+        _hawk_template_cache[path] = xml
+    else:
+        raise ValueError("Template does not exist: %s" % (path))
+
+    step['shortdesc'] = _strip(''.join(xml.xpath('./shortdesc/text()')))
+    step['longdesc'] = ''.join(xml.xpath('./longdesc/text()'))
+
+    actions.append({'cib': _hawk_to_handles(name, xml.xpath('./crm_script')[0])})
+
+    for item in xml.xpath('./parameters/parameter'):
+        obj = {}
+        obj['name'] = item.get('name')
+        obj['required'] = item.get('required', False)
+        content = next(item.iter('content'))
+        obj['type'] = content.get('type', 'string')
+        val = content.get('default', content.get('value', None))
+        if val:
+            obj['value'] = val
+        obj['shortdesc'] = _strip(''.join(item.xpath('./shortdesc/text()')))
+        obj['longdesc'] = ''.join(item.xpath('./longdesc/text()'))
+        step['parameters'].append(obj)
+
+
+def _mkhandle(pfx, scope, text):
+    if scope:
+        return '{{%s%s:%s}}' % (pfx, scope, text)
+    else:
+        return '{{%s%s}}' % (pfx, text)
+
+
+def _hawk_to_handles(context, tag):
+    """
+    input: a context name to prefix variable references with (may be empty)
+    and a crm_script tag
+    output: text with {{handles}}
+    """
+    s = ""
+    s += tag.text
+    for c in tag:
+        if c.tag == 'if':
+            cond = c.get('set')
+            if cond:
+                s += _mkhandle('#', context, cond)
+                s += _hawk_to_handles(context, c)
+                s += _mkhandle('/', context, cond)
+        elif c.tag == 'insert':
+            param = c.get('param')
+            src = c.get('from_template') or context
+            s += _mkhandle('', src, param)
+        s += c.tail
+    return s
+
+
+def _parse_hawk_workflow(scriptname, scriptfile):
+    """
+    Reads a hawk workflow into a script.
+
+    TODO: Parse hawk workflows that invoke legacy cluster scripts?
+    """
+    xml = etree.parse(scriptfile).getroot()
+    if xml.tag != "workflow":
+        raise ValueError("Not a hawk workflow: %s" % (scriptfile))
+    data = {
+        'version': 2.2,
+        'name': scriptname,
+        'shortdesc': _strip(''.join(xml.xpath('./shortdesc/text()'))),
+        'longdesc': ''.join(xml.xpath('./longdesc/text()')),
+        'category': ''.join(xml.xpath('./@category')) or 'Wizard',
+        'dir': None,
+        'steps': [],
+        'actions': [],
+    }
+
+    # the parameters together form a step with an optional shortdesc
+    # then each template becomes an additional step with an optional shortdesc
+    paramstep = {
+        'shortdesc': _strip(''.join(xml.xpath('./parameters/stepdesc/text()'))),
+        'parameters': []
+    }
+    data['steps'].append(paramstep)
+    for item in xml.xpath('./parameters/parameter'):
+        obj = {}
+        obj['name'] = item.get('name')
+        obj['required'] = item.get('required', False)
+        obj['unique'] = item.get('unique', False)
+        content = next(item.iter('content'))
+        obj['type'] = content.get('type', 'string')
+        val = content.get('default', content.get('value', None))
+        if val is not None:
+            obj['value'] = val
+        obj['shortdesc'] = _strip(''.join(item.xpath('./shortdesc/text()')))
+        obj['longdesc'] = ''.join(item.xpath('./longdesc/text()'))
+        paramstep['parameters'].append(obj)
+
+    data['actions'] = []
+
+    for item in xml.xpath('./templates/template'):
+        templatestep = {
+            'shortdesc': _strip(''.join(item.xpath('./stepdesc/text()'))),
+            'name': item.get('name'),
+            # Optional steps in the legacy wizards was broken (!?)
+            'required': True,  # item.get('required'),
+            'parameters': []
+        }
+        data['steps'].append(templatestep)
+
+        _parse_hawk_template(scriptfile, item.get('name'), item.get('type', item.get('name')),
+                             templatestep, data['actions'])
+        for override in item.xpath('./override'):
+            name = override.get("name")
+            for param in templatestep['parameters']:
+                if param['name'] == name:
+                    param['value'] = override.get("value")
+                    param['required'] = False
+                    break
+
+    data['actions'].append({'cib': _hawk_to_handles('', xml.xpath('./crm_script')[0])})
+
+    if config.core.debug:
+        import pprint
+        print("Parsed hawk workflow:")
+        pprint.pprint(data)
+    return data
+
+
+def _build_script_cache():
+    global _script_cache
+    if _script_cache is not None:
+        return
+    _script_cache = {}
+    for d in _script_dirs():
+        if d:
+            for s in glob(os.path.join(d, '*/main.yml')):
+                name = os.path.dirname(s).split('/')[-1]
+                if name not in _script_cache:
+                    _script_cache[name] = os.path.join(d, s)
+            for s in glob(os.path.join(d, '*.yml')):
+                name = os.path.splitext(os.path.basename(s))[0]
+                if name not in _script_cache:
+                    _script_cache[name] = os.path.join(d, s)
+            for s in glob(os.path.join(d, 'workflows/*.xml')):
+                name = os.path.splitext(os.path.basename(s))[0]
+                if name not in _script_cache:
+                    _script_cache[name] = os.path.join(d, s)
+
+
+def list_scripts():
+    '''
+    List the available cluster installation scripts.
+    Yields the names of the main script files.
+    '''
+    _build_script_cache()
+    return sorted(_script_cache.keys())
+
+
+def _meta_text(meta, tag):
+    for c in meta.iterchildren(tag):
+        return c.text
+    return ''
+
+
+def _listfind(needle, haystack, keyfn):
+    for x in haystack:
+        if keyfn(x) == needle:
+            return x
+    return None
+
+
+def _listfindpend(needle, haystack, keyfn, orfn):
+    for x in haystack:
+        if keyfn(x) == needle:
+            return x
+    x = orfn()
+    haystack.append(x)
+    return x
+
+
+def _make_cib_for_agent(name, agent, data, ops):
+    aid = "{{%s:id}}" % (name) if name else "{{id}}"
+    template = ['primitive %s %s' % (aid, agent)]
+    params = []
+    ops = [op.strip() for op in ops.split('\n') if op.strip()]
+    for param in data['parameters']:
+        paramname = param['name']
+        if paramname == 'id':
+            # FIXME: What if the resource actually has a parameter named id?
+            continue
+        path = ':'.join((name, paramname)) if name else paramname
+        params.append('{{#%s}}%s="{{%s}}"{{/%s}}' % (path, paramname, path, path))
+    ret = '\n\t'.join(template + params + ops)
     return ret
 
 
+def _merge_objects(o1, o2):
+    for key, value in o2.iteritems():
+        o1[key] = value
+
+
+def _lookup_step(name, steps, stepname):
+    for step in steps:
+        if step.get('name', '') == stepname:
+            return step
+    if not stepname and len(steps) == 1:
+        return steps[0]
+    if not stepname:
+        raise ValueError("Parameter '%s' not found" % (name))
+    raise ValueError("Referenced step '%s' not found in '%s'" % (stepname, name))
+
+
+def _process_agent_include(script, include):
+    import ra
+    agent = include['agent']
+    info = ra.get_ra(agent)
+    meta = info.meta()
+    if meta is None:
+        raise ValueError("No meta-data for agent: %s" % (agent))
+    name = include.get('name', meta.get('name'))
+    if not name:
+        cls, provider, type = ra.disambiguate_ra_type(agent)
+        name = type
+    if 'name' not in include:
+        include['name'] = name
+    step = _listfindpend(name, script['steps'], lambda x: x.get('name'), lambda: {
+        'name': name,
+        'longdesc': '',
+        'shortdesc': '',
+        'parameters': [],
+    })
+    step['longdesc'] = include.get('longdesc') or _meta_text(meta, 'longdesc')
+    step['shortdesc'] = _strip(include.get('shortdesc') or _meta_text(meta, 'shortdesc'))
+    step['required'] = include.get('required', True)
+    step['parameters'].append({
+        'name': 'id',
+        'shortdesc': 'Identifier for the cluster resource',
+        'longdesc': '',
+        'required': True,
+        'unique': True,
+        'type': 'resource',
+    })
+
+    def newparamobj(param):
+        pname = param.get('name')
+        return _listfindpend(pname, step['parameters'], lambda x: x.get('name'), lambda: {'name': pname})
+
+    for param in meta.xpath('./parameters/parameter'):
+        pobj = newparamobj(param)
+        pobj['required'] = _make_boolean(param.get('required', False))
+        pobj['unique'] = _make_boolean(param.get('unique', False))
+        pobj['longdesc'] = _meta_text(param, 'longdesc')
+        pobj['shortdesc'] = _strip(_meta_text(param, 'shortdesc'))
+        # set 'advanced' flag on all non-required agent parameters by default
+        # a UI should hide these parameters unless "show advanced" is set
+        pobj['advanced'] = not pobj['required']
+        ctype = param.xpath('./content/@type')
+        cexample = param.xpath('./content/@default')
+        if ctype:
+            pobj['type'] = ctype[0]
+        if cexample:
+            pobj['example'] = cexample[0]
+
+    for param in include.get('parameters', []):
+        pobj = newparamobj(param)
+        # Make any overriden parameters non-advanced
+        # unless explicitly set to advanced
+        pobj['advanced'] = False
+        for key, value in param.iteritems():
+            if key in ('shortdesc', 'longdesc'):
+                pobj[key] = value
+            elif key == 'value':
+                pobj[key] = Text(script, value)
+            else:
+                pobj[key] = value
+            if 'value' in pobj:
+                pobj['required'] = False
+
+    # If the script doesn't have any base parameters
+    # and the name of this step is the same as the
+    # script name itself, then make this the base step
+    hoist = False
+    hoist_from = None
+    if step['name'] == script['name']:
+        zerostep = _listfind('', script['steps'], lambda x: x.get('name', ''))
+        if not zerostep:
+            hoist = True
+        elif zerostep.get('parameters'):
+            zp = zerostep['parameters']
+            for pname in [p['name'] for p in step['parameters']]:
+                if _listfind(pname, zp, lambda x: x['name']):
+                    break
+            else:
+                hoist, hoist_from = True, zerostep
+
+    # use step['name'] here in case we did the zerostep hoist
+    step['value'] = Text.cib(script, _make_cib_for_agent('' if hoist else step['name'],
+                                                         agent, step, include.get('ops', '')))
+
+    if hoist:
+        step['name'] = ''
+        if hoist_from:
+            step['parameters'] = hoist_from['parameters'] + step['parameters']
+            script['steps'] = [s for s in script['steps'] if s != hoist_from]
+
+    if not step['name']:
+        del step['name']
+
+    # this works despite possible hoist above,
+    # since name is still the actual name
+    for action in script['actions']:
+        if 'include' in action and action['include'] == name:
+            del action['include']
+            action['cib'] = step['value']
+
+
+def _process_script_include(script, include):
+    script_name = include['script']
+    if 'name' not in include:
+        include['name'] = script_name
+    subscript = load_script(script_name)
+    name = include['name']
+
+    scriptstep = {
+        'name': name,
+        'shortdesc': subscript['shortdesc'],
+        'longdesc': subscript['longdesc'],
+        'required': _make_boolean(include.get('required', True)),
+        'steps': deepcopy(subscript['steps']),
+        'sub-script': subscript,
+    }
+
+    def _merge_step_params(step, params):
+        for param in params:
+            _merge_step_param(step, param)
+
+    def _merge_step_param(step, param):
+        for p in step.get('parameters', []):
+            if p['name'] == param['name']:
+                for key, value in param.iteritems():
+                    if key in ('shortdesc', 'longdesc'):
+                        p[key] = value
+                    elif key == 'value' and Text.isa(value):
+                        p[key] = Text(script, value)
+                    else:
+                        p[key] = value
+                if 'value' in p:
+                    p['required'] = False
+                break
+        else:
+            raise ValueError("Referenced parameter '%s' not found in '%s'" % (param['name'], name))
+
+    for incparam in include.get('parameters', []):
+        if 'step' in incparam and 'name' not in incparam:
+            _merge_step_params(_lookup_step(name, scriptstep.get('steps', []), incparam['step']),
+                               incparam['parameters'])
+        else:
+            _merge_step_param(_lookup_step(name, scriptstep.get('steps', []), ''),
+                              incparam)
+
+    script['steps'].append(scriptstep)
+
+
+def _process_include(script, include):
+    """
+    includes add parameter steps and actions
+    an agent include works like a hawk template:
+    it adds a parameter step
+    a script include however adds any number of
+    parameter steps and actions
+
+    OK. here's what to do: Don't rescope the steps
+    and actions. Instead, keep the actions attached
+    to script step 0, as above. And for each step, add
+    a scope which states its scope. Then, when evaluating
+    handles, build custom environments for those scopes to
+    pass into handles.parse.
+
+    This is just for scripts, no need to do this for agents.
+    Of course, how about scripts that include other scripts?
+    _scope has to be a list which gets expanded...
+    """
+    if 'agent' in include:
+        return _process_agent_include(script, include)
+
+    elif 'script' in include:
+        return _process_script_include(script, include)
+    else:
+        raise ValueError("Unknown include type: %s" % (', '.join(include.keys())))
+
+
+def _postprocess_script_step(script, step):
+    if 'name' in step and not step['name']:
+        del step['name']
+    step['required'] = _make_boolean(step.get('required', True))
+    step['shortdesc'] = _strip(step.get('shortdesc', ''))
+    step['longdesc'] = step.get('longdesc', '')
+    for p in step.get('parameters', []):
+        if 'name' not in p:
+            raise ValueError("Parameter has no name: %s" % (p.keys()))
+        p['shortdesc'] = _strip(p.get('shortdesc', ''))
+        p['longdesc'] = p.get('longdesc', '')
+        if 'default' in p and 'value' not in p:
+            p['value'] = p['default']
+            del p['default']
+        if 'value' in p:
+            if p['value'] is None:
+                del p['value']
+            elif isinstance(p['value'], basestring):
+                p['value'] = Text(script, p['value'])
+        if 'required' not in p:
+            p['required'] = False
+        else:
+            p['required'] = _make_boolean(p['required'])
+        if 'advanced' in p:
+            p['advanced'] = _make_boolean(p['advanced'])
+        else:
+            p['advanced'] = False
+        if 'unique' in p:
+            p['unique'] = _make_boolean(p['unique'])
+        else:
+            p['unique'] = False
+        if 'type' not in p or p['type'] == '':
+            if p['name'] == 'id':
+                p['type'] = 'resource'
+            else:
+                p['type'] = 'string'
+    for s in step.get('steps', []):
+        _postprocess_script_step(script, s)
+
+
+def _postprocess_script_steps(script):
+    def empty(step):
+        if 'parameters' in step and len(step['parameters']) > 0:
+            return False
+        if 'steps' in step and len(step['steps']) > 0:
+            return False
+        return True
+
+    script['steps'] = [step for step in script['steps'] if not empty(step)]
+
+    for step in script['steps']:
+        _postprocess_script_step(script, step)
+
+
+def _postprocess_script(script):
+    """
+    Post-process the parsed script into an executable
+    form. This means parsing all included agents and
+    scripts, merging parameters, steps and actions.
+    """
+    ver = script.get('version')
+    if ver is None or str(ver) != str(_script_version):
+        raise ValueError("Unsupported script version (expected %s, got %s)" % (_script_version, repr(ver)))
+
+    if 'category' not in script:
+        script['category'] = 'Custom'
+
+    if 'actions' not in script:
+        script['actions'] = []
+
+        # if we include subscripts but have no defined actions, assume that's a
+        # mistake and generate include actions for all includes
+        for inc in [{"include": inc['name']} for inc in script.get('include', [])]:
+            script['actions'].append(inc)
+
+    _postprocess_script_steps(script)
+
+    # Includes may add steps, or modify parameters,
+    # but assume that any included data is already
+    # postprocessed. To run this before the
+    # step processing would risk replacing Text() objects
+    # with references to other scripts with references
+    # to this script.
+    for inc in script.get('include', []):
+        _process_include(script, inc)
+
+    for action in script['actions']:
+        if 'include' in action:
+            includes = [inc['name'] for inc in script.get('include', [])]
+            if action['include'] not in includes:
+                raise ValueError("Script references '%s', but only includes: %s" %
+                                 (action['include'], ', '.join(includes)))
+
+    if 'include' in script:
+        del script['include']
+
+    def _setdesc(name):
+        desc = script.get(name)
+        if desc is None:
+            desc = ''
+        if not desc:
+            if script['steps'] and script['steps'][0][name]:
+                desc = script['steps'][0][name]
+                script['steps'][0][name] = ''
+        script[name] = desc
+    _setdesc('shortdesc')
+    _setdesc('longdesc')
+
+    return script
+
+
+def _join_script_lines(txt):
+    s = ""
+    current_line = ""
+    for line in [line for line in txt.split('\n')]:
+        if not line.strip():
+            pass
+        elif re.match('^\s+\S', line):
+            current_line += line
+        else:
+            if current_line.strip():
+                s += current_line + "\n"
+            current_line = line
+    if current_line:
+        s += current_line + "\n"
+    return s
+
+
+def _load_script_file(script, filename):
+    if filename.endswith('.yml'):
+        parsed = _parse_yaml(script, filename)
+    elif filename.endswith('.xml'):
+        parsed = _parse_hawk_workflow(script, filename)
+    if parsed is None:
+        raise ValueError("Failed to parse script: %s (%s)" % (script, filename))
+    obj = _postprocess_script(parsed)
+    if 'name' in obj:
+        script = obj['name']
+    if script not in _script_cache or isinstance(_script_cache[script], basestring):
+        _script_cache[script] = obj
+    return obj
+
+
+def load_script_string(script, yml):
+    _build_script_cache()
+    import cStringIO
+    import yaml
+    data = yaml.load(cStringIO.StringIO(yml))
+    if isinstance(data, list):
+        data = data[0]
+    if 'parameters' in data:
+        data['steps'] = [{'parameters': data['parameters']}]
+        del data['parameters']
+    elif 'steps' not in data:
+        data['steps'] = []
+    data['name'] = script
+    data['dir'] = None
+
+    obj = _postprocess_script(data)
+    if 'name' in obj:
+        script = obj['name']
+    _script_cache[script] = obj
+    return obj
+
+
+def load_script(script):
+    _build_script_cache()
+    if script not in _script_cache:
+        common_debug("cache: %s" % (_script_cache.keys()))
+        raise ValueError("Script not found: %s" % (script))
+    s = _script_cache[script]
+    if isinstance(s, basestring):
+        try:
+            return _load_script_file(script, s)
+        except KeyError as err:
+            raise ValueError("Error when loading script %s: Expected key %s not found" % (script, err))
+        except Exception as err:
+            raise ValueError("Error when loading script %s: %s" % (script, err))
+    return s
+
+
+def _script_dirs():
+    "list of directories that may contain cluster scripts"
+    ret = [d for d in options.scriptdir.split(';') if d and os.path.isdir(d)]
+    return ret + [os.path.join(userdir.CONFIG_HOME, 'scripts'),
+                  os.path.join(config.path.sharedir, 'scripts'),
+                  config.path.hawk_wizards]
+
+
 def _check_control_persist():
     '''
     Checks if ControlPersist is available. If so,
@@ -58,7 +1057,7 @@ def _check_control_persist():
     '''
     cmd = 'ssh -o ControlPersist'.split()
     if options.regression_tests:
-        print ".EXT", cmd
+        print(".EXT", cmd)
     cmd = subprocess.Popen(cmd,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE)
@@ -66,18 +1065,33 @@ def _check_control_persist():
     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 _parallax_call(printer, hosts, cmd, opts):
+    "parallax.call with debug logging"
+    printer.debug("parallax.call(%s, %s)" % (repr(hosts), cmd))
+    return parallax.call(hosts, cmd, opts)
+
+
+def _resolve_script(name):
+    for p in list_scripts():
+        if p.endswith('main.yml') and os.path.dirname(p).endswith('/' + name):
+            return p
+        elif p.endswith('.yml') and os.path.splitext(os.path.basename(p))[0] == name:
+            return p
+        elif p.endswith('.xml') and os.path.splitext(os.path.basename(p))[0] == name:
+            return p
+    return None
+
 
+def _parallax_copy(printer, hosts, src, dst, opts):
+    "parallax.copy with debug logging"
+    printer.debug("parallax.copy(%s, %s, %s)" % (repr(hosts), src, dst))
+    return parallax.copy(hosts, src, dst, 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 _tempname(prefix):
+    return '%s-%s%s' % (prefix,
+                        hex(int(time.time()))[2:],
+                        hex(random.randint(0, 2**48))[2:])
 
 
 def _generate_workdir_name():
@@ -86,106 +1100,81 @@ def _generate_workdir_name():
     running the script
     '''
     # TODO: make use of /tmp configurable
-    basefile = 'crm-tmp-%s-%s' % (time.time(), random.randint(0, 2**48))
+    basefile = _tempname('crm-tmp')
     basetmp = os.path.join(utils.get_tempdir(), basefile)
+    if os.path.isdir(basetmp):
+        raise ValueError("Invalid temporary workdir %s" % (basetmp))
     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 _print_debug(printer, local_node, hosts, workdir, opts):
+    "Print debug output (if any)"
+    dbglog = os.path.join(workdir, 'crm_script.debug')
+    for host, result in _parallax_call(printer, hosts,
+                                       "if [ -f '%s' ]; then cat '%s'; fi" % (dbglog, dbglog),
+                                       opts).iteritems():
+        if isinstance(result, parallax.Error):
+            printer.error(host, result)
+        else:
+            printer.output(host, *result)
+    if os.path.isfile(dbglog):
+        f = open(dbglog).read()
+        printer.output(local_node, 0, f, '')
 
-    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 _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 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 _run_cleanup(printer, has_remote_actions, local_node, hosts, workdir, opts):
+    "Clean up after the cluster script"
+    if has_remote_actions and hosts and workdir:
+        cleanscript = os.path.join(workdir, 'crm_clean.py')
+        for host, result in _parallax_call(printer, hosts,
+                                           "%s %s" % (cleanscript,
+                                                      workdir),
+                                           opts).iteritems():
+            if isinstance(result, parallax.Error):
+                printer.error(host, "Clean: %s" % (result))
+            else:
+                printer.output(host, *result)
+    _cleanup_local(workdir)
 
 
-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 _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
 
 
+# TODO: remove common params?
+# Pass them in a separate list of options?
+# Right now these names are basically reserved..
 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'),
+            ('dry_run', 'no', 'If set, simulate execution only'),
+            ('action', None, 'If set, only execute a single action (index, as returned by verify)'),
             ('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',
@@ -194,93 +1183,13 @@ def common_params():
             ('timeout', '600', 'Execution timeout in seconds')]
 
 
-def common_param_default(name):
+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)
@@ -298,75 +1207,380 @@ def _filter_nodes(nodes, user, port):
     return nodes
 
 
-def _parse_parameters(name, args, main):
+def _scoped_param(context, name):
+    if context:
+        return ':'.join(context) + ':' + name
+    return name
+
+
+def _find_by_name(params, name):
+    try:
+        return next(x for x in params if x.get('name') == name)
+    except StopIteration:
+        return None
+
+
+_IDENT_RE = re.compile(r'^([a-z0-9_#$-][^\s=]*)$', re.IGNORECASE)
+
+
+def is_valid_ipv4_address(address):
+    try:
+        socket.inet_pton(socket.AF_INET, address)
+    except AttributeError:
+        try:
+            socket.inet_aton(address)
+        except socket.error:
+            return False
+        return address.count('.') == 3
+    except socket.error:  # not a valid address
+        return False
+
+    return True
+
+
+def is_valid_ipv6_address(address):
+    try:
+        socket.inet_pton(socket.AF_INET6, address)
+    except socket.error:  # not a valid address
+        return False
+    return True
+
+# Types:
+# OCF types
+#
+# string
+# integer
+# boolean
+#
+# Propose to add
+# resource ==> a valid resource identifier
+# ip_address ==> a valid ipv4 or ipv6 address
+# ip_network ==> a valid ipv4 or ipv6 network (or address without /XX)
+# port ==> integer between 0 and 65535
+# email ==> a valid email address
+
+# node ==> name of a node in the cluster
+# select <value>, <value>, <value>, ... ==> any of the values in the list.
+# range <n> <m> ==> integer in range
+# rx <rx> ==> anything matching the regular expression.
+
+
+def _valid_integer(value):
+    try:
+        return True, int(value, base=0)
+    except ValueError:
+        return False, value
+
+
+def _valid_ip(value):
+    return is_valid_ipv4_address(value) or is_valid_ipv6_address(value)
+
+
+def _verify_type(param, value, errors):
+    if value is None:
+        value = ''
+    type = param.get('type')
+    if not type:
+        return value
+    elif type == 'integer':
+        ok, _ = _valid_integer(value)
+        if not ok:
+            errors.append("%s=%s is not an integer" % (param.get('name'), value))
+    elif type == 'string':
+        return value
+    elif type == 'boolean':
+        return "true" if _make_boolean(value) else "false"
+    elif type == 'resource':
+        try:
+            if not _IDENT_RE.match(value):
+                errors.append("%s=%s invalid resource identifier" % (param.get('name'), value))
+        except TypeError as e:
+            errors.append("%s=%s %s" % (param.get('name'), value, str(e)))
+    elif type == 'enum':
+        if 'values' not in param:
+            errors.append("%s=%s enum without list of values" % (param.get('name'), value))
+        else:
+            opts = param['values']
+            if isinstance(opts, basestring):
+                opts = opts.replace(',', ' ').split(' ')
+            for v in opts:
+                if value.lower() == v.lower():
+                    return v
+            else:
+                errors.append("%s=%s does not match '%s'" % (param.get('name'), value, "|".join(opts)))
+    elif type == 'ip_address':
+        if not _valid_ip(value):
+            errors.append("%s=%s is not an IP address" % (param.get('name'), value))
+    elif type == 'ip_network':
+        sp = value.rsplit('/', 1)
+        if len(sp) == 1 and not (is_valid_ipv4_address(value) or is_valid_ipv6_address(value)):
+            errors.append("%s=%s is not a valid IP network" % (param.get('name'), value))
+        elif len(sp) == 2 and (not _valid_ip(sp[0]) or not _valid_integer(sp[1])):
+            errors.append("%s=%s is not a valid IP network" % (param.get('name'), value))
+        else:
+            errors.append("%s=%s is not a valid IP network" % (param.get('name'), value))
+    elif type == 'port':
+        ok, ival = _valid_integer(value)
+        if not ok:
+            errors.append("%s=%s is not a valid port" % (param.get('name'), value))
+        if ival < 0 or ival > 65535:
+            errors.append("%s=%s is out of port range" % (param.get('name'), value))
+    elif type == 'email':
+        if not re.match(r'[^@]+@[^@]+', value):
+            errors.append("%s=%s is not a valid email address" % (param.get('name'), value))
+    else:
+        errors.append("%s=%s is unknown type %s" % (param.get('name'), value, type))
+    return value
+
+_NO_RESOLVE = object()
+
+
+def _resolve_direct(step, pname, pvalue, path, errors):
+    step_parameters = step.get('parameters', [])
+    step_steps = step.get('steps', [])
+    param = _find_by_name(step_parameters, pname)
+    if param is not None:
+        # resolved to a parameter... now verify the value type?
+        return _verify_type(param, pvalue, errors)
+    substep = _find_by_name(step_steps, pname)
+    if substep is not None:
+        # resolved to a step... recurse
+        return _resolve_params(substep, pvalue, path + [pname], errors)
+    return _NO_RESOLVE
+
+
+def _resolve_unnamed_step(step, pname, pvalue, path, errors):
+    step_steps = step.get('steps', [])
+    substep = _find_by_name(step_steps, '')
+    if substep is not None:
+        return _resolve_direct(substep, pname, pvalue, path, errors)
+    return _NO_RESOLVE
+
+
+def _resolve_single_step(step, pname, pvalue, path, errors):
+    step_steps = step.get('steps', [])
+    if len(step_steps) >= 1:
+        first_step = step_steps[0]
+        return _resolve_direct(first_step, pname, pvalue, path + [first_step.get('name')], errors)
+    return _NO_RESOLVE
+
+
+def _resolve_params(step, params, path, errors):
+    """
+    any parameter that doesn't resolve is an error
+    """
+    ret = {}
+
+    for pname, pvalue in params.iteritems():
+        result = _resolve_direct(step, pname, pvalue, path, errors)
+        if result is not _NO_RESOLVE:
+            ret[pname] = result
+            continue
+
+        result = _resolve_unnamed_step(step, pname, pvalue, path, errors)
+        if result is not _NO_RESOLVE:
+            ret[pname] = result
+            continue
+
+        result = _resolve_single_step(step, pname, pvalue, path, errors)
+        if result is not _NO_RESOLVE:
+            stepname = step['steps'][0].get('name', '')
+            if stepname not in ret:
+                ret[stepname] = {}
+            ret[stepname][pname] = result
+            ret[pname] = result
+            continue
+
+        errors.append("Unknown parameter %s" % (':'.join(path + [pname])))
+
+    return ret
+
+
+def _check_parameters(script, params):
     '''
-    Parse run parameters into a dict.
+    1. Fill in values where none are supplied and there's a value
+    in the step data
+    2. Check missing values
+    3. For each input parameter: look it up and adjust the path
     '''
-    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']
+    errors = []
+    # params = deepcopy(params)
+    # recursively resolve parameters: report
+    # error if a parameter can't be resolved
+    # TODO: move "common params" out of the params dict completely
+    # pass as flags to command line
+
+    def _split_commons(params):
+        ret, cdict = {}, dict([(c, d) for c, d, _ in common_params()])
+        for key, value in params.iteritems():
+            if key in cdict:
+                cdict[key] = value
+            else:
+                ret[key] = deepcopy(value)
+        return ret, cdict
 
-    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
+    params, commons = _split_commons(params)
+    params = _resolve_params(script, params, [], errors)
+
+    if errors:
+        raise ValueError('\n'.join(errors))
+
+    for key, value in commons.iteritems():
+        params[key] = value
+
+    def _fill_values(path, into, source, srcreq):
+        """
+        Copy values into into while checking for missing required parameters.
+        If into has content, all required parameters ARE required, even if the
+        whole step is not required (since we're supplying it). This is checked
+        by checking if the step is not required, but there are some parameters
+        set by the user anyway.
+        """
+        if 'required' in source:
+            srcreq = (source['required'] and srcreq) or (into and srcreq)
+
+        for param in source.get('parameters', []):
+            if param['name'] not in into:
+                if 'value' in param:
+                    into[param['name']] = param['value']
+                elif srcreq and param['required']:
+                    errors.append(_scoped_param(path, param['name']))
+
+        for step in source.get('steps', []):
+            required = step.get('required', True)
+            if not required and step['name'] not in into:
+                continue
+            if not required and step['name'] in into and into[step['name']]:
+                required = True
+            if 'name' not in step:
+                _fill_values(path, into, step, required and srcreq)
+            else:
+                if step['name'] not in into:
+                    into[step['name']] = {}
+                _fill_values(path + [step['name']], into[step['name']], step, required and srcreq)
+
+    _fill_values([], params, script, True)
+
+    if errors:
+        raise ValueError("Missing required parameter(s): %s" % (', '.join(errors)))
+
+    # if config.core.debug:
+    #    from pprint import pprint
+    #    print("Checked script parameters:")
+    #    pprint(params)
     return params
 
 
-def _extract_localnode(hosts):
+def _handles_values(ret, script, params, subactions):
     """
-    Remove loal node from hosts list, so
-    we can treat it separately
+    Generate a values structure that the handles
+    templates understands.
     """
-    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 _process(to, context, params):
+        """
+        to: level writing to
+        context: source step
+        params: values for step
+        """
+        for key, value in params.iteritems():
+            if not isinstance(value, dict):
+                to[key] = value
+
+        for step in context.get('steps', []):
+            name = step.get('name', '')
+            if name:
+                if step['required'] or name in params:
+                    obj = {}
+                    vobj = handles.value(obj, '')
+                    to[name] = vobj
+                    subaction = None
+                    if step.get('sub-script'):
+                        subaction = subactions.get(step['sub-script']['name'])
+                    if subaction and subaction[-1]['name'] == 'cib':
+                        vobj.value = Text.cib(script, subaction[-1]['value'])
+                    else:
+                        vobj.value = Text.cib(script, step.get('value', vobj.value))
+
+                    _process(obj, step, params.get(name, {}))
+            else:
+                _process(to, step, params)
+
+    _process(ret, script, params)
+
+
+def _has_remote_actions(actions):
+    """
+    True if any actions execute on remote nodes
+    """
+    for action in actions:
+        if action['name'] in ('collect', 'apply', 'install', 'service', 'copy'):
+            return True
+        if action.get('nodes') == 'all':
+            return True
+    return False
 
 
 def _set_controlpersist(opts):
-    #_has_controlpersist = _check_control_persist()
-    #if _has_controlpersist:
+    # _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,
+    # unfortunately, due to bad interaction between parallax and ssh,
     # ControlPersist is broken
     # See: http://code.google.com/p/parallel-ssh/issues/detail?id=67
-    # Fixed in openssh 6.3
+    # Supposedly fixed in openssh 6.3, but isn't: This may be an
+    # issue in parallel-ssh, not openssh
     pass
 
 
-def _create_script_workdir(scriptdir, workdir):
+def _flatten_parameters(steps):
+    pret = []
+    for step in steps:
+        stepname = step.get('name', '')
+        for param in step.get('parameters', []):
+            if stepname:
+                pret.append('%s:%s' % (stepname, param['name']))
+            else:
+                pret.append(param['name'])
+    return pret
+
+
+def param_completion_list(name):
+    """
+    Returns completions for the given script
+    """
+    try:
+        script = load_script(name)
+        params = _flatten_parameters(script.get('steps', []))
+        ps = [p['name'] + '=' for p in params]
+        return ps
+    except Exception:
+        return []
+
+
+def _create_script_workdir(script, 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")
+    scriptdir = script['dir']
     try:
-        shutil.copytree(scriptdir, workdir)
+        if scriptdir is not None:
+            if os.path.basename(scriptdir) == script['name']:
+                cmd = ["mkdir", "-p", os.path.dirname(workdir)]
+            else:
+                cmd = ["mkdir", "-p", workdir]
+            if options.regression_tests:
+                print ".EXT", cmd
+            if subprocess.call(cmd, shell=False) != 0:
+                raise ValueError("Failed to create temporary working directory")
+            # only copytree if script is a dir
+            if os.path.basename(scriptdir) == script['name']:
+                shutil.copytree(scriptdir, workdir)
+        else:
+            cmd = ["mkdir", "-p", workdir]
+            if options.regression_tests:
+                print ".EXT", cmd
+            if subprocess.call(cmd, shell=False) != 0:
+                raise ValueError("Failed to create temporary working directory")
     except (IOError, OSError), e:
         raise ValueError(e)
 
@@ -378,19 +1592,19 @@ def _copy_utils(dst):
     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)
+            shutil.copy(f, dst)
     except (IOError, OSError), e:
         raise ValueError(e)
 
 
-def _create_remote_workdirs(hosts, path, opts):
+def _create_remote_workdirs(printer, 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))
+    for host, result in _parallax_call(printer, hosts,
+                                       "mkdir -p %s" % (os.path.dirname(path)),
+                                       opts).iteritems():
+        if isinstance(result, parallax.Error):
+            printer.error(host, "Start: %s" % (result))
             ok = False
     if not ok:
         msg = "Failed to connect to one or more of these hosts via SSH: %s" % (
@@ -398,34 +1612,23 @@ def _create_remote_workdirs(hosts, path, opts):
         raise ValueError(msg)
 
 
-def _copy_to_remote_dirs(hosts, path, opts):
+def _copy_to_remote_dirs(printer, 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))
+    for host, result in _parallax_copy(printer, hosts,
+                                       path,
+                                       path, opts).iteritems():
+        if isinstance(result, parallax.Error):
+            printer.debug("_copy_to_remote_dirs failed: %s, %s, %s" % (hosts, path, opts))
+            printer.error(host, result)
             ok = False
     if not ok:
         raise ValueError("Failed when copying script data, aborting.")
+    return ok
 
 
-def _copy_to_all(workdir, hosts, local_node, src, dst, opts):
-    """
-    Copy src to dst both locally and remotely
-    """
+def _copy_local(printer, workdir, local_node, src, dst):
     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):
@@ -433,16 +1636,92 @@ def _copy_to_all(workdir, hosts, local_node, src, dst, opts):
                     shutil.copy(src, dst)
                 else:
                     shutil.copytree(src, dst)
-        except (IOError, OSError, shutil.Error), e:
-            err_buf.error("[%s]: %s" % (local_node, e))
+        except (IOError, OSError, shutil.Error) as e:
+            printer.error(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]
+def _copy_to_all(printer, workdir, hosts, local_node, src, dst, opts):
+    """
+    Copy src to dst both locally and remotely
+    """
+    ok = True
+    ret = _parallax_copy(printer, hosts, src, dst, opts)
+    for host, result in ret.iteritems():
+        if isinstance(result, parallax.Error):
+            printer.error(host, result)
+            ok = False
+        else:
+            rc, out, err = result
+            if rc != 0:
+                printer.error(host, err)
+                ok = False
+    return ok and _copy_local(printer, workdir, local_node, src, dst)
+
+
+def _clean_parameters(params):
+    ret = []
+    for param in params:
+        rp = {}
+        for elem in ('name', 'required', 'unique', 'advanced', 'type', 'example'):
+            if elem in param:
+                rp[elem] = param[elem]
+        if 'shortdesc' in param:
+            rp['shortdesc'] = _strip(param['shortdesc'])
+        if 'longdesc' in param:
+            rp['longdesc'] = format_desc(param['longdesc'])
+        if 'value' in param:
+            val = param['value']
+            if isinstance(val, Text):
+                val = val.text
+            rp['value'] = val
+        ret.append(rp)
+    return ret
+
+
+def clean_steps(steps):
+    ret = []
+    for step in steps:
+        rstep = {}
+        if 'name' in step:
+            rstep['name'] = step['name']
+        if 'shortdesc' in step:
+            rstep['shortdesc'] = _strip(step['shortdesc'])
+        if 'longdesc' in step:
+            rstep['longdesc'] = format_desc(step['longdesc'])
+        if 'required' in step:
+            rstep['required'] = step['required']
+        if 'parameters' in step:
+            rstep['parameters'] = _clean_parameters(step['parameters'])
+        if 'steps' in step:
+            rstep['steps'] = clean_steps(step['steps'])
+        ret.append(rstep)
+    return ret
+
+
+def clean_run_params(params):
+    for key, value in params.iteritems():
+        if isinstance(value, dict):
+            clean_run_params(value)
+        elif Text.isa(value):
+            params[key] = str(value)
+    return params
+
+
+def _chmodx(path):
+    "chmod +x <path>"
+    mode = os.stat(path).st_mode
+    mode |= (mode & 0o444) >> 2
+    os.chmod(path, mode)
+
+
+class RunActions(object):
+    def __init__(self, printer, script, params, actions, local_node, hosts, opts, workdir):
+        self.printer = printer
+        self.script = script
+        self.data = [clean_run_params(params)]
+        self.actions = actions
         self.local_node = local_node
         self.hosts = hosts
         self.opts = opts
@@ -451,293 +1730,447 @@ class RunStep(object):
         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.result = None
+        self.output = None
+        self.rc = False
+
+    def prepare(self, has_remote_actions):
+        if not self.dry_run:
+            _create_script_workdir(self.script, self.workdir)
+            json.dump(self.data, open(self.statefile, 'w'))
+            _copy_utils(self.workdir)
+            if has_remote_actions:
+                _create_remote_workdirs(self.printer, self.hosts, self.workdir, self.opts)
+                _copy_to_remote_dirs(self.printer, self.hosts, self.workdir, self.opts)
+            # make sure all path references are relative to the script directory
+            os.chdir(self.workdir)
+
+    def single_action(self, action_index, 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
+        try:
+            action_index = int(action_index) - 1
+        except ValueError:
+            raise ValueError("action parameter must be an index")
+        if action_index < 0 or action_index >= len(self.actions):
+            raise ValueError("action index out of range")
+
+        action = self.actions[action_index]
+        common_debug("Execute: %s" % (action))
+        # if this is not the first action, load action data
+        if action_index != 1:
+            if not os.path.isfile(statefile):
+                raise ValueError("No state for action: %s" % (action_index))
+            self.data = json.load(open(statefile))
+        if Actions._needs_sudo(action):
+            self._check_sudo_pass()
+        result = self._run_action(action)
+        json.dump(self.data, open(self.statefile, 'w'))
+        return result
+
+    def all_actions(self):
+        # TODO: run asynchronously on remote nodes
+        # run on remote nodes
+        # run on local nodes
+        # TODO: wait for remote results
+        for action in self.actions:
+            if Actions._needs_sudo(action):
+                self._check_sudo_pass()
+            if not self._run_action(action):
+                return False
+        return True
 
     def _update_state(self):
+        if self.dry_run:
+            return True
         json.dump(self.data, open(self.statefile, 'w'))
-        return _copy_to_all(self.workdir,
+        return _copy_to_all(self.printer,
+                            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
+    def run_command(self, nodes, command, is_json_output):
+        "called by Actions"
+        cmdline = 'cd "%s"; ./%s' % (self.workdir, command)
+        if not self._update_state():
+            raise ValueError("Failed when updating input, aborting.")
+        self.call(nodes, cmdline, is_json_output)
+
+    def copy_file(self, nodes, src, dst):
+        if not self._is_local(nodes):
+            ok = _copy_to_all(self.printer,
+                              self.workdir,
+                              self.hosts,
+                              self.local_node,
+                              src,
+                              dst,
+                              self.opts)
         else:
-            print fmt
-
-    def error(self, fmt, *args):
-        self.flush()
-        err_buf.error(fmt % args)
+            ok = _copy_local(self.printer,
+                             self.workdir,
+                             self.local_node,
+                             src,
+                             dst)
+        self.result = '' if ok else None
+        self.rc = ok
+
+    def record_json(self):
+        "called by Actions"
+        if self.result is not None:
+            if not self.result:
+                self.result = {}
+            self.data.append(self.result)
+            self.rc = True
+        else:
+            self.rc = False
+
+    def validate_json(self):
+        "called by Actions"
+        if self.dry_run:
+            self.rc = True
+            return
+
+        if self.result is not None:
+            if not self.result:
+                self.result = ''
+            self.data.append(self.result)
+            if isinstance(self.result, dict):
+                for k, v in self.result.iteritems():
+                    self.data[0][k] = v
+            self.rc = True
+        else:
+            self.rc = False
 
-    def debug(self, msg):
-        err_buf.debug(msg)
+    def report_result(self):
+        "called by Actions"
+        if self.result is not None:
+            self.output = self.result
+            self.rc = True
+        else:
+            self.rc = False
 
-    def run_step(self, name, action, call):
+    def _run_action(self, action):
         """
-        Execute a single step
+        Execute a single action
         """
-
-        self.start('%s...' % (name))
+        method = _actions[action['name']]
+        self.printer.start(action)
         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
+            self.output = None
+            self.result = None
+            self.rc = False
+            method(Actions(self, action))
+            self.printer.finish(action, self.rc, self.output)
+            return self.rc
         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
+            self.printer.flush()
+        return False
 
     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):
+    def _is_local(self, nodes):
+        islocal = False
+        if nodes == 'all':
+            pass
+        elif nodes is not None and nodes != []:
+            islocal = nodes == [self.local_node_name()]
+        else:
+            islocal = True
+        self.printer.debug("is_local (%s): %s" % (nodes, islocal))
+        return islocal
+
+    def call(self, nodes, cmdline, is_json_output=False):
+        if not self._is_local(nodes):
+            self.result = self._process_remote(cmdline, is_json_output)
+        else:
+            self.result = self._process_local(cmdline, is_json_output)
+        self.rc = self.result not in (False, None)
+
+    def execute_shell(self, nodes, cmdscript):
+        """
+        execute the shell script...
+        """
+        if self.dry_run:
+            self.printer.print_command(nodes, cmdscript)
+            self.result = ''
+            self.rc = True
+            return
+        elif config.core.debug:
+            self.printer.print_command(nodes, cmdscript)
+
+        tmpf = self.str2tmp(cmdscript)
+        _chmodx(tmpf)
+        if not self._is_local(nodes):
+            ok = _copy_to_remote_dirs(self.printer,
+                                      self.hosts,
+                                      tmpf,
+                                      self.opts)
+            if not ok:
+                self.result = False
+            else:
+                cmdline = 'cd "%s"; %s' % (self.workdir, tmpf)
+                self.result = self._process_remote(cmdline, False)
+        else:
+            cmdline = 'cd "%s"; %s' % (self.workdir, tmpf)
+            self.result = self._process_local(cmdline, False)
+        self.rc = self.result not in (None, False)
+
+    def str2tmp(self, s):
+        """
+        Create a temporary file in the temp workdir
+        Returns path to file
+        """
+        fn = os.path.join(self.workdir, _tempname('str2tmp'))
+        if self.dry_run:
+            self.printer.print_command(self.local_node_name(), 'temporary file <<END\n%s\nEND\n' % (s))
+            return fn
+        elif config.core.debug:
+            self.printer.print_command(self.local_node_name(), 'temporary file <<END\n%s\nEND\n' % (s))
+        try:
+            with open(fn, "w") as f:
+                f.write(s)
+                if not s.endswith('\n'):
+                    f.write("\n")
+        except IOError, msg:
+            self.printer.error(self.local_node_name(), "Write failed: %s" % (msg))
+            return
+        return fn
+
+    def _process_remote(self, cmdline, is_json_output):
         """
-        Handle a step that executes on all nodes
+        Handle an action that executes on all nodes
         """
         ok = True
-        step_result = {}
+        action_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)
+        if self.dry_run:
+            self.printer.print_command(self.hosts, cmdline)
+            return {}
+        elif config.core.debug:
+            self.printer.print_command(self.hosts, cmdline)
+
+        for host, result in _parallax_call(self.printer,
+                                           self.hosts,
+                                           cmdline,
+                                           self.opts).iteritems():
+            if isinstance(result, parallax.Error):
+                self.printer.error(host, "Remote error: %s" % (result))
                 ok = False
             else:
                 rc, out, err = result
                 if rc != 0:
-                    self.error("[%s]: %s%s", host, out, err)
+                    self.printer.error(host, "Remote error (rc=%s) %s%s" % (rc, out, err))
                     ok = False
+                elif is_json_output:
+                    action_result[host] = json.loads(out)
                 else:
-                    step_result[host] = json.loads(out)
+                    action_result[host] = out
         if self.local_node:
-            ret = self._process_local(cmdline)
+            ret = self._process_local(cmdline, False)
             if ret is None:
                 ok = False
+            elif is_json_output:
+                action_result[self.local_node_name()] = json.loads(ret)
             else:
-                step_result[self.local_node[0]] = json.loads(ret)
+                action_result[self.local_node_name()] = ret
         if ok:
-            self.debug("%s" % repr(step_result))
-            return step_result
+            self.printer.debug("Result: %s" % repr(action_result))
+            return action_result
         return None
 
-    def _process_local(self, cmdline):
+    def _process_local(self, cmdline, is_json_output):
         """
-        Handle a step that executes locally
+        Handle an action that executes locally
         """
         if self.sudo_pass:
             input_s = u'sudo: %s\n' % (self.sudo_pass)
         else:
             input_s = None
+        if self.dry_run:
+            self.printer.print_command(self.local_node_name(), cmdline)
+            return {}
+        elif config.core.debug:
+            self.printer.print_command(self.local_node_name(), cmdline)
         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)
+            self.printer.error(self.local_node_name(), "Error (%d): %s" % (rc, err))
             return None
-        self.debug("%s" % repr(out))
+        self.printer.debug("Result(local): %s" % repr(out))
+        if is_json_output:
+            out = json.loads(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 local_node_name(self):
+        if self.local_node:
+            return self.local_node[0]
+        return "localhost"
 
 
-def run(name, args):
+def run(script, params, printer):
     '''
     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
+    name: a cluster script is a folder <name> containing a main.yml or main.xml file
+    params: a tree of parameters
+    printer: Object that receives and formats output
     '''
-    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)
+    # pull out the actions to perform based on the actual
+    # parameter values (so discard actions conditional on
+    # conditions that are false)
+    params = _check_parameters(script, params)
+    user = params['user']
+    port = params['port']
+    _filter_dict(params, 'nodes', _filter_nodes, user, port)
+    _filter_dict(params, 'dry_run', _make_boolean)
+    _filter_dict(params, 'sudo', _make_boolean)
+    _filter_dict(params, 'statefile', lambda x: (x and os.path.abspath(x)) or x)
+    if config.core.debug:
+        params['debug'] = True
+    actions = _process_actions(script, params)
+    name = script['name']
     hosts = params['nodes']
-    err_buf.info(main['name'])
-    err_buf.info("Nodes: " + ', '.join([x[0] for x in hosts]))
+    printer.print_header(script, params, hosts)
     local_node, hosts = _extract_localnode(hosts)
     opts = _make_options(params)
     _set_controlpersist(opts)
 
+    dry_run = params.get('dry_run', False)
+
+    has_remote_actions = _has_remote_actions(actions)
+
     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']
+        runner = RunActions(printer, script, params, actions, local_node, hosts, opts, workdir)
+        runner.prepare(has_remote_actions)
+        action = params['action']
         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)
+        if action or statefile:
+            if not action or not statefile:
+                raise ValueError("Must set both action and statefile")
+            return runner.single_action(action, statefile)
         else:
-            return stepper.all_steps()
+            return runner.all_actions()
 
     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)
+        if not dry_run:
+            if not config.core.debug:
+                _run_cleanup(printer, has_remote_actions, local_node, hosts, workdir, opts)
+            else:
+                _print_debug(printer, local_node, hosts, workdir, opts)
+
+
+def _remove_empty_lines(txt):
+    return '\n'.join(line for line in txt.split('\n') if line.strip())
+
+
+def _process_actions(script, params):
+    """
+    Given parameter values, we can process
+    all the handles data and generate all the
+    actions to perform, validate and check conditions.
+    """
+
+    subactions = {}
+    values = {}
+    script['__values__'] = values
+
+    for step in script['steps']:
+        _handles_values(values, script, params, subactions)
+        if not step.get('required', True) and not params.get(step['name']):
+            continue
+        obj = step.get('sub-script')
+        if obj:
+            try:
+                subparams = params.get(step['name'], {})
+                subactions[step['name']] = _process_actions(obj, subparams)
+            except ValueError as err:
+                raise ValueError("Error in included script %s: %s" % (step['name'], err))
+
+    _handles_values(values, script, params, subactions)
+    actions = deepcopy(script['actions'])
+
+    ret = []
+    for action in actions:
+        name = _find_action(action)
+        if name is None:
+            raise ValueError("Unknown action: %s" % (action.keys()))
+        action['name'] = name
+        toadd = []
+        if name == 'include':
+            if action['include'] in subactions:
+                toadd.extend(subactions[action['include']])
+        else:
+            Actions._parse(script, action)
+            if 'when' in action:
+                when = str(action['when']).strip()
+                if when not in (False, None, '', 'false'):
+                    toadd.append(action)
+            else:
+                toadd.append(action)
+        if ret:
+            for add in toadd:
+                if Actions._mergeable(add) and ret[-1]['name'] == add['name']:
+                    if not Actions._merge(ret[-1], add):
+                        ret.append(add)
+                else:
+                    ret.append(add)
         else:
-            _print_debug(local_node, hosts, workdir, opts)
+            ret.extend(toadd)
+    return ret
+
+
+def verify(script, params, external_check=True):
+    """
+    Verify the given parameter values, reporting
+    errors where such are detected.
+
+    Return a list of actions to perform.
+    """
+    params = _check_parameters(script, params)
+    actions = _process_actions(script, params)
+
+    if external_check and all(action['name'] == 'cib' for action in actions) and utils.is_program('crm'):
+        errors = set([])
+        cmd = ["cib new"]
+        for action in actions:
+            cmd.append(_join_script_lines(action['value']))
+        cmd.extend(["verify", "commit", "\n"])
+        try:
+            common_debug("Try executing %s" % ("\n".join(cmd)))
+            rc, out = utils.filter_string(['crm', '-f', '-', 'configure'], "\n".join(cmd), stderr_on='stdout', shell=False)
+            errm = re.compile(r"^ERROR: \d+: (.*)$")
+            outp = []
+            for l in (out or "").splitlines():
+                m = errm.match(l)
+                if m:
+                    errors.add(m.group(1))
+                else:
+                    outp.append(l)
+            if rc != 0 and len(errors) == 0:
+                errors.add("Failed to verify (rc=%s): %s" % (rc, "\n".join(outp)))
+        except OSError as e:
+            errors.add(str(e))
+        if len(errors):
+            raise ValueError("\n".join(errors))
+
+    return actions
+
+
+def _make_boolean(v):
+    if isinstance(v, basestring):
+        return utils.get_boolean(v)
+    return v not in (0, False, None)
diff --git a/modules/template.py b/modules/template.py
index 0716efe..d6f8206 100644
--- a/modules/template.py
+++ b/modules/template.py
@@ -1,25 +1,11 @@
 # 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
-#
+# See COPYING for license information.
 
 import os
 import re
-import config
-import userdir
-from msg import common_err, common_info, common_warn
+from . import config
+from . import userdir
+from .msg import common_err, common_info, common_warn
 
 
 def get_var(l, key):
@@ -125,11 +111,10 @@ class LoadTemplate(object):
 
     def load_template(self, tmpl):
         try:
-            f = open(os.path.join(config.path.sharedir, 'templates', tmpl))
+            l = open(os.path.join(config.path.sharedir, 'templates', tmpl)).read().split('\n')
         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)
diff --git a/modules/term.py b/modules/term.py
index 95b58e6..2627277 100644
--- a/modules/term.py
+++ b/modules/term.py
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 import sys
 import re
@@ -130,7 +116,8 @@ def _init():
         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():
+    from . import config
+    if not _term_stream.isatty() and 'color-always' not in config.color.style:
         return
     # Check the terminal type.  If we fail, then assume that the
     # terminal has no capabilities.
@@ -164,8 +151,6 @@ def _init():
         for i, color in zip(range(len(_ANSICOLORS)), _ANSICOLORS):
             setattr(colors, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
 
-_init()
-
 
 def render(template):
     """
diff --git a/modules/tmpfiles.py b/modules/tmpfiles.py
index 9f94312..bf5dbe7 100644
--- a/modules/tmpfiles.py
+++ b/modules/tmpfiles.py
@@ -1,20 +1,6 @@
 # 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
-#
+# See COPYING for license information.
 '''
 Files added to tmpfiles are removed at program exit.
 '''
@@ -24,7 +10,7 @@ import shutil
 import atexit
 from tempfile import mkstemp, mkdtemp
 
-import utils
+from . import utils
 
 _FILES = []
 _DIRS = []
diff --git a/modules/ui_assist.py b/modules/ui_assist.py
index 0145ad6..b3137d1 100644
--- a/modules/ui_assist.py
+++ b/modules/ui_assist.py
@@ -1,25 +1,11 @@
 # 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
+# See COPYING for license information.
+
+from . import utils
+from . import command
+from . import completers as compl
+from . import xmlutil
+from .cibconfig import cib_factory
 
 
 def rmattrs(e, *attrs):
@@ -53,7 +39,7 @@ class Assist(command.UI):
         '''
         if len(primitives) < 1:
             context.fatal_error("Expected at least one primitive argument")
-        objs = [cib_factory.find_object(p) for p in primitives]
+        objs = [cib_factory.find_resource(p) for p in primitives]
         for prim, obj in zip(primitives, objs):
             if obj is None:
                 context.fatal_error("Primitive %s not found" % (prim))
@@ -114,7 +100,7 @@ class Assist(command.UI):
             context.fatal_error("Need at least two arguments")
 
         for node in nodes:
-            obj = cib_factory.find_object(node)
+            obj = cib_factory.find_resource(node)
             if not obj:
                 context.fatal_error("Object not found: %s" % (node))
             if not xmlutil.is_primitive(obj.node):
diff --git a/modules/ui_cib.py b/modules/ui_cib.py
index 165f2e2..c2bc209 100644
--- a/modules/ui_cib.py
+++ b/modules/ui_cib.py
@@ -1,36 +1,22 @@
 # 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
-#
+# See COPYING for license information.
 
 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
+from . import command
+from . import xmlutil
+from . import utils
+from . import ui_cibstatus
+from . import constants
+from . import config
+from . import options
+from .msg import no_prog_err
+from .cibstatus import cib_status
+from .cibconfig import cib_factory
+from . import tmpfiles
+
+from . import completers as compl
 
 _NEWARGS = ('force', '--force', 'withstatus', 'empty')
 
@@ -40,8 +26,8 @@ class CibShadow(command.UI):
     CIB shadow management class
     '''
     name = "cib"
-    extcmd = ">/dev/null </dev/null crm_shadow"
-    extcmd_stdout = "</dev/null crm_shadow"
+    extcmd = ">/dev/null </dev/null crm_shadow -b"
+    extcmd_stdout = "</dev/null crm_shadow -b"
 
     def requires(self):
         if not utils.is_program('crm_shadow'):
@@ -60,7 +46,7 @@ class CibShadow(command.UI):
         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))
+            context.fatal_error("Unexpected argument(s): " + ' '.join(argl))
 
         name = None
         if argl:
diff --git a/modules/ui_cibstatus.py b/modules/ui_cibstatus.py
index 122e1ca..2fd40fb 100644
--- a/modules/ui_cibstatus.py
+++ b/modules/ui_cibstatus.py
@@ -1,27 +1,13 @@
 # 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
-#
+# See COPYING for license information.
 
-import command
-import completers as compl
-import utils
-import ui_utils
-import constants
-from cibstatus import cib_status
+from . import command
+from . import completers as compl
+from . import utils
+from . import ui_utils
+from . import constants
+from .cibstatus import cib_status
 
 
 _status_node_list = compl.call(cib_status.status_node_list)
diff --git a/modules/ui_cluster.py b/modules/ui_cluster.py
index 4198a92..704a03d 100644
--- a/modules/ui_cluster.py
+++ b/modules/ui_cluster.py
@@ -1,26 +1,12 @@
 # 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
+# See COPYING for license information.
+
+from . import command
+from . import utils
+from .msg import err_buf
+from . import scripts
+from . import completers as compl
 
 
 def _remove_completer(args):
@@ -31,6 +17,16 @@ def _remove_completer(args):
     return scripts.param_completion_list('remove') + n
 
 
+def script_printer():
+    from .ui_script import ConsolePrinter
+    return ConsolePrinter()
+
+
+def script_args(args):
+    from .ui_script import _nvpairs2parameters
+    return _nvpairs2parameters(args)
+
+
 class Cluster(command.UI):
     '''
     Whole cluster management.
@@ -101,15 +97,19 @@ class Cluster(command.UI):
             return args + ['%s=%s' % (name, ','.join(vals))]
         return args
 
-    @command.completers_repeating(compl.choice(scripts.param_completion_list('init')))
+    @command.completers_repeating(compl.call(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'))
+        args = self._args_implicit(context, args, 'nodes')
+        script = scripts.load_script('init')
+        if script is None:
+            raise ValueError("init script failed to load")
+        return scripts.run(script, script_args(args), script_printer())
 
-    @command.completers_repeating(compl.choice(scripts.param_completion_list('add')))
+    @command.completers_repeating(compl.call(scripts.param_completion_list, 'add'))
     @command.skill_level('administrator')
     def do_add(self, context, *args):
         '''
@@ -129,7 +129,10 @@ class Cluster(command.UI):
             nodes = utils.list_cluster_nodes()
         nodes += node
         params += ['nodes=%s' % (','.join(nodes))]
-        return scripts.run('add', params)
+        script = scripts.load_script('add')
+        if script is None:
+            raise ValueError("add script failed to load")
+        return scripts.run(script, script_args(params), script_printer())
 
     @command.completers_repeating(_remove_completer)
     @command.skill_level('administrator')
@@ -138,15 +141,21 @@ class Cluster(command.UI):
         Remove the given node(s) from the cluster.
         '''
         params = self._args_implicit(context, args, 'node')
-        return scripts.run('remove', params)
+        script = scripts.load_script('remove')
+        if script is None:
+            raise ValueError("remove script failed to load")
+        return scripts.run(script, script_args(params), script_printer())
 
-    @command.completers_repeating(compl.choice(scripts.param_completion_list('health')))
+    @command.completers_repeating(compl.call(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)
+        script = scripts.load_script('health')
+        if script is None:
+            raise ValueError("health script failed to load")
+        return scripts.run(script, script_args(params), script_printer())
 
     def _node_in_cluster(self, node):
         return node in utils.list_cluster_nodes()
@@ -157,7 +166,7 @@ class Cluster(command.UI):
         '''
         stack = utils.cluster_stack()
         if not stack:
-            err_buf.error("Cluster stack not detected!")
+            err_buf.error("No supported cluster stack found (tried heartbeat|openais|corosync)")
         if utils.cluster_stack() == 'corosync':
             print "Services:"
             for svc in ["corosync", "pacemaker"]:
@@ -194,21 +203,48 @@ class Cluster(command.UI):
         Execute the given command on all nodes, report outcome
         '''
         try:
-            from psshlib import api as pssh
-            _has_pssh = True
+            import parallax
+            _has_parallax = True
         except ImportError:
-            _has_pssh = False
+            _has_parallax = False
 
-        if not _has_pssh:
-            context.fatal_error("PSSH not found")
+        if not _has_parallax:
+            context.fatal_error("python package parallax is needed for this command")
 
         hosts = utils.list_cluster_nodes()
-        opts = pssh.Options()
-        for host, result in pssh.call(hosts, cmd, opts).iteritems():
-            if isinstance(result, pssh.Error):
+        opts = parallax.Options()
+        for host, result in parallax.call(hosts, cmd, opts).iteritems():
+            if isinstance(result, parallax.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]))
+
+    def do_copy(self, context, local_file, *nodes):
+        '''
+        usage: copy <filename> [nodes ...]
+        Copy file to other cluster nodes.
+        If given no nodes as arguments, copy to all other cluster nodes.
+        '''
+        return utils.cluster_copy_file(local_file, nodes)
+
+    def do_diff(self, context, filename, *nodes):
+        "usage: diff <filename> [--checksum] [nodes...]. Diff file across cluster."
+        this_node = utils.this_node()
+        checksum = False
+        if len(nodes) and nodes[0] == '--checksum':
+            nodes = nodes[1:]
+            checksum = True
+        if not nodes:
+            nodes = utils.list_cluster_nodes()
+        if checksum:
+            utils.remote_checksum(filename, nodes, this_node)
+        elif len(nodes) == 1:
+            utils.remote_diff_this(filename, nodes, this_node)
+        elif this_node in nodes:
+            nodes.remove(this_node)
+            utils.remote_diff_this(filename, nodes, this_node)
+        elif len(nodes):
+            utils.remote_diff(filename, nodes)
diff --git a/modules/ui_configure.py b/modules/ui_configure.py
index e3e0ec3..cf98702 100644
--- a/modules/ui_configure.py
+++ b/modules/ui_configure.py
@@ -1,46 +1,32 @@
 # 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
-#
+# See COPYING for license information.
 
 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
+from . import command
+from . import completers as compl
+from . import config
+from . import utils
+from . import constants
+from . import userdir
+from . import xmlutil
+from . import ra
+from .cibconfig import mkset_obj, cib_factory
+from . import clidisplay
+from . import term
+from . import options
+from .msg import common_err, common_info, common_warn
+from .msg import err_buf, syntax_err
+from . import rsctest
+from . import schema
+from . import ui_cib
+from . import ui_cibstatus
+from . import ui_ra
+from . import ui_template
+from . import ui_history
+from . import ui_utils
+from . import ui_assist
+from .crm_gv import gv_types
 
 
 def _type_completions():
@@ -49,12 +35,17 @@ def _type_completions():
     return ['type:%s' % (t) for t in typelist]
 
 
+def _tag_completions():
+    "completer for tag: use in show"
+    return ['tag:%s' % (t) for t in cib_factory.tag_list()]
+
 # 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))
+                           compl.call(_type_completions),
+                           compl.call(_tag_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)
@@ -114,7 +105,7 @@ def get_prim_token(words, n):
 
 def ra_agent_for_template(tmpl):
     '''@template -> ra.agent'''
-    obj = cib_factory.find_object(tmpl[1:])
+    obj = cib_factory.find_resource(tmpl[1:])
     if obj is None:
         return None
     return ra.get_ra(obj.node)
@@ -277,6 +268,12 @@ class CibConfig(command.UI):
     def do_showobjects(self, context):
         cib_factory.showobjects()
 
+    @command.name('_keywords')
+    @command.skill_level('administrator')
+    def do_keywords(self, context):
+        for k, v in sorted(constants.keywords.iteritems(), key=lambda v: v[0].lower()):
+            print("%-16s %s" % (k, v))
+
     @command.level(ui_ra.RA)
     def do_ra(self):
         pass
@@ -308,6 +305,38 @@ class CibConfig(command.UI):
         set_obj = mkset_obj(*args)
         return set_obj.show()
 
+    @command.name("show_property")
+    @command.alias("show-property")
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.call(ra.get_properties_list))
+    def do_show_property(self, context, *args):
+        "usage: show-property [-t|--true [<name>...]"
+        properties = [a for a in args if a not in ('-t', '--true')]
+        truth = any(a for a in args if a in ('-t', '--true'))
+
+        if not properties:
+            utils.multicolumn(ra.get_properties_list())
+            return
+
+        def print_value(v):
+            if truth:
+                print utils.canonical_boolean(v)
+            else:
+                print v
+        for p in properties:
+            v = cib_factory.get_property(p)
+            if v is None:
+                try:
+                    v = ra.get_properties_meta().param_default(p)
+                except:
+                    pass
+            if v is not None:
+                print_value(v)
+            elif truth:
+                print "false"
+            else:
+                context.fatal_error("%s: Property not set" % (p))
+
     @command.skill_level('administrator')
     @command.completers_repeating(compl.null, _id_xml_list, _id_list)
     def do_filter(self, context, filterprog, *args):
@@ -316,6 +345,29 @@ class CibConfig(command.UI):
         return set_obj.filter(filterprog)
 
     @command.skill_level('administrator')
+    @command.completers(_id_list)
+    def do_set(self, context, path, value):
+        "usage: set <path> <value>"
+        def split_path():
+            for oid in cib_factory.id_list():
+                if path.startswith(oid + "."):
+                    return oid, path[len(oid)+1:]
+            context.fatal_error("Invalid path: " + path)
+        obj_id, obj_attr = split_path()
+        rsc = cib_factory.find_object(obj_id)
+        if not rsc:
+            context.fatal_error("Resource %s not found" % (obj_id))
+        nvpairs = rsc.node.xpath(".//nvpair[@name='%s']" % (obj_attr))
+        if not nvpairs:
+            context.fatal_error("Attribute not found: %s" % (path))
+        if len(nvpairs) != 1:
+            context.fatal_error("Expected 1 attribute named %s, found %s" %
+                                (obj_attr, len(nvpairs)))
+        rsc.set_updated()
+        nvpairs[0].set("value", value)
+        return True
+
+    @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):
@@ -386,19 +438,56 @@ class CibConfig(command.UI):
         set_obj_all = mkset_obj("xml")
         return self._verify(set_obj_all, set_obj_all)
 
+    @command.name('validate-all')
+    @command.alias('validate_all')
     @command.skill_level('administrator')
+    @command.completers_repeating(_id_list)
+    def do_validate_all(self, context, rsc):
+        "usage: validate-all <rsc>"
+        from . import ra
+        from . import cibconfig
+        from . import cliformat
+        from . import msg as msglog
+        obj = cib_factory.find_object(rsc)
+        if not obj:
+            context.error("Not found: %s" % (rsc))
+        if obj.obj_type != "primitive":
+            context.error("Not a primitive: %s" % (rsc))
+        rnode = cibconfig.reduce_primitive(obj.node)
+        if rnode is None:
+            context.error("No resource template %s for %s" % (self.node.get("template"), rsc))
+        params = []
+        for attrs in rnode.iterchildren("instance_attributes"):
+            params.extend(cliformat.nvpairs2list(attrs))
+        if not all(nvp.get('name') is not None and nvp.get('value') is not None for nvp in params):
+            context.error("Primitive too complex: %s" % (rsc))
+        params = dict([(nvp.get('name'), nvp.get('value')) for nvp in params])
+        agentname = xmlutil.mk_rsc_type(rnode)
+        if not ra.can_validate_agent(agentname):
+            context.error("%s: Cannot run validate-all for agent: %s" % (rsc, agentname))
+        rc, out = ra.validate_agent(agentname, params)
+        for msg in out.splitlines():
+            if msg.startswith("ERROR: "):
+                msglog.err_buf.error(msg[7:])
+            elif msg.startswith("WARNING: "):
+                msglog.err_buf.warning(msg[9:])
+            elif msg.startswith("INFO: "):
+                msglog.err_buf.info(msg[6:])
+            elif msg.startswith("DEBUG: "):
+                msglog.err_buf.debug(msg[7:])
+            else:
+                msglog.err_buf.writemsg(msg)
+        return rc == 0
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(_id_show_list)
     def do_save(self, context, *args):
-        "usage: save [xml] <filename>"
+        "usage: save [xml] [<id>...] <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()
+        filename = args[-1]
+        setargs = args[:-1]
+        set_obj = mkset_obj(*setargs)
         return set_obj.save_to_file(filename)
 
     @command.skill_level('administrator')
@@ -456,7 +545,7 @@ class CibConfig(command.UI):
     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
+        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)
@@ -466,14 +555,15 @@ class CibConfig(command.UI):
 
     @command.skill_level('administrator')
     @command.completers_repeating(_id_list)
+    @command.alias('rm')
     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]
+        arg_force = any((x in ('-f', '--force')) for x in argl)
+        argl = [x for x in argl if (x not in ('-f', '--force'))]
         if arg_force or config.core.force:
             self._stop_if_running(argl)
+            utils.wait4dc(what="Stopping %s" % (", ".join(argl)))
         return cib_factory.delete(*argl)
 
     @command.name('default-timeouts')
@@ -519,46 +609,51 @@ class CibConfig(command.UI):
         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":
+    def _commit(self, force=False, replace=False):
+        if 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
+        replace = replace or not utils.cibadmin_can_patch()
         rc1 = True
-        if not (force or utils.cibadmin_can_patch()):
+        if replace and not force:
             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()
+            return cib_factory.commit(replace=replace)
         if force or config.core.force:
             common_info("commit forced")
-            return cib_factory.commit(force=True)
+            return cib_factory.commit(force=True, replace=replace)
         if utils.ask("Do you still want to commit?"):
-            return cib_factory.commit(force=True)
+            return cib_factory.commit(force=True, replace=replace)
         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.completers(compl.choice(['force', 'replace']), compl.choice(['force', 'replace']))
+    def do_commit(self, context, arg0=None, arg1=None):
+        "usage: commit [force] [replace]"
+        force = "force" in [arg0, arg1]
+        replace = "replace" in [arg0, arg1]
+        if arg0 is not None and arg0 not in ("force", "replace"):
+            syntax_err(('configure.commit', arg0))
+            return False
+        if arg1 is not None and arg1 not in ("force", "replace"):
+            syntax_err(('configure.commit', arg1))
+            return False
+        return self._commit(force=force, replace=replace)
 
     @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()
+            context.fatal_error("Expected 'force' or no argument")
+        return cib_factory.upgrade_validate_with(force=config.core.force or force)
 
     @command.skill_level('administrator')
     def do_schema(self, context, schema_st=None):
@@ -586,6 +681,7 @@ class CibConfig(command.UI):
 
     @command.skill_level('administrator')
     @command.completers_repeating(compl.null, ra_classes_or_tmpl, primitive_complete_complex)
+    @command.alias('resource')
     def do_primitive(self, context, *args):
         """usage: primitive <rsc> {[<class>:[<provider>:]]<type>|@<template>}
         [params <param>=<value> [<param>=<value>...]]
diff --git a/modules/ui_context.py b/modules/ui_context.py
index c3ef623..143425d 100644
--- a/modules/ui_context.py
+++ b/modules/ui_context.py
@@ -1,28 +1,14 @@
 # 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
-#
+# See COPYING for license information.
 
 import shlex
 import sys
-import config
-import utils
-import options
-from msg import common_err, common_info, common_warn
-import ui_utils
-import userdir
+from . import config
+from . import utils
+from . import options
+from .msg import common_err, common_info, common_warn
+from . import ui_utils
+from . import userdir
 
 
 #import logging
@@ -79,6 +65,7 @@ class Context(object):
                 self.command_info = self.current_level().get_child(token)
                 if not self.command_info:
                     self.fatal_error("No such command")
+                self.command_name = self.command_info.name
                 if self.command_info.type == 'level':
                     self.enter_level(self.command_info.level)
                 else:
@@ -87,9 +74,15 @@ class Context(object):
             if cmd:
                 rv = self.execute_command() is not False
         except ValueError, msg:
+            if config.core.debug:
+                import traceback
+                traceback.print_exc()
             common_err("%s: %s" % (self.get_qualified_name(), msg))
             rv = False
         except IOError, msg:
+            if config.core.debug:
+                import traceback
+                traceback.print_exc()
             common_err("%s: %s" % (self.get_qualified_name(), msg))
             rv = False
         if cmd or (rv is False):
@@ -348,6 +341,13 @@ class Context(object):
         prev = self.previous_level()
         return prev and prev.name == level_name
 
+    def error(self, msg):
+        """
+        Too easy to misremember and type error()
+        when I meant fatal_error().
+        """
+        raise ValueError(msg)
+
     def fatal_error(self, msg):
         """
         TODO: Better error messages, with full context information
diff --git a/modules/ui_corosync.py b/modules/ui_corosync.py
index 0378908..771acdf 100644
--- a/modules/ui_corosync.py
+++ b/modules/ui_corosync.py
@@ -1,26 +1,12 @@
 # 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
-#
+# See COPYING for license information.
 
 import os
-import command
-import completers
-import utils
-from msg import err_buf
-import corosync
+from . import command
+from . import completers
+from . import utils
+from .msg import err_buf
+from . import corosync
 
 
 def _push_completer(args):
@@ -52,7 +38,7 @@ class Corosync(command.UI):
         if len(stack) > 0 and stack != 'corosync':
             err_buf.warning("Unsupported cluster stack %s detected." % (stack))
             return False
-        return True
+        return corosync.check_tools()
 
     def do_status(self, context):
         '''
diff --git a/modules/ui_history.py b/modules/ui_history.py
index 468b959..2a632d1 100644
--- a/modules/ui_history.py
+++ b/modules/ui_history.py
@@ -1,45 +1,34 @@
 # 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
-#
+# See COPYING for license information.
 
 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
+from . import config
+from . import command
+from . import completers as compl
+from . import utils
+from . import ui_utils
+from . import userdir
+from . import xmlutil
+from . import constants
+from . import options
+from .cibconfig import mkset_obj, cib_factory
+from .msg import common_err, common_debug, common_info
+from .msg import syntax_err
+from . import history
+from . import cmd_status
 
 
 ptest_options = ["@v+", "nograph", "scores", "actions", "utilization"]
 
-crm_report = report.Report()
+
+ at utils.memoize
+def crm_report():
+    return history.Report()
 
 
 class History(command.UI):
@@ -72,18 +61,21 @@ class History(command.UI):
             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)
+        if to_dt and from_dt:
+            if to_dt < from_dt:
+                from_dt, to_dt = to_dt, from_dt
+            elif to_dt == from_dt:
+                common_err("%s - %s: To and from dates cannot be the same" % (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):
+        if src == "live":
             return True
-        else:
-            common_err("source %s doesn't exist" % src)
-            return False
+        if os.path.isfile(src) or os.path.isdir(src):
+            return True
+        return False
 
     def _set_source(self, src, live_from_time=None):
         '''
@@ -92,8 +84,17 @@ class History(command.UI):
         '''
         common_debug("setting source to %s" % src)
         if not self._check_source(src):
+            if os.path.exists(crm_report().get_session_dir(src)):
+                common_debug("Interpreting %s as session" % src)
+                if crm_report().load_state(crm_report().get_session_dir(src)):
+                    options.history = crm_report().get_source()
+                    crm_report().prepare_source()
+                    self.current_session = src
+                    return True
+            else:
+                common_err("source %s doesn't exist" % src)
             return False
-        crm_report.set_source(src)
+        crm_report().set_source(src)
         options.history = src
         self.current_session = None
         to_time = ''
@@ -133,32 +134,31 @@ class History(command.UI):
             if force != "force" and force != "--force":
                 context.fatal_error("Expected 'force' or '--force' (was '%s')" % (force))
             force = True
-        return crm_report.refresh_source(force)
+        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)
+        if detail_num is None or detail_num not in (0, 1):
+            context.fatal_error("Expected '0' or '1' (was '%s')" % (detail_lvl))
+        return crm_report().set_detail(detail_lvl)
 
     @command.skill_level('administrator')
-    @command.completers_repeating(compl.call(crm_report.node_list))
+    @command.completers_repeating(compl.call(lambda: 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)
+        return crm_report().set_nodes(*args)
 
     @command.skill_level('administrator')
     def do_info(self, context):
         "usage: info"
         self._init_source()
-        return crm_report.info()
+        return crm_report().info()
 
     @command.skill_level('administrator')
     def do_latest(self, context):
@@ -167,47 +167,51 @@ class History(command.UI):
         if not utils.wait4dc("transition", not options.batch):
             return False
         self._set_source("live")
-        crm_report.refresh_source()
+        crm_report().refresh_source()
         f = self._get_pe_byidx(-1)
         if not f:
             return False
-        crm_report.show_transition_log(f)
+        crm_report().show_transition_log(f)
 
     @command.skill_level('administrator')
-    @command.completers_repeating(compl.call(crm_report.rsc_list))
+    @command.completers_repeating(compl.call(lambda: crm_report().rsc_list()))
     def do_resource(self, context, *args):
         "usage: resource <rsc> [<rsc> ...]"
         self._init_source()
-        return crm_report.resource(*args)
+        return crm_report().resource(*args)
 
     @command.skill_level('administrator')
     @command.wait
-    @command.completers_repeating(compl.call(crm_report.node_list))
+    @command.completers_repeating(compl.call(lambda: crm_report().node_list()))
     def do_node(self, context, *args):
         "usage: node <node> [<node> ...]"
         self._init_source()
-        return crm_report.node(*args)
+        return crm_report().node(*args)
 
     @command.skill_level('administrator')
-    @command.completers_repeating(compl.call(crm_report.node_list))
+    @command.completers_repeating(compl.call(lambda: crm_report().node_list()))
     def do_log(self, context, *args):
         "usage: log [<node> ...]"
         self._init_source()
-        return crm_report.log(*args)
+        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)
+            s = bz2.decompress(open(self.pe_file).read())
         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),
+    def do_events(self, context):
+        "usage: events"
+        self._init_source()
+        return crm_report().events()
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.join(compl.call(lambda: crm_report().peinputs_list()),
                                              compl.choice(['v'])))
     def do_peinputs(self, context, *args):
         """usage: peinputs [{<range>|<number>} ...] [v]"""
@@ -221,16 +225,16 @@ class History(command.UI):
                 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))
+                l += crm_report().pelist(a, long=("v" in opt_l))
         else:
-            l = crm_report.pelist(long=("v" in opt_l))
+            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)
+        l = crm_report().find_pe_files(s)
         if len(l) == 0:
             common_err("%s: path not found" % s)
             return None
@@ -240,7 +244,7 @@ class History(command.UI):
         return l[0]
 
     def _get_pe_byidx(self, idx):
-        l = crm_report.pelist()
+        l = crm_report().pelist()
         if len(l) < abs(idx):
             if idx == -1:
                 common_err("no transitions found in the source")
@@ -250,7 +254,7 @@ class History(command.UI):
         return l[idx]
 
     def _get_pe_bynum(self, n):
-        l = crm_report.pelist([n])
+        l = crm_report().pelist([n])
         if len(l) == 0:
             common_err("PE file %d not found" % n)
             return None
@@ -277,13 +281,13 @@ class History(command.UI):
     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)
+        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)
+        f = crm_report().pe2dot(f)
         if not f:
             common_err("dot file not found in the report")
             return False
@@ -299,7 +303,7 @@ class History(command.UI):
         return xmlutil.pe2shadow(f, name)
 
     @command.skill_level('administrator')
-    @command.completers(compl.join(compl.call(crm_report.peinputs_list),
+    @command.completers(compl.join(compl.call(lambda: 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]
@@ -309,7 +313,7 @@ class History(command.UI):
         self._init_source()
         argl = list(args)
         subcmd = "show"
-        if argl and argl[0] in ("showdot", "log", "save"):
+        if argl and argl[0] in ("showdot", "log", "save", "tags"):
             subcmd = argl[0]
             del argl[0]
         if subcmd == "show":
@@ -332,8 +336,10 @@ class History(command.UI):
             rc = self._display_dot(f)
         elif subcmd == "save":
             rc = self._pe2shadow(f, argl)
+        elif subcmd == "tags":
+            rc = crm_report().show_transition_tags(f)
         else:
-            rc = crm_report.show_transition_log(f, True)
+            rc = crm_report().show_transition_log(f, True)
         return rc
 
     def _save_cib_env(self):
@@ -436,15 +442,12 @@ class History(command.UI):
         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
+            _, s = utils.get_stdout("wdiff %s %s" % (f1, f2))
+        for f in (f1, f2):
+            try:
+                os.unlink(f)
+            except:
+                pass
         return s
 
     def _unidiff(self, s1, s2, t1, t2):
@@ -452,16 +455,14 @@ class History(command.UI):
         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" %
+            _, 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
+
+        for f in (f1, f2):
+            try:
+                os.unlink(f)
+            except:
+                pass
         return s
 
     def _diffhtml(self, s1, s2, t1, t2):
@@ -522,7 +523,8 @@ class History(command.UI):
         print utils.str2tmp(s)
 
     @command.skill_level('administrator')
-    @command.completers(compl.join(compl.call(crm_report.peinputs_list), compl.choice(['live'])),
+    @command.completers(compl.join(compl.call(lambda: crm_report().peinputs_list()),
+                                   compl.choice(['live'])),
                         compl.choice(['status']))
     def do_show(self, context, t, *args):
         "usage: show <pe> [status]"
@@ -541,7 +543,8 @@ class History(command.UI):
         utils.page_string(s)
 
     @command.skill_level('administrator')
-    @command.completers(compl.join(compl.call(crm_report.peinputs_list), compl.choice(['live'])))
+    @command.completers(compl.join(compl.call(lambda: crm_report().peinputs_list()),
+                                   compl.choice(['live'])))
     def do_graph(self, context, t, *args):
         "usage: graph <pe> [<gtype> [<file> [<img_format>]]]"
         self._init_source()
@@ -566,8 +569,10 @@ class History(command.UI):
         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'])))
+    @command.completers(compl.join(compl.call(lambda: crm_report().peinputs_list()),
+                                   compl.choice(['live'])),
+                        compl.join(compl.call(lambda: crm_report().peinputs_list()),
+                                   compl.choice(['live'])))
     def do_diff(self, context, t1, t2, *args):
         "usage: diff <pe> <pe> [status] [html]"
         self._init_source()
@@ -591,8 +596,10 @@ class History(command.UI):
             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'])))
+    @command.completers(compl.join(compl.call(lambda: crm_report().peinputs_list()),
+                                   compl.choice(['live'])),
+                        compl.join(compl.call(lambda: crm_report().peinputs_list()),
+                                   compl.choice(['live'])))
     def do_wdiff(self, context, t1, t2, *args):
         "usage: wdiff <pe> <pe> [status]"
         self._init_source()
@@ -610,8 +617,8 @@ class History(command.UI):
         utils.page_string(s)
 
     @command.skill_level('administrator')
-    @command.completers(compl.call(crm_report.session_subcmd_list),
-                        compl.call(crm_report.session_list))
+    @command.completers(compl.call(lambda: crm_report().session_subcmd_list()),
+                        compl.call(lambda: 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()
@@ -638,11 +645,11 @@ class History(command.UI):
         if not name:
             # some commands work on the existing session
             name = self.current_session
-        rc = crm_report.manage_session(subcmd, name)
+        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()
+            options.history = crm_report().get_source()
+            crm_report().prepare_source()
             self.current_session = name
         elif rc and subcmd == "delete":
             if name == self.current_session:
@@ -656,11 +663,9 @@ class History(command.UI):
         "usage: exclude [<regex>|clear]"
         self._init_source()
         if not arg:
-            rc = crm_report.manage_excludes("show")
+            return crm_report().manage_excludes("show")
         elif arg == "clear":
-            rc = crm_report.manage_excludes("clear")
-        else:
-            rc = crm_report.manage_excludes("add", arg)
-        return rc
+            return crm_report().manage_excludes("clear")
+        return crm_report().manage_excludes("add", arg)
 
 # vim:ts=4:sw=4:et:
diff --git a/modules/ui_maintenance.py b/modules/ui_maintenance.py
new file mode 100644
index 0000000..4d6ce0b
--- /dev/null
+++ b/modules/ui_maintenance.py
@@ -0,0 +1,94 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+# See COPYING for license information.
+
+from . import command
+from . import completers as compl
+from .cibconfig import cib_factory
+from . import utils
+from . import xmlutil
+
+_compl_actions = compl.choice(['start', 'stop', 'monitor', 'meta-data', 'validate-all',
+                               'promote', 'demote', 'notify', 'reload', 'migrate_from',
+                               'migrate_to', 'recover'])
+
+
+class Maintenance(command.UI):
+    '''
+    Commands that should only be run while in
+    maintenance mode.
+    '''
+    name = "maintenance"
+
+    rsc_maintenance = "crm_resource -r '%s' --meta -p maintenance -v '%s'"
+
+    def __init__(self):
+        command.UI.__init__(self)
+
+    def requires(self):
+        return cib_factory.initialize()
+
+    def _onoff(self, resource, onoff):
+        if resource is not None:
+            return utils.ext_cmd(self.rsc_maintenance % (resource, onoff)) == 0
+        else:
+            return cib_factory.create_object('property', 'maintenance-mode=%s' % (onoff))
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.call(cib_factory.rsc_id_list))
+    def do_on(self, context, resource=None):
+        '''
+        Enable maintenance mode (for the optional resource or for everything)
+        '''
+        return self._onoff(resource, 'true')
+
+    @command.skill_level('administrator')
+    @command.completers_repeating(compl.call(cib_factory.rsc_id_list))
+    def do_off(self, context, resource=None):
+        '''
+        Disable maintenance mode (for the optional resource or for everything)
+        '''
+        return self._onoff(resource, 'false')
+
+    def _in_maintenance_mode(self, obj):
+        if cib_factory.get_property("maintenance-mode") == "true":
+            return True
+        v = obj.meta_attributes("maintenance")
+        return v and all(x == 'true' for x in v)
+
+    def _runs_on_this_node(self, resource):
+        nodes = utils.running_on(resource)
+        return set(nodes) == set([utils.this_node()])
+
+    @command.skill_level('administrator')
+    @command.completers(compl.call(cib_factory.rsc_id_list), _compl_actions, compl.choice(["ssh"]))
+    def do_action(self, context, resource, action, ssh=None):
+        '''
+        Issue action out-of-band to the given resource, making
+        sure that the resource is in maintenance mode first
+        '''
+        obj = cib_factory.find_object(resource)
+        if not obj:
+            context.fatal_error("Resource not found: %s" % (resource))
+        if not xmlutil.is_resource(obj.node):
+            context.fatal_error("Not a resource: %s" % (resource))
+        if not self._in_maintenance_mode(obj):
+            context.fatal_error("Not in maintenance mode.")
+
+        if ssh is None:
+            if action not in ('start', 'monitor'):
+                if not self._runs_on_this_node(resource):
+                    context.fatal_error("Resource %s must be running on this node (%s)" %
+                                        (resource, utils.this_node()))
+
+            from . import rsctest
+            return rsctest.call_resource(obj.node, action, [utils.this_node()], local_only=True)
+        elif ssh == "ssh":
+            from . import rsctest
+            if action in ('start', 'promote', 'demote', 'recover', 'meta-data'):
+                return rsctest.call_resource(obj.node, action,
+                                             [utils.this_node()], local_only=True)
+            else:
+                all_nodes = cib_factory.node_id_list()
+                return rsctest.call_resource(obj.node, action, all_nodes, local_only=False)
+        else:
+            context.fatal_error("Unknown argument: %s" % (ssh))
diff --git a/modules/ui_node.py b/modules/ui_node.py
index 80176f9..e752039 100644
--- a/modules/ui_node.py
+++ b/modules/ui_node.py
@@ -1,30 +1,16 @@
 # 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
-#
+# See COPYING for license information.
 
-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
+from . import config
+from . import command
+from . import completers as compl
+from . import ui_utils
+from . import utils
+from . import xmlutil
+from .msg import common_err, syntax_err, no_prog_err, common_info, common_warn
+from .cliformat import cli_nvpairs, nvpairs2list
+from . import term
 
 
 def _oneline(s):
@@ -36,7 +22,7 @@ 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?
+    is_offline: true|false
     """
     type = uname = id = ""
     inst_attr = []
@@ -53,7 +39,7 @@ def unpack_node_xmldata(node, is_offline):
             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)
+    return uname, id, type, other, inst_attr, is_offline
 
 
 def print_node(uname, id, node_type, other, inst_attr, offline):
@@ -86,6 +72,7 @@ class NodeMgmt(command.UI):
     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_cleanup_resources = "crm_resource --cleanup --node '%s'"
     node_clear_state = _oneline("""cibadmin %s
       -o status --xml-text
       '<node_state id="%s"
@@ -101,22 +88,22 @@ class NodeMgmt(command.UI):
     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"
+    node_fence = "crm_attribute -t status -N '%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'",
+        'set': "crm_attribute -t nodes -N '%s' -n '%s' -v '%s'",
+        'delete': "crm_attribute -D -t nodes -N '%s' -n '%s'",
+        'show': "crm_attribute -G -t nodes -N '%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'",
+        'set': "crm_attribute -t status -N '%s' -n '%s' -v '%s'",
+        'delete': "crm_attribute -D -t status -N '%s' -n '%s'",
+        'show': "crm_attribute -G -t status -N '%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'",
+        'set': "crm_attribute -z -t nodes -N '%s' -n '%s' -v '%s'",
+        'delete': "crm_attribute -z -D -t nodes -N '%s' -n '%s'",
+        'show': "crm_attribute -z -G -t nodes -N '%s' -n '%s'",
     }
 
     def requires(self):
@@ -137,26 +124,33 @@ class NodeMgmt(command.UI):
     @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:
+        cib = xmlutil.cibdump2elem()
+        if cib is None:
             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
+        cfg_nodes = cib.xpath('/cib/configuration/nodes/node')
+        node_states = cib.xpath('/cib/status/node_state')
+
+        def find(it, lst):
+            for n in lst:
+                if n.get("uname") == it:
+                    return n
+            return None
+
+        def do_print(uname):
+            xml = find(uname, cfg_nodes)
+            state = find(uname, node_states)
+            if xml is not None or state is not None:
+                is_offline = state is not None and state.get("crmd") == "offline"
+                print_node(*unpack_node_xmldata(xml if xml is not None else state, is_offline))
 
-        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))
+        if node is not None:
+            do_print(node)
+        else:
+            all_nodes = set([n.get("uname") for n in cfg_nodes + node_states])
+            for uname in sorted(all_nodes):
+                do_print(uname)
+        return True
 
     @command.wait
     @command.completers(compl.nodes)
@@ -235,7 +229,14 @@ class NodeMgmt(command.UI):
                 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
+            cib_elem = xmlutil.cibdump2elem()
+            if cib_elem is None:
+                return False
+            node_state = cib_elem.xpath("//node_state[@uname=\"%s\"]/@crmd" % node)
+            if node_state == ['online']:
+                return utils.ext_cmd(self.node_cleanup_resources % node) == 0
+            else:
+                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
diff --git a/modules/ui_options.py b/modules/ui_options.py
index 014b72b..43744a5 100644
--- a/modules/ui_options.py
+++ b/modules/ui_options.py
@@ -1,25 +1,11 @@
 # 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
+# See COPYING for license information.
+
+from . import command
+from . import completers
+from . import config
+from . import options
 
 _yesno = completers.choice(['yes', 'no'])
 
@@ -111,7 +97,9 @@ class CliOptions(command.UI):
     @command.completers(_getprefs('output'))
     def do_output(self, context, output_type):
         "usage: output <type>"
-        return _legacy_set_pref("output", output_type)
+        _legacy_set_pref("output", output_type)
+        from . import term
+        term._init()
 
     def do_colorscheme(self, context, colors):
         "usage: colorscheme <colors>"
@@ -157,24 +145,26 @@ class CliOptions(command.UI):
         "usage: manage-children <option>"
         return _legacy_set_pref("manage-children", opt)
 
+    @command.alias('list')
     @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()
+        from . import utils
+        opts = config.get_configured_options() if option is None else config.get_all_options()
+
+        def show_options(fn):
+            s = ''
             for opt in opts:
-                parts = opt.split('.')
-                print "%s = %s" % (opt, config.get_option(parts[0], parts[1], raw=True))
+                if fn(opt):
+                    parts = opt.split('.')
+                    val = (opt, config.get_option(parts[0], parts[1], raw=True))
+                    s += "%s = %s\n" % val
+            utils.page_string(s)
+
+        if option == 'all' or option is None:
+            show_options(lambda o: 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)
+            show_options(lambda o: o.startswith(option) or o.endswith(option))
 
     def do_save(self, context):
         "usage: save"
diff --git a/modules/ui_ra.py b/modules/ui_ra.py
index d3d2cf6..50c22f3 100644
--- a/modules/ui_ra.py
+++ b/modules/ui_ra.py
@@ -1,27 +1,14 @@
 # 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
-#
+# See COPYING for license information.
 
-import command
-import completers as compl
-import utils
-import ra
-import constants
-import options
+from . import command
+from . import completers as compl
+from . import utils
+from . import ra
+from . import constants
+from . import options
+from . import msg as msglog
 
 
 def complete_class_provider_type(args):
@@ -55,7 +42,9 @@ class RA(command.UI):
         "usage: classes"
         for c in ra.ra_classes():
             if c in self.provider_classes:
-                print "%s / %s" % (c, ' '.join(ra.ra_providers_all(c)))
+                providers = ra.ra_providers_all(c)
+                if providers:
+                    print "%s / %s" % (c, ' '.join(providers))
             else:
                 print "%s" % c
 
@@ -87,18 +76,14 @@ class RA(command.UI):
         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"
+                ra_type, ra_class, ra_provider = args[0], args[1], "heartbeat"
             else:
-                ra_provider = args[2]
+                ra_type, ra_class, ra_provider = args[0], args[1], args[2]
+        elif args[0] in constants.meta_progs:
+            ra_class, ra_provider, ra_type = args[0], None, None
         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])
+            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
@@ -106,3 +91,20 @@ class RA(command.UI):
             utils.page_string(agent.meta_pretty())
         except Exception, msg:
             context.fatal_error(msg)
+
+    @command.skill_level('administrator')
+    def do_validate(self, context, agentname, *params):
+        "usage: validate [<class>:[<provider>:]]<type> [<key>=<value> ...]"
+        rc, out = ra.validate_agent(agentname, dict([param.split('=', 1) for param in params]))
+        for msg in out.splitlines():
+            if msg.startswith("ERROR: "):
+                msglog.err_buf.error(msg[7:])
+            elif msg.startswith("WARNING: "):
+                msglog.err_buf.warning(msg[9:])
+            elif msg.startswith("INFO: "):
+                msglog.err_buf.info(msg[6:])
+            elif msg.startswith("DEBUG: "):
+                msglog.err_buf.debug(msg[7:])
+            else:
+                msglog.err_buf.writemsg(msg)
+        return rc == 0
diff --git a/modules/ui_report.py b/modules/ui_report.py
index 0d8b91d..a4e9f1e 100644
--- a/modules/ui_report.py
+++ b/modules/ui_report.py
@@ -1,41 +1,32 @@
 # 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
-#
+# See COPYING for license information.
 
 import os
-import utils
-import config
-import options
 import subprocess
 from signal import signal, SIGPIPE, SIG_DFL
 
+from . import utils
+from . import config
+from . import options
 
-def create_report(context, args):
-    toolopts = [os.path.join(config.path.sharedir, 'hb_report'),
+
+def report_tool():
+    toolopts = [os.path.join(config.path.sharedir, 'hb_report', 'hb_report'),
                 'hb_report',
                 'crm_report']
-    extcmd = None
     for tool in toolopts:
         if utils.is_program(tool):
-            extcmd = tool
-            break
+            return tool
+    return None
+
+
+def create_report(context, args):
+    extcmd = report_tool()
     if not extcmd:
         context.fatal_error("No reporting tool found")
-    cmd = [extcmd] + list(args)
+    extraopts = str(config.core.report_tool_options).strip().split()
+    cmd = [extcmd] + extraopts + 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
index b1f0403..429235c 100644
--- a/modules/ui_resource.py
+++ b/modules/ui_resource.py
@@ -1,33 +1,19 @@
 # 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
+# See COPYING for license information.
+
+from . import command
+from . import completers as compl
+from . import constants
+from . import config
+from . import utils
+from . import xmlutil
+from . import ui_utils
+from . 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):
@@ -185,11 +171,12 @@ class RscMgmt(command.UI):
     name = "resource"
 
     rsc_status_all = "crm_resource -L"
-    rsc_status = "crm_resource -W -r '%s'"
+    rsc_status = "crm_resource --locate -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_migrate = "crm_resource --quiet --move -r '%s' %s"
+    rsc_unmigrate = "crm_resource --quiet --clear -r '%s'"
+    rsc_ban = "crm_resource --ban -r '%s' %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'"
@@ -235,12 +222,15 @@ class RscMgmt(command.UI):
 
     @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
+    def do_status(self, context, *resources):
+        "usage: status [<rsc> ...]"
+        if len(resources) > 0:
+            rc = True
+            for rsc in resources:
+                if not utils.is_name_sane(rsc):
+                    return False
+                rc = rc and (utils.ext_cmd(self.rsc_status % rsc) == 0)
+            return rc
         else:
             return utils.ext_cmd(self.rsc_status_all) == 0
 
@@ -255,29 +245,54 @@ class RscMgmt(command.UI):
             context.info("Currently editing the CIB, changes will not be committed")
         return set_deep_meta_attr(rsc, name, value, commit=commit)
 
+    def _commit_meta_attrs(self, context, resources, name, value):
+        """
+        Perform change to list of resources
+        """
+        for rsc in resources:
+            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")
+
+        rc = True
+        for rsc in resources:
+            rc = rc and set_deep_meta_attr(rsc, name, value, commit=False)
+        if commit and rc:
+            ok = cib_factory.commit()
+            if not ok:
+                common_error("Failed to commit updates to %s" % (rsc))
+            return ok
+        return rc
+
     @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")
+    def do_start(self, context, *resources):
+        "usage: start <rsc> [<rsc> ...]"
+        if len(resources) == 0:
+            context.error("Expected at least one resource as argument")
+        return self._commit_meta_attrs(context, resources, "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")
+    def do_stop(self, context, *resources):
+        "usage: stop <rsc> [<rsc> ...]"
+        if len(resources) == 0:
+            context.error("Expected at least one resource as argument")
+        return self._commit_meta_attrs(context, resources, "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"):
+    def do_restart(self, context, *resources):
+        "usage: restart <rsc> [<rsc> ...]"
+        common_info("ordering %s to stop" % ", ".join(resources))
+        if not self._commit_meta_attrs(context, resources, "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")
+        common_info("ordering %s to start" % ", ".join(resources))
+        return self._commit_meta_attrs(context, resources, "target-role", "Started")
 
     @command.wait
     @command.completers(compl.resources)
@@ -346,7 +361,31 @@ class RscMgmt(command.UI):
             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_repeating(compl.resources, compl.nodes)
+    def do_ban(self, context, rsc, *args):
+        """usage: ban <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_ban % (rsc, opts)) == 0
+
+    @command.alias('unmove', 'unban')
     @command.skill_level('administrator')
     @command.wait
     @command.completers(compl.resources)
@@ -366,6 +405,23 @@ class RscMgmt(command.UI):
         return cleanup_resource(resource, node)
 
     @command.wait
+    @command.completers(compl.resources, compl.nodes)
+    def do_operations(self, context, resource=None, node=None):
+        "usage: operations [<rsc>] [<node>]"
+        cmd = "crm_resource -O"
+        if resource is None:
+            return utils.ext_cmd(cmd)
+        if node is None:
+            return utils.ext_cmd("%s -r '%s'" % (cmd, resource))
+        return utils.ext_cmd("%s -r '%s' -N '%s'" % (cmd, resource, node))
+
+    @command.wait
+    @command.completers(compl.resources)
+    def do_constraints(self, context, resource):
+        "usage: constraints <rsc>"
+        return utils.ext_cmd("crm_resource -a -r '%s'" % (resource))
+
+    @command.wait
     @command.completers(compl.resources, _attrcmds, compl.nodes)
     def do_failcount(self, context, rsc, cmd, node, value=None):
         """usage:
diff --git a/modules/ui_root.py b/modules/ui_root.py
index ca57480..8fefdb7 100644
--- a/modules/ui_root.py
+++ b/modules/ui_root.py
@@ -1,20 +1,6 @@
 # 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
-#
+# See COPYING for license information.
 
 # Revised UI structure for crmsh
 #
@@ -31,20 +17,22 @@
 #   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
+from . import command
+from . import cmd_status
+from . import ui_cib
+from . import ui_cibstatus
+from . import ui_cluster
+from . import ui_configure
+from . import ui_corosync
+from . import ui_history
+from . import ui_maintenance
+from . import ui_node
+from . import ui_options
+from . import ui_ra
+from . import ui_report
+from . import ui_resource
+from . import ui_script
+from . import ui_site
 
 
 class Root(command.UI):
@@ -55,32 +43,6 @@ class Root(command.UI):
     # 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
@@ -91,13 +53,19 @@ 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.
+    @command.level(ui_cibstatus.CibStatusUI)
+    @command.help('''CIB status management and editing
+Enter edit and manage the CIB status section level.
 ''')
-    def do_resource(self):
+    def do_cibstatus(self):
+        pass
+
+    @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_configure.CibConfig)
@@ -111,20 +79,13 @@ 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.
+    @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_options(self):
+    def do_corosync(self):
         pass
 
     @command.level(ui_history.History)
@@ -136,13 +97,28 @@ 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.
+    @command.level(ui_maintenance.Maintenance)
+    @command.help('''maintenance
+Commands that should only be executed while in
+maintenance mode.
+''')
+    def do_maintenance(self):
+        pass
 
-Geo-cluster related management.
+    @command.level(ui_node.NodeMgmt)
+    @command.help('''nodes management
+A few node related tasks such as node standby are implemented
+here.
 ''')
-    def do_site(self):
+    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_ra.RA)
@@ -160,7 +136,35 @@ 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)
+        rc = ui_report.create_report(context, args)
+        return rc == 0
+
+    @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_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_site.Site)
+    @command.help('''Geo-cluster support
+The site level.
+
+Geo-cluster related management.
+''')
+    def do_site(self):
+        pass
 
     @command.help('''show cluster status
 Show cluster status. The status is displayed by `crm_mon`. Supply
diff --git a/modules/ui_script.py b/modules/ui_script.py
index 334ab93..b0cd449 100644
--- a/modules/ui_script.py
+++ b/modules/ui_script.py
@@ -1,24 +1,171 @@
 # 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
+# See COPYING for license information.
+
+
+import sys
+
+try:
+    import json
+except ImportError:
+    import simplejson as json
+
+from . import config
+from . import command
+from . import scripts
+from . import utils
+from . import options
+from . import completers as compl
+from .msg import err_buf
+
+
+class ConsolePrinter(object):
+    def __init__(self):
+        self.in_progress = False
+
+    def print_header(self, script, params, hosts):
+        if script['shortdesc']:
+            err_buf.info(script['shortdesc'])
+        err_buf.info("Nodes: " + ', '.join([x[0] for x in hosts]))
+
+    def error(self, host, message):
+        err_buf.error("[%s]: %s" % (host, message))
+
+    def output(self, host, rc, out, err):
+        if out:
+            err_buf.ok("[%s]: %s" % (host, out))
+        if err or rc != 0:
+            err_buf.error("[%s]: (rc=%d) %s" % (host, rc, err))
+
+    def start(self, action):
+        if not options.batch:
+            txt = '%s...' % (action['shortdesc'] or action['name'])
+            sys.stdout.write(txt)
+            sys.stdout.flush()
+            self.in_progress = True
+
+    def finish(self, action, rc, output):
+        self.flush()
+        if rc:
+            err_buf.ok(action['shortdesc'] or action['name'])
+        else:
+            err_buf.error("%s (rc=%s)" % (action['shortdesc'] or action['name'], rc))
+        if output:
+            print(output)
+
+    def flush(self):
+        if self.in_progress:
+            self.in_progress = False
+            if not config.core.debug:
+                sys.stdout.write('\r')
+            else:
+                sys.stdout.write('\n')
+            sys.stdout.flush()
+
+    def debug(self, msg):
+        if config.core.debug or options.regression_tests:
+            self.flush()
+            err_buf.debug(msg)
+
+    def print_command(self, nodes, command):
+        self.flush()
+        sys.stdout.write("** %s - %s\n" % (nodes, command))
+
+
+class JsonPrinter(object):
+    def __init__(self):
+        self.results = []
+
+    def print_header(self, script, params, hosts):
+        pass
+
+    def error(self, host, message):
+        self.results.append({'host': str(host), 'error': str(message) if message else ''})
+
+    def output(self, host, rc, out, err):
+        ret = {'host': host, 'rc': rc, 'output': str(out)}
+        if err:
+            ret['error'] = str(err)
+        self.results.append(ret)
+
+    def start(self, action):
+        pass
+
+    def finish(self, action, rc, output):
+        ret = {'rc': rc, 'shortdesc': str(action['shortdesc'])}
+        if rc != 0 and not rc:
+            ret['error'] = str(output) if output else ''
+        else:
+            ret['output'] = str(output) if output else ''
+        print(json.dumps(ret))
+
+    def flush(self):
+        pass
+
+    def debug(self, msg):
+        if config.core.debug:
+            err_buf.debug(msg)
+
+    def print_command(self, nodes, command):
+        pass
+
+
+def describe_param(p, name, all):
+    if not all and p.get('advanced'):
+        return ""
+    opt = ' (required) ' if p['required'] else ''
+    opt += ' (unique) ' if p['unique'] else ''
+    if 'value' in p:
+        opt += (' (default: %s)' % (repr(p['value']))) if p['value'] else ''
+    s = "  %s%s\n" % (name, opt)
+    s += "      %s\n" % (p['shortdesc'])
+    return s
+
+
+def _scoped_name(context, name):
+    if context:
+        return ':'.join(context) + ':' + name
+    return name
+
+
+def describe_step(icontext, context, s, all):
+    ret = "%s. %s" % ('.'.join([str(i + 1) for i in icontext]), scripts.format_desc(s['shortdesc']) or 'Parameters')
+    if not s['required']:
+        ret += ' (optional)'
+    ret += '\n\n'
+    if s.get('name'):
+        context = context + [s['name']]
+    for p in s.get('parameters', []):
+        ret += describe_param(p, _scoped_name(context, p['name']), all)
+    for i, step in enumerate(s.get('steps', [])):
+        ret += describe_step(icontext + [i], context, step, all)
+    return ret
+
+
+def _nvpairs2parameters(args):
+    """
+    input: list with name=value nvpairs, where each name is a :-path
+    output: dict tree of name:value, where value can be a nested dict tree
+    """
+    def _set(d, path, val):
+        if len(path) == 1:
+            d[path[0]] = val
+        else:
+            if path[0] not in d:
+                d[path[0]] = {}
+            _set(d[path[0]], path[1:], val)
+
+    ret = {}
+    for key, val in utils.nvpairs2dict(args).iteritems():
+        _set(ret, key.split(':'), val)
+    return ret
+
+
+def _category_pretty(c):
+    if str(c).lower() == 'wizard':
+        return "Wizard (Legacy)"
+    elif str(c).lower() == 'sap':
+        return "SAP"
+    return str(c).capitalize()
 
 
 class Script(command.UI):
@@ -32,37 +179,337 @@ class Script(command.UI):
     '''
     name = "script"
 
-    def do_list(self, context):
+    @command.completers_repeating(compl.choice(['all', 'names']))
+    def do_list(self, context, *args):
         '''
         List available scripts.
+        hides scripts with category Script or '' by default,
+        unless "all" is passed as argument
         '''
-        for name in scripts.list_scripts():
-            main = scripts.load_script(name)
-            print "%-16s %s" % (name, main.get('name', ''))
+        for arg in args:
+            if arg.lower() not in ("all", "names"):
+                context.fatal_error("Unexpected argument '%s': expected  [all|names]" % (arg))
+        all = any([x for x in args if x.lower() == 'all'])
+        names = any([x for x in args if x.lower() == 'names'])
+        if not names:
+            categories = {}
+            for name in scripts.list_scripts():
+                try:
+                    script = scripts.load_script(name)
+                    if script is None:
+                        continue
+                    cat = script['category'].lower()
+                    if not all and cat == 'script':
+                        continue
+                    cat = _category_pretty(cat)
+                    if cat not in categories:
+                        categories[cat] = []
+                    categories[cat].append("%-16s %s" % (script['name'], script['shortdesc']))
+                except ValueError as err:
+                    err_buf.error(str(err))
+                    continue
+            for c, lst in sorted(categories.iteritems(), key=lambda x: x[0]):
+                if c:
+                    print("%s:\n" % (c))
+                for s in sorted(lst):
+                    print(s)
+                print('')
+        elif all:
+            for name in scripts.list_scripts():
+                print(name)
+        else:
+            for name in scripts.list_scripts():
+                try:
+                    script = scripts.load_script(name)
+                    if script is None or script['category'] == 'script':
+                        continue
+                except ValueError as err:
+                    err_buf.error(str(err))
+                    continue
+                print(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):
+    @command.completers_repeating(compl.call(scripts.list_scripts))
+    @command.alias('info', 'describe')
+    def do_show(self, context, name, all=None):
         '''
         Describe the given script.
         '''
-        return scripts.describe(name)
+        script = scripts.load_script(name)
+        if script is None:
+            return False
+
+        all = all == 'all'
+
+        vals = {
+            'name': script['name'],
+            'category': _category_pretty(script['category']),
+            'shortdesc': str(script['shortdesc']),
+            'longdesc': scripts.format_desc(script['longdesc']),
+            'steps': "\n".join((describe_step([i], [], s, all) for i, s in enumerate(script['steps'])))}
+        output = """%(name)s (%(category)s)
+%(shortdesc)s
+
+%(longdesc)s
 
-    def do_steps(self, context, name):
+%(steps)s
+""" % vals
+        if all:
+            output += "Common Parameters\n\n"
+            for name, defval, desc in scripts.common_params():
+                output += "  %s\n" % (name)
+                output += "      %s\n" % (desc)
+                if defval is not None:
+                    output += "      (default: %s)\n" % (defval)
+        utils.page_string(output)
+
+    @command.completers(compl.call(scripts.list_scripts))
+    def do_verify(self, context, name, *args):
         '''
-        Print names of steps in script
+        Verify the script parameters
         '''
-        main = scripts.load_script(name)
-        for step in main['steps']:
-            print step['name']
+        script = scripts.load_script(name)
+        if script is None:
+            return False
+        ret = scripts.verify(script, _nvpairs2parameters(args))
+        if ret is None:
+            return False
+        if not ret:
+            print("OK (no actions)")
+        for i, action in enumerate(ret):
+            shortdesc = action.get('shortdesc', '')
+            text = str(action.get('text', ''))
+            longdesc = str(action.get('longdesc', ''))
+            print("%s. %s\n" % (i + 1, shortdesc))
+            if longdesc:
+                for line in str(longdesc).split('\n'):
+                    print("\t%s" % (line))
+                print('')
+            if text:
+                for line in str(text).split('\n'):
+                    print("\t%s" % (line))
+                print('')
 
+    @command.completers(compl.call(scripts.list_scripts))
     def do_run(self, context, name, *args):
         '''
         Run the given script.
         '''
-        return scripts.run(name, args)
+        if not scripts.has_parallax:
+            raise ValueError("The parallax python package is missing")
+        script = scripts.load_script(name)
+        if script is not None:
+            return scripts.run(script, _nvpairs2parameters(args), ConsolePrinter())
+        return False
+
+    @command.name('_print')
+    @command.skill_level('administrator')
+    @command.completers(compl.call(scripts.list_scripts))
+    def do_print(self, context, name):
+        '''
+        Debug print the given script.
+        '''
+        script = scripts.load_script(name)
+        if script is None:
+            return False
+        import pprint
+        pprint.pprint(script)
+
+    @command.name('_actions')
+    @command.skill_level('administrator')
+    @command.completers(compl.call(scripts.list_scripts))
+    def do_actions(self, context, name, *args):
+        '''
+        Debug print the actions for the given script.
+        '''
+        script = scripts.load_script(name)
+        if script is None:
+            return False
+        ret = scripts.verify(script, _nvpairs2parameters(args))
+        if ret is None:
+            return False
+        import pprint
+        pprint.pprint(ret)
+
+    @command.name('_convert')
+    def do_convert(self, context, workflow, outdir=".", category="basic"):
+        """
+        Convert hawk wizards to cluster scripts
+        Needs more work to be really useful.
+        workflow: hawk workflow script
+        tgtdir: where the cluster script will be written
+        category: category set in new wizard
+        """
+        import yaml
+        import os
+        from collections import OrderedDict
+
+        def flatten(script):
+            if not isinstance(script, dict):
+                return script
+
+            for k, v in script.iteritems():
+                if isinstance(v, scripts.Text):
+                    script[k] = str(v)
+                elif isinstance(v, dict):
+                    script[k] = flatten(v)
+                elif isinstance(v, tuple) or isinstance(v, list):
+                    script[k] = [flatten(vv) for vv in v]
+                elif isinstance(v, basestring):
+                    script[k] = v.strip()
+
+            return script
+
+        def order_rep(dumper, data):
+            return dumper.represent_mapping(u'tag:yaml.org,2002:map', data.items(), flow_style=False)
+
+        def scriptsorter(item):
+            order = ["version", "name", "category", "shortdesc", "longdesc", "include", "parameters", "steps", "actions"]
+            return order.index(item[0])
+
+        yaml.add_representer(OrderedDict, order_rep)
+        fromscript = os.path.abspath(workflow)
+        tgtdir = outdir
+
+        scripts._build_script_cache()
+        name = os.path.splitext(os.path.basename(fromscript))[0]
+        script = scripts._load_script_file(name, fromscript)
+        script = flatten(script)
+        script["category"] = category
+        del script["name"]
+        del script["dir"]
+        script["actions"] = [{"cib": "\n\n".join([action["cib"] for action in script["actions"]])}]
+
+        script = OrderedDict(sorted(script.items(), key=scriptsorter))
+        if script is not None:
+            try:
+                os.mkdir(os.path.join(tgtdir, name))
+            except:
+                pass
+            tgtfile = os.path.join(tgtdir, name, "main.yml")
+            with open(tgtfile, 'w') as tf:
+                try:
+                    print("%s -> %s" % (fromscript, tgtfile))
+                    yaml.dump([script], tf, explicit_start=True, default_flow_style=False)
+                except Exception as err:
+                    print(err)
+
+    def _json_list(self, context, cmd):
+        """
+        ["list"]
+        """
+        for name in scripts.list_scripts():
+            try:
+                script = scripts.load_script(name)
+                if script is not None:
+                    print(json.dumps({'name': name,
+                                      'category': script['category'].lower(),
+                                      'shortdesc': script['shortdesc'],
+                                      'longdesc': scripts.format_desc(script['longdesc'])}))
+            except ValueError as err:
+                print(json.dumps({'name': name,
+                                  'error': str(err)}))
+        return True
+
+    def _json_show(self, context, cmd):
+        """
+        ["show", <name>]
+        """
+        if len(cmd) < 2:
+            print(json.dumps({'error': 'Incorrect number of arguments: %s (expected %s)' % (len(cmd), 2)}))
+            return False
+        name = cmd[1]
+        script = scripts.load_script(name)
+        if script is None:
+            return False
+        print(json.dumps({'name': script['name'],
+                          'category': script['category'].lower(),
+                          'shortdesc': script['shortdesc'],
+                          'longdesc': scripts.format_desc(script['longdesc']),
+                          'steps': scripts.clean_steps(script['steps'])}))
+        return True
+
+    def _json_verify(self, context, cmd):
+        """
+        ["verify", <name>, <params>]
+        """
+        if len(cmd) < 3:
+            print(json.dumps({'error': 'Incorrect number of arguments: %s (expected %s)' % (len(cmd), 3)}))
+            return False
+        name = cmd[1]
+        params = cmd[2]
+        script = scripts.load_script(name)
+        if script is None:
+            return False
+        actions = scripts.verify(script, params)
+        if actions is None:
+            return False
+        else:
+            for action in actions:
+                print(json.dumps({'name': str(action.get('name', '')),
+                                  'shortdesc': str(action.get('shortdesc', '')),
+                                  'longdesc': str(action.get('longdesc', '')),
+                                  'text': str(action.get('text', '')),
+                                  'nodes': str(action.get('nodes', ''))}))
+        return True
+
+    def _json_run(self, context, cmd):
+        """
+        ["run", <name>, <params>]
+        """
+        if len(cmd) < 3:
+            print(json.dumps({'error': 'Incorrect number of arguments: %s (expected %s)' % (len(cmd), 3)}))
+            return False
+        name = cmd[1]
+        params = cmd[2]
+        if not scripts.has_parallax:
+            raise ValueError("The parallax python package is missing")
+        script = scripts.load_script(name)
+        if script is None:
+            return False
+        printer = JsonPrinter()
+        ret = scripts.run(script, params, printer)
+        if not ret and printer.results:
+            for result in printer.results:
+                if 'error' in result:
+                    print(json.dumps(result))
+        return ret
+
+    def do_json(self, context, command):
+        """
+        JSON API for the scripts, for use in web frontends.
+        Line-based output: enter a JSON command,
+        get lines of output back. In the description below, the output is
+        described as an array, but really it is returned line-by-line.
+
+        API:
+
+        ["list"]
+        => [{name, shortdesc, category}]
+        ["show", <name>]
+        => [{name, shortdesc, longdesc, category, <<steps>>}]
+        <<steps>> := [{name, shortdesc, longdesc, required, <<parameters>>, <<steps>>}]
+        <<params>> := [{name, shortdesc, longdesc, required, unique, type, advanced, value, example}]
+        ["verify", <name>, <values>]
+        => [{shortdesc, longdesc, nodes}]
+        ["run", <name>, <values>]
+        => [{shortdesc, rc, output|error}]
+        """
+        cmd = json.loads(command)
+        if len(cmd) < 1:
+            print(json.dumps({'error': 'Failed to decode valid JSON command'}))
+            return False
+        try:
+            if cmd[0] == "list":
+                return self._json_list(context, cmd)
+            elif cmd[0] == "show":
+                return self._json_show(context, cmd)
+            elif cmd[0] == "verify":
+                return self._json_verify(context, cmd)
+            elif cmd[0] == "run":
+                return self._json_run(context, cmd)
+            else:
+                print(json.dumps({'error': "Unknown command: %s" % (cmd[0])}))
+                return False
+        except ValueError, err:
+            print(json.dumps({'error': str(err)}))
+            return False
diff --git a/modules/ui_site.py b/modules/ui_site.py
index c45bf5c..c14770c 100644
--- a/modules/ui_site.py
+++ b/modules/ui_site.py
@@ -1,27 +1,13 @@
 # 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
-#
+# See COPYING for license information.
 
 import time
-import command
-import completers as compl
-import config
-import utils
-from msg import no_prog_err
+from . import command
+from . import completers as compl
+from . import config
+from . import utils
+from .msg import no_prog_err
 
 _ticket_commands = {
     'grant': "%s -t '%s' -g",
diff --git a/modules/ui_template.py b/modules/ui_template.py
index a219b16..936f984 100644
--- a/modules/ui_template.py
+++ b/modules/ui_template.py
@@ -1,35 +1,21 @@
 # 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
-#
+# See COPYING for license information.
 
 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
+from . import command
+from . import completers as compl
+from . import utils
+from . import config
+from . import userdir
+from . 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):
@@ -63,7 +49,7 @@ class Template(command.UI):
     @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 ...]"
+        "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)):
@@ -201,12 +187,17 @@ class Template(command.UI):
             pass
         return rc
 
-    @command.completers(compl.choice(['templates']))
+    @command.completers(compl.choice(['configs', 'templates']))
     def do_list(self, context, templates=''):
-        "usage: list [templates]"
+        "usage: list [configs|templates]"
         if templates == "templates":
             utils.multicolumn(utils.listtemplates())
+        elif templates == "configs":
+            utils.multicolumn(utils.listconfigs())
         else:
+            print "Templates:"
+            utils.multicolumn(utils.listtemplates())
+            print "\nConfigurations:"
             utils.multicolumn(utils.listconfigs())
 
     def init_dir(self):
diff --git a/modules/ui_utils.py b/modules/ui_utils.py
index 16238cc..20cf296 100644
--- a/modules/ui_utils.py
+++ b/modules/ui_utils.py
@@ -1,25 +1,11 @@
 # 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
-#
+# See COPYING for license information.
 
 import re
 import inspect
-from msg import bad_usage, common_err
-import utils
+from .msg import bad_usage, common_err
+from . import utils
 
 
 def _get_attr_cmd(attr_ext_commands, subcmd):
@@ -98,7 +84,7 @@ def graph_args(args):
         configure graph [<gtype> [<file> [<img_format>]]]
         history graph <pe> [<gtype> [<file> [<img_format>]]]
     '''
-    from crm_gv import gv_types
+    from .crm_gv import gv_types
     gtype, outf, ftype = None, None, None
     try:
         gtype = args[0]
diff --git a/modules/userdir.py b/modules/userdir.py
index 4bdd857..8f44ea7 100644
--- a/modules/userdir.py
+++ b/modules/userdir.py
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 import os
 
@@ -54,11 +40,11 @@ def mv_user_files():
     global CRMCONF_DIR
 
     def _xdg_file(name, xdg_name, chk_fun, directory):
-        from msg import common_warn, common_info, common_debug
+        from .msg import common_warn, common_info, common_debug
         if not name:
             return name
         if not os.path.isdir(directory):
-            os.makedirs(directory, 0700)
+            os.makedirs(directory, 0o700)
         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))
diff --git a/modules/utils.py b/modules/utils.py
index de9721d..a72aa19 100644
--- a/modules/utils.py
+++ b/modules/utils.py
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 import os
 import sys
@@ -25,18 +11,33 @@ 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
+from . import config
+from . import userdir
+from . import constants
+from . import options
+from . import term
+from .msg import common_warn, common_info, common_debug, common_err, err_buf
+
+
+class memoize:
+    "Decorator to invoke a function once only for any argument"
+    def __init__(self, function):
+        self.function = function
+        self.memoized = {}
+
+    def __call__(self, *args):
+        try:
+            return self.memoized[args]
+        except KeyError:
+            self.memoized[args] = self.function(*args)
+            return self.memoized[args]
 
 
 getuser = userdir.getuser
 gethomedir = userdir.gethomedir
 
 
+ at memoize
 def this_node():
     'returns name of this node (hostname)'
     return os.uname()[1]
@@ -82,7 +83,7 @@ def can_ask():
     Is user-interactivity possible?
     Checks if connected to a TTY.
     """
-    return sys.stdin.isatty()
+    return (not options.ask_no) and sys.stdin.isatty()
 
 
 def ask(msg):
@@ -152,16 +153,16 @@ def multi_input(prompt=''):
 
 
 def verify_boolean(opt):
-    return opt.lower() in ("yes", "true", "on") or \
-        opt.lower() in ("no", "false", "off")
+    return opt.lower() in ("yes", "true", "on", "1") or \
+        opt.lower() in ("no", "false", "off", "0")
 
 
 def is_boolean_true(opt):
-    return opt.lower() in ("yes", "true", "on")
+    return opt.lower() in ("yes", "true", "on", "1")
 
 
 def is_boolean_false(opt):
-    return opt.lower() in ("no", "false", "off")
+    return opt.lower() in ("no", "false", "off", "0")
 
 
 def get_boolean(opt, dflt=False):
@@ -243,10 +244,10 @@ def pipe_string(cmd, s):
     return rc
 
 
-def filter_string(cmd, s, stderr_on=True):
+def filter_string(cmd, s, stderr_on=True, shell=True):
     rc = -1  # command failed
     outp = ''
-    if stderr_on:
+    if stderr_on is True:
         stderr = None
     else:
         stderr = subprocess.PIPE
@@ -255,12 +256,16 @@ def filter_string(cmd, s, stderr_on=True):
     if options.regression_tests:
         print ".EXT", cmd
     p = subprocess.Popen(cmd,
-                         shell=True,
+                         shell=shell,
                          stdin=subprocess.PIPE,
                          stdout=subprocess.PIPE,
                          stderr=stderr)
     try:
-        outp = p.communicate(s)[0]
+        ret = p.communicate(s)
+        if stderr_on == 'stdout':
+            outp = "\n".join(ret)
+        else:
+            outp = ret[0]
         p.wait()
         rc = p.returncode
     except OSError, (errno, strerror):
@@ -325,13 +330,10 @@ def file2list(fname):
     Read a file into a list (newlines dropped).
     '''
     try:
-        f = open(fname, "r")
+        return open(fname).read().split('\n')
     except IOError, msg:
         common_err(msg)
         return None
-    l = ''.join(f).split('\n')
-    f.close()
-    return l
 
 
 def safe_open_w(fname):
@@ -375,13 +377,6 @@ def is_name_sane(name):
     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:
@@ -408,6 +403,7 @@ def ext_cmd_nosudo(cmd, shell=True):
 
 
 def rmdir_r(d):
+    # TODO: Make sure we're not deleting something we shouldn't!
     if d and os.path.isdir(d):
         shutil.rmtree(d)
 
@@ -554,20 +550,11 @@ def stdout2list(cmd, stderr_on=True, shell=True):
 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)
+        open(dest, "a").write(open(src).read())
+        return True
     except IOError, msg:
-        common_err("open %s: %s" % (src, msg))
-        dest_f.close()
+        common_err("append %s to %s: %s" % (src, dest, msg))
         return False
-    dest_f.write(''.join(f))
-    f.close()
-    dest_f.close()
-    return True
 
 
 def get_dc():
@@ -677,7 +664,7 @@ def run_ptest(graph_s, nograph, scores, utilization, actions, verbosity):
     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))
+        common_debug("'%s' exited with (rc=%d)" % (ptest, rc))
         if actions and rc == 1:
             common_warn("No actions found.")
         else:
@@ -838,6 +825,17 @@ def is_process(s):
     return proc.returncode == 0
 
 
+def print_stacktrace():
+    """
+    Print the stack at the site of call
+    """
+    import traceback
+    import inspect
+    sf = inspect.currentframe().f_back.f_back
+    traceback.print_stack(sf)
+
+
+ at memoize
 def cluster_stack():
     if is_process("heartbeat:.[m]aster"):
         return "heartbeat"
@@ -1019,15 +1017,70 @@ def lines2cli(s):
     return [x for x in cl if x]
 
 
+def datetime_is_aware(dt):
+    """
+    Determines if a given datetime.datetime is aware.
+
+    The logic is described in Python's docs:
+    http://docs.python.org/library/datetime.html#datetime.tzinfo
+    """
+    return dt and dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
+
+
+def make_datetime_naive(dt):
+    """
+    Ensures that the datetime is not time zone-aware:
+
+    The returned datetime object is a naive time in UTC.
+    """
+    if dt and datetime_is_aware(dt):
+        return dt.replace(tzinfo=None) - dt.utcoffset()
+    return dt
+
+
+def total_seconds(td):
+    """
+    Backwards compatible implementation of timedelta.total_seconds()
+    """
+    if hasattr(datetime.timedelta, 'total_seconds'):
+        return td.total_seconds()
+    else:
+        return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
+
+
+def datetime_to_timestamp(dt):
+    """
+    Convert a datetime object into a floating-point second value
+    """
+    try:
+        return total_seconds(make_datetime_naive(dt) - datetime.datetime(1970, 1, 1))
+    except Exception as e:
+        common_err("datetime_to_timestamp error: %s" % (e))
+        return None
+
+
 def parse_time(t):
     '''
     Try to make sense of the user provided time spec.
     Use dateutil if available, otherwise strptime.
     Return the datetime value.
+
+    Also does time zone elimination by passing the datetime
+    through a timestamp conversion if necessary
     '''
     try:
         import dateutil.parser
+        import dateutil.tz
         dt = dateutil.parser.parse(t)
+
+        if datetime_is_aware(dt):
+            ts = datetime_to_timestamp(dt)
+            if ts is None:
+                return None
+            dt = datetime.datetime.fromtimestamp(ts)
+        else:
+            # convert to UTC from local time
+            dt = make_datetime_naive(dt.replace(tzinfo=dateutil.tz.tzlocal()))
     except ValueError, msg:
         common_err("%s: %s" % (t, msg))
         return None
@@ -1158,14 +1211,14 @@ def get_cib_attributes(cib_f, tag, attr_l, dflt_l):
     val_patt_l = [re.compile('%s="([^"]+)"' % x) for x in attr_l]
     val_l = []
     try:
-        f = open(cib_f, "r")
+        f = open(cib_f).read()
     except IOError, msg:
         common_err(msg)
         return dflt_l
     if os.path.splitext(cib_f)[-1] == '.bz2':
-        cib_s = bz2.decompress(''.join(f))
+        cib_s = bz2.decompress(f)
     else:
-        cib_s = ''.join(f)
+        cib_s = f
     for s in cib_s.split('\n'):
         if s.startswith(open_t):
             i = 0
@@ -1174,7 +1227,6 @@ def get_cib_attributes(cib_f, tag, attr_l, dflt_l):
                 val_l.append(r and r.group(1) or dflt_l[i])
                 i += 1
             break
-    f.close()
     return val_l
 
 
@@ -1194,25 +1246,21 @@ def is_pcmk_118(cib_f=None):
     return is_min_pcmk_ver("1.1.8", cib_f=cib_f)
 
 
-_cibadmin_features_cached = None
-
+ at memoize
 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
+    rc, outp = get_stdout(['cibadmin', '-!'], shell=False)
+    if rc == 0:
+        m = re.match(r'Pacemaker\s(\S+)\s\(Build: ([^\)]+)\):\s(.*)', outp.strip())
+        if m and len(m.groups()) > 2:
+            return m.group(3).split()
+    return []
 
 
+ at memoize
 def cibadmin_can_patch():
     # cibadmin -P doesn't handle comments in <1.1.11 (unless patched)
     return is_min_pcmk_ver("1.1.11")
@@ -1345,6 +1393,23 @@ def service_info(name):
     return None
 
 
+def running_on(resource):
+    "returns list of node names where the given resource is running"
+    rsc_locate = "crm_resource --resource '%s' --locate"
+    rc, out, err = get_stdout_stderr(rsc_locate % (resource))
+    if rc != 0:
+        return []
+    nodes = []
+    head = "resource %s is running on: " % (resource)
+    for line in out.split('\n'):
+        if line.strip().startswith(head):
+            w = line[len(head):].split()
+            if w:
+                nodes.append(w[0])
+    common_debug("%s running on: %s" % (resource, nodes))
+    return nodes
+
+
 # This RE matches nvpair values that can
 # be left unquoted
 _NOQUOTES_RE = re.compile(r'^[\w\.-]+$')
@@ -1354,4 +1419,97 @@ def noquotes(v):
     return _NOQUOTES_RE.match(v) is not None
 
 
+def remote_diff_slurp(nodes, filename):
+    try:
+        import parallax
+    except ImportError:
+        raise ValueError("Parallax is required to diff")
+    from . import tmpfiles
+
+    tmpdir = tmpfiles.create_dir()
+    opts = parallax.Options()
+    opts.localdir = tmpdir
+    dst = os.path.basename(filename)
+    return parallax.slurp(nodes, filename, dst, opts).items()
+
+
+def remote_diff_this(local_path, nodes, this_node):
+    try:
+        import parallax
+    except ImportError:
+        raise ValueError("Parallax is required to diff")
+
+    by_host = remote_diff_slurp(nodes, local_path)
+    for host, result in by_host:
+        if isinstance(result, parallax.Error):
+            raise ValueError("Failed on %s: %s" % (host, str(result)))
+        _, _, _, path = result
+        _, s = get_stdout("diff -U 0 -d -b --label %s --label %s %s %s" %
+                          (host, this_node, path, local_path))
+        page_string(s)
+
+
+def remote_diff(local_path, nodes):
+    try:
+        import parallax
+    except ImportError:
+        raise ValueError("parallax is required to diff")
+
+    by_host = remote_diff_slurp(nodes, local_path)
+    for host, result in by_host:
+        if isinstance(result, parallax.Error):
+            raise ValueError("Failed on %s: %s" % (host, str(result)))
+    h1, r1 = by_host[0]
+    h2, r2 = by_host[1]
+    _, s = get_stdout("diff -U 0 -d -b --label %s --label %s %s %s" %
+                      (h1, h2, r1[3], r2[3]))
+    page_string(s)
+
+
+def remote_checksum(local_path, nodes, this_node):
+    try:
+        import parallax
+    except ImportError:
+        raise ValueError("Parallax is required to diff")
+    import hashlib
+
+    by_host = remote_diff_slurp(nodes, local_path)
+    for host, result in by_host:
+        if isinstance(result, parallax.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 cluster_copy_file(local_path, nodes=None):
+    """
+    Copies given file to all other cluster nodes.
+    """
+    try:
+        import parallax
+    except ImportError:
+        raise ValueError("parallax is required to copy cluster files")
+    if not nodes:
+        nodes = list_cluster_nodes()
+        nodes.remove(this_node())
+    opts = parallax.Options()
+    opts.timeout = 60
+    opts.ssh_options += ['ControlPersist=no']
+    ok = True
+    for host, result in parallax.copy(nodes,
+                                      local_path,
+                                      local_path, opts).iteritems():
+        if isinstance(result, parallax.Error):
+            err_buf.error("Failed to push %s to %s: %s" % (local_path, host, result))
+            ok = False
+        else:
+            err_buf.ok(host)
+    return ok
+
+
 # vim:ts=4:sw=4:et:
diff --git a/modules/xmlbuilder.py b/modules/xmlbuilder.py
index c4c8f57..51ae952 100644
--- a/modules/xmlbuilder.py
+++ b/modules/xmlbuilder.py
@@ -1,22 +1,8 @@
 # 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
-#
+# See COPYING for license information.
 
 from lxml import etree
-import constants
+from . import constants
 
 
 def new(tag, **attributes):
@@ -65,7 +51,6 @@ 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:
diff --git a/modules/xmlutil.py b/modules/xmlutil.py
index 8052fec..0dfd31a 100644
--- a/modules/xmlutil.py
+++ b/modules/xmlutil.py
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 import os
 import subprocess
@@ -22,16 +8,16 @@ 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
+from . import config
+from . import options
+from . import schema
+from . import constants
+from .msg import common_err, common_error, common_debug, cib_parse_err, err_buf
+from . import userdir
+from . 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):
@@ -81,32 +67,35 @@ def compressed_file_to_cib(s):
 cib_dump = "cibadmin -Ql"
 
 
-def cibdump2file(fname):
-    cmd = add_sudo(cib_dump)
+def sudocall(cmd):
+    cmd = add_sudo(cmd)
     if options.regression_tests:
         print ".EXT", cmd
-    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     try:
-        s = ''.join(p.stdout)
+        outp, errp = p.communicate()
         p.wait()
+        return p.returncode, outp, errp
     except IOError, msg:
-        common_err(msg)
-        return None
-    return str2file(s, fname)
+        common_err("running %s: %s" % (cmd, msg))
+        return None, None, None
+
+
+def cibdump2file(fname):
+    _, outp, _ = sudocall(cib_dump)
+    if outp is not None:
+        return str2file(outp, fname)
+    return None
 
 
 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()
+        _, outp, _ = sudocall(cib_dump)
+        if outp is not None:
+            return str2tmp(outp)
     except IOError, msg:
         common_err(msg)
-        return None
-    return tmpf
+    return None
 
 
 def cibtext2elem(cibtext):
@@ -126,27 +115,12 @@ def cibdump2elem(section=None):
         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
+    rc, outp, errp = sudocall(cmd)
     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
+    elif rc != constants.cib_no_section_rc:
+        common_error("running %s: %s" % (cmd, errp))
+    return None
 
 
 def read_cib(fun, params=None):
@@ -217,13 +191,11 @@ class RscState(object):
         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")
+        cib = cibdump2elem("configuration")
+        self.current_cib = cib
+        self.rsc_elem = get_first_conf_elem(cib, "resources")
+        self.prop_elem = get_first_conf_elem(cib, "crm_config/cluster_property_set")
+        self.rsc_dflt_elem = get_first_conf_elem(cib, "rsc_defaults/meta_attributes")
 
     def rsc2node(self, id):
         '''
@@ -333,9 +305,9 @@ def is_normal_node(n):
 def unique_ra(typ, klass, provider):
     """
     Unique:
-    * it's explicitly ocf:heartbeat: or ocf:pacemaker:
+    * it's explicitly ocf:heartbeat:
     * no explicit class or provider
-    * only one provider (heartbeat and pacemaker counts as one provider)
+    * only one provider (heartbeat counts as one provider)
     Not unique:
     * class is not ocf
     * multiple providers
@@ -421,23 +393,19 @@ def shadowfile(name):
 def pe2shadow(pe_file, name):
     '''Copy a PE file (or any CIB file) to a shadow.'''
     try:
-        f = open(pe_file)
+        s = open(pe_file).read()
     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")
+        open(shadowfile(name), "w").write(s)
     except IOError, msg:
         common_err("open: %s" % msg)
         return False
-    f.write(s)
-    f.close()
     return True
 
 
@@ -615,6 +583,26 @@ def rsc_constraint(rsc_id, con_elem):
     return False
 
 
+def is_related(rsc_id, node):
+    """
+    checks if the given node is an element
+    that has a direct relation to rsc_id. That is,
+    if it contains it, if it references it...
+    """
+    if is_constraint(node) and rsc_constraint(rsc_id, node):
+        return True
+    if node.tag == 'tag':
+        if len(node.xpath('.//obj_ref[@id="%s"]' % (rsc_id))) > 0:
+            return True
+        return False
+    if is_container(node):
+        for tag in ('primitive', 'group', 'clone', 'master'):
+            if len(node.xpath('.//%s[@id="%s"]' % (tag, rsc_id))) > 0:
+                return True
+        return False
+    return False
+
+
 def sort_container_children(e_list):
     '''
     Make sure that attributes's nodes are first, followed by the
@@ -831,19 +819,22 @@ def make_sort_map(*order):
     return m
 
 
-_sort_xml_order = make_sort_map('node', 'template', 'primitive',
-                                'group', 'master', 'clone',
-                                'rsc_location', 'rsc_colocation',
-                                'rsc_order', 'rsc_ticket', 'fencing-topology',
+_sort_xml_order = make_sort_map('node',
+                                'template', 'primitive', 'group', 'master', 'clone', 'op',
+                                'tag',
+                                ['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',
+                                'acl_role', ['acl_target', 'acl_group', 'acl_user'])
+
+_sort_cli_order = make_sort_map('node',
+                                'rsc_template', 'primitive', 'group',
+                                ['ms', 'master'], 'clone', 'op',
+                                'tag',
+                                ['location', 'colocation', 'collocation', 'order'],
+                                ['rsc_ticket', 'fencing_topology'],
                                 'property', 'rsc_defaults', 'op_defaults',
-                                'op', 'role', 'user', 'tag')
+                                'role', ['acl_target', 'acl_group', 'user'])
 
 _SORT_LAST = 1000
 
@@ -852,6 +843,8 @@ def processing_sort(nl):
     '''
     It's usually important to process cib objects in this order,
     i.e. simple objects first.
+
+    TODO: if sort_elements is disabled, only sort to resolve inter-dependencies.
     '''
     if config.core.sort_elements:
         sortfn = lambda k: (_sort_xml_order.get(k.tag, _SORT_LAST), k.get('id'))
@@ -864,6 +857,8 @@ def processing_sort_cli(cl):
     '''
     cl: list of objects (CibObject)
     Returns the given list in order
+
+    TODO: if sort_elements is disabled, only sort to resolve inter-dependencies.
     '''
     if config.core.sort_elements:
         sortfn = lambda k: (_sort_cli_order.get(k.obj_type, _SORT_LAST), k.obj_id)
@@ -1026,7 +1021,7 @@ def rename_rscref_rset(c_obj, old_id, new_id):
 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))
+        err_buf.info("modified %s from %s to %s" % (str(c_obj), old_id, new_id))
 
 
 def delete_rscref(c_obj, rsc_id):
@@ -1180,7 +1175,7 @@ def set_attr(e, attr, value):
     '''
     nvpair = get_attr_in_set(e, attr)
     if nvpair is None:
-        import idmgmt
+        from . import idmgmt
         nvpair = etree.SubElement(e, "nvpair", id="", name=attr, value=value)
         nvpair.set("id", idmgmt.new(nvpair, e.get("id")))
     else:
@@ -1196,7 +1191,7 @@ def get_set_nodes(e, setname, create=False):
     if l:
         return l
     if create:
-        import idmgmt
+        from . import idmgmt
         elem = etree.SubElement(e, setname, id="")
         elem.set("id", idmgmt.new(elem, e.get("id")))
         l.append(elem)
@@ -1207,7 +1202,10 @@ _checker = doctestcompare.LXMLOutputChecker()
 
 
 def xml_equals_unordered(a, b):
-    "used by xml_equals to compare xml trees without ordering"
+    """
+    used by xml_equals to compare xml trees without ordering.
+    NOTE: resource_set children SHOULD be compared with ordering.
+    """
     def fail(msg):
         common_debug("%s!=%s: %s" % (a.tag, b.tag, msg))
         return False
@@ -1238,8 +1236,11 @@ def xml_equals_unordered(a, b):
 
     # 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)
+    if a.tag == 'resource_set':
+        return all(xml_equals_unordered(a, b) for a, b in zip(a, b))
+    else:
+        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):
@@ -1320,4 +1321,12 @@ def merge_tmpl_into_prim(prim_node, tmpl_node):
     return dnode
 
 
+def check_id_ref(elem, id_ref):
+    target = 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)
+
+
 # vim:ts=4:sw=4:et:
diff --git a/requirements.txt b/requirements.txt
index fd7c75d..18f3eb9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 lxml
 PyYAML
 nosexcover
+python-dateutil
diff --git a/scripts/Makefile.am b/scripts/Makefile.am
deleted file mode 100644
index 4045865..0000000
--- a/scripts/Makefile.am
+++ /dev/null
@@ -1,2 +0,0 @@
-SUBDIRS = check-uptime health init add remove
-
diff --git a/scripts/add/add.py b/scripts/add/add.py
index 68859f1..0fab968 100755
--- a/scripts/add/add.py
+++ b/scripts/add/add.py
@@ -21,8 +21,8 @@ def run_collect():
 
 
 def make_opts():
-    from psshlib import api as pssh
-    opts = pssh.Options()
+    import parallax
+    opts = parallax.Options()
     opts.timeout = 60
     opts.recursive = True
     opts.user = 'root'
@@ -34,9 +34,9 @@ def make_opts():
 
 def run_validate():
     try:
-        from psshlib import api
+        import parallax
     except ImportError:
-        crm_script.exit_fail("Command node needs pssh installed")
+        crm_script.exit_fail("Command node needs parallax installed")
 
     if host in add_nodes:
         crm_script.exit_fail("Run script from node in cluster")
@@ -52,10 +52,10 @@ def run_install():
     crm_script.exit_ok(host)
 
 
-def check_results(pssh, results):
+def check_results(parallax, results):
     failures = []
     for host, result in results.items():
-        if isinstance(result, pssh.Error):
+        if isinstance(result, parallax.Error):
             failures.add("%s: %s" % (host, str(result)))
     if failures:
         crm_script.exit_fail(', '.join(failures))
@@ -63,18 +63,18 @@ def check_results(pssh, results):
 
 def run_copy():
     try:
-        from psshlib import api as pssh
+        import parallax
     except ImportError:
-        crm_script.exit_fail("Command node needs pssh installed")
+        crm_script.exit_fail("Command node needs parallax 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)
+        results = parallax.copy(add_nodes, COROSYNC_AUTH, COROSYNC_AUTH, opts)
+        check_results(parallax, results)
+        results = parallax.call(add_nodes,
+                                "chown root:root %s;chmod 400 %s" % (COROSYNC_AUTH, COROSYNC_AUTH),
+                                opts)
+        check_results(parallax, results)
 
     # Add new nodes to corosync.conf before copying
     for node in add_nodes:
@@ -82,8 +82,8 @@ def run_copy():
         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)
+    results = parallax.copy(add_nodes, COROSYNC_CONF, COROSYNC_CONF, opts)
+    check_results(parallax, results)
 
     # reload corosync config here?
     rc, _, err = crm_script.call(['crm', 'corosync', 'reload'])
diff --git a/scripts/add/main.yml b/scripts/add/main.yml
index 28b26ea..cb7b212 100644
--- a/scripts/add/main.yml
+++ b/scripts/add/main.yml
@@ -1,34 +1,39 @@
----
-- 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
+version: 2.2
+category: Script
+shortdesc: Add a new node to an already existing cluster
+longdesc: >
+  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
+    shortdesc: Node to add to the cluster
+    type: string
+    required: true
+
+actions:
+  - shortdesc: Check cluster
+    collect: add.py collect
+
+  - shortdesc: Validate parameters
+    validate: add.py validate
+
+  - shortdesc: Install required packages
+    apply: add.py install
+
+  - shortdesc: Copy configuration files
+    apply_local: add.py copy
+
+  - shortdesc: Configure firewall
+    apply: add.py firewall
+
+  - shortdesc: Start cluster on new node
+    apply: add.py start
 
diff --git a/scripts/apache/main.yml b/scripts/apache/main.yml
new file mode 100644
index 0000000..228c568
--- /dev/null
+++ b/scripts/apache/main.yml
@@ -0,0 +1,68 @@
+# Copyright (C) 2009 Dejan Muhamedagic
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+category: Server
+shortdesc: Apache Webserver
+longdesc: |
+  Configure a resource group containing a virtual IP address and
+  an instance of the Apache web server.
+
+  You can optionally configure a Filesystem resource which will be
+  mounted before the web server is started.
+
+  You can also optionally configure a database resource which will
+  be started before the web server but after mounting the optional
+  filesystem.
+include:
+  - agent: ocf:heartbeat:apache
+    name: apache
+    longdesc: |
+      The Apache configuration file specified here must be available via the
+      same path on all cluster nodes, and Apache must be configured with
+      mod_status enabled.  If in doubt, try running Apache manually via
+      its init script first, and ensure http://localhost:80/server-status is
+      accessible.
+    ops: |
+      op start timeout="40"
+      op stop timeout="60"
+      op monitor interval="10" timeout="20"
+  - script: virtual-ip
+    shortdesc: The IP address configured here will start before the Apache instance.
+    parameters:
+      - name: id
+        value: "{{id}}-vip"
+  - script: filesystem
+    shortdesc: Optional filesystem mounted before the web server is started.
+    required: false
+  - script: database
+    shortdesc: Optional database started before the web server is started.
+    required: false
+parameters:
+  - name: install
+    type: boolean
+    shortdesc: Install and configure apache
+    value: false
+actions:
+  - install:
+      - apache2
+    shortdesc: Install the apache package
+    when: install
+  - service:
+      - apache: disable
+    shortdesc: Let cluster manage apache
+    when: install
+  - call: a2enmod status; true
+    shortdesc: Enable status module
+    when: install
+  - include: filesystem
+  - include: database
+  - include: virtual-ip
+  - include: apache
+  - cib: |
+      group g-{{id}}
+        {{filesystem:id}}
+        {{database:id}}
+        {{virtual-ip:id}}
+        {{id}}
diff --git a/scripts/check-uptime/Makefile.am b/scripts/check-uptime/Makefile.am
deleted file mode 100644
index f4aa605..0000000
--- a/scripts/check-uptime/Makefile.am
+++ /dev/null
@@ -1,28 +0,0 @@
-#
-# 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/main.yml b/scripts/check-uptime/main.yml
index 419d0ad..d37f712 100644
--- a/scripts/check-uptime/main.yml
+++ b/scripts/check-uptime/main.yml
@@ -1,17 +1,19 @@
----
-- name: Check uptime of nodes
-  description: >
-    Fetches the uptime of all nodes and reports which
-    node has lived longest.
+version: 2.2
+category: Script
+shortdesc: Check uptime of nodes
+longdesc: >
+  Fetches the uptime of all nodes and reports which
+  node has lived longest.
 
-  parameters:
-    - name: show_all
-      description: Show all uptimes
-      default: false
+parameters:
+  - name: show_all
+    shortdesc: Show all uptimes
+    type: boolean
+    value: false
 
-  steps:
-    - name: Fetch uptimes
-      collect: fetch.py
+actions:
+  - shortdesc: Fetch uptimes
+    collect: fetch.py
 
-    - name: Report uptime
-      report: report.py
+  - shortdesc: Report uptime
+    report: report.py
diff --git a/scripts/clvm-vg/main.yml b/scripts/clvm-vg/main.yml
new file mode 100644
index 0000000..e092618
--- /dev/null
+++ b/scripts/clvm-vg/main.yml
@@ -0,0 +1,46 @@
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+category: Filesystem
+shortdesc: Cluster-aware LVM (Volume Group)
+longdesc: |
+  Configures an cLVM volume group instance. Once created,
+  this resource is added to the cLVM group resource.
+
+  The cLVM group resource is assumed to be named g-clvm. This
+  is the name of the resource created by the clvm wizard.
+
+parameters:
+  - name: id
+    shortdesc: Volume group instance ID
+    longdesc: Unique ID for the volume group instance in the cluster.
+    required: true
+    unique: true
+    type: resource
+    value: vg1
+
+  - name: volgrpname
+    shortdesc: Volume Group Name
+    longdesc: LVM volume group name.
+    required: true
+    type: string
+    value: vg1
+
+  - name: clvm-group
+    shortdesc: cLVM Resource Group ID
+    longdesc: ID of the cLVM resource group.
+    type: resource
+    required: false
+    value: g-clvm
+
+actions:
+  - cib: |
+      primitive {{id}} ocf:heartbeat:LVM
+        params volgrpname="{{volgrpname}}"
+        op start timeout=60s
+        op stop timeout=60s
+        op monitor interval=30s timeout=60s
+
+  - crm: configure modgroup {{clvm-group}} add {{id}}
+    shortdesc: Add volume group to the cLVM group resource
diff --git a/scripts/clvm/main.yml b/scripts/clvm/main.yml
new file mode 100644
index 0000000..987c47b
--- /dev/null
+++ b/scripts/clvm/main.yml
@@ -0,0 +1,39 @@
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+category: Filesystem
+shortdesc: Cluster-aware LVM
+longdesc: |
+  Configure a cloned instance of cLVM.
+
+  NB: Only one clvm cluster resource is necessary, regardless
+  of how many clustered volume groups are managed as resources.
+  To create volume groups after configuring cLVM, the wizard
+  for cLVM volume groups can be used.
+
+parameters:
+  - name: install
+    type: boolean
+    shortdesc: Install packages for cLVM
+    value: false
+
+actions:
+  - install:
+      - lvm2-clvm
+    shortdesc: Install the clvm package
+    when: install
+  - cib: |
+      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
+
+      group g-clvm dlm clvm
+
+      clone c-clvm g-clvm
+        meta interleave=true ordered=true
diff --git a/scripts/database/main.yml b/scripts/database/main.yml
new file mode 100644
index 0000000..749ede7
--- /dev/null
+++ b/scripts/database/main.yml
@@ -0,0 +1,34 @@
+version: 2.2
+category: Database
+shortdesc: MySQL/MariaDB Database
+longdesc: >
+  Configure a MySQL or MariaDB SQL Database.
+  Enable the install option to install the necessary
+  packages for the database.
+include:
+  - agent: ocf:heartbeat:mysql
+    name: database
+    parameters:
+      - name: test_table
+        value: ""
+    ops: |
+      op start timeout=120s
+      op stop timeout=120s
+      op monitor interval=20s timeout=30s
+
+parameters:
+  - name: install
+    shortdesc: Enable to install required packages
+    type: boolean
+    value: false
+
+actions:
+  - install: mariadb
+    shortdesc: Install packages
+    when: install
+  - service:
+      - name: mysql
+        action: disable
+    shortdesc: Let cluster manage the database
+    when: install
+  - include: database
diff --git a/scripts/db2-hadr/main.yml b/scripts/db2-hadr/main.yml
new file mode 100644
index 0000000..7b404c5
--- /dev/null
+++ b/scripts/db2-hadr/main.yml
@@ -0,0 +1,43 @@
+version: 2.2
+category: Database
+shortdesc: IBM DB2 Database with HADR
+longdesc: >-
+  Configure an IBM DB2 database resource as active/passive HADR,
+  along with a Virtual IP.
+
+include:
+  - agent: ocf:heartbeat:db2
+    parameters:
+      - name: id
+        required: true
+        shortdesc: DB2 Resource ID
+        longdesc: Unique ID for the database resource in the cluster.
+        type: string
+        value: db2-database
+      - name: instance
+        required: true
+        type: string
+        value: db2inst1
+      - name: dblist
+        value: db1
+    ops: |
+      op start interval="0" timeout="130"
+      op stop interval="0" timeout="120"
+      op promote interval="0" timeout="120"
+      op demote interval="0" timeout="120"
+      op monitor interval="30" timeout="60"
+      op monitor interval="45" role="Master" timeout="60"
+
+  - script: virtual-ip
+    shortdesc: The IP address configured here will start before the DB2 instance.
+    parameters:
+      - name: id
+        value: db2-virtual-ip
+actions:
+  - include: virtual-ip
+  - include: db2
+  - cib: |
+      ms ms-{{db2:id}} {{db2:id}}
+        meta target-role=Stopped notify=true
+      colocation {{virtual-ip:id}}-with-master inf: {{virtual-ip:id}}:Started ms-{{db2:id}}:Master
+      order {{virtual-ip:id}}-after-master inf: ms-{{db2:id}}:promote {{virtual-ip:id}}:start
diff --git a/scripts/db2/main.yml b/scripts/db2/main.yml
new file mode 100644
index 0000000..5eb2d92
--- /dev/null
+++ b/scripts/db2/main.yml
@@ -0,0 +1,45 @@
+version: 2.2
+category: Database
+shortdesc: IBM DB2 Database
+longdesc: >-
+  Configure an IBM DB2 database resource, along with a Virtual IP and a Filesystem.
+
+  Note that the filesystem will be stopped initially, in case you need to run mkfs.
+
+include:
+  - agent: ocf:heartbeat:db2
+    parameters:
+      - name: id
+        required: true
+        shortdesc: DB2 Resource ID
+        longdesc: Unique ID for the database resource in the cluster.
+        type: string
+        value: db2-database
+      - name: instance
+        required: true
+        type: string
+        value: db2inst1
+  - script: virtual-ip
+    shortdesc: The IP address configured here will start before the DB2 instance.
+    parameters:
+      - name: id
+        value: db2-virtual-ip
+  - script: filesystem
+    shortdesc: The filesystem configured here will be mounted before the DB2 instance.
+    parameters:
+      - name: id
+        value: db2-fs
+      - name: fstype
+        value: xfs
+      - name: directory
+        value: "/db2/db2inst1"
+actions:
+  - include: virtual-ip
+  - include: filesystem
+  - include: db2
+  - cib: |
+      group g-{{id}}
+        {{virtual-ip:id}}
+        {{filesystem:id}}
+        {{id}}
+        meta target-role=Stopped
diff --git a/scripts/drbd/main.yml b/scripts/drbd/main.yml
new file mode 100644
index 0000000..4e7d4a1
--- /dev/null
+++ b/scripts/drbd/main.yml
@@ -0,0 +1,39 @@
+version: 2.2
+category: Filesystem
+shortdesc: DRBD Block Device
+longdesc: >-
+  Distributed Replicated Block Device. Configure a DRBD cluster resource.
+
+  Also creates a multistate resource managing the state of DRBD.
+
+parameters:
+  - name: id
+    shortdesc: DRBD Cluster Resource ID
+    required: true
+    value: drbd-data
+    type: resource
+  - name: drbd_resource
+    shortdesc: DRBD Resource Name
+    required: true
+    value: drbd0
+    type: string
+  - name: drbdconf
+    value: "/etc/drbd.conf"
+  - name: install
+    type: boolean
+    shortdesc: Install packages for DRBD
+    value: false
+
+actions:
+  - install: drbd drbd-kmp-default
+    shortdesc: Install packages for DRBD
+    when: install
+  - cib: |
+      primitive {{id}} ocf:linbit:drbd
+        params
+          drbd_resource="{{drbd_resource}}"
+          drbdconf="{{drbdconf}}"
+        op monitor interval="29s" role="Master"
+        op monitor interval="31s" role="Slave"
+      ms ms-{{id}} {{id}}
+        meta master-max=1 master-node-max=1 clone-max=2 clone-node-max=1 notify=true
diff --git a/scripts/exportfs/main.yml b/scripts/exportfs/main.yml
new file mode 100644
index 0000000..cd0dfea
--- /dev/null
+++ b/scripts/exportfs/main.yml
@@ -0,0 +1,35 @@
+version: 2.2
+shortdesc: "NFS Exported File System"
+category: Server
+include:
+  - agent: ocf:heartbeat:exportfs
+    parameters:
+      - name: id
+        required: true
+        shortdesc: Unique ID for this export in the cluster.
+        type: resource
+        value: exportfs
+      - name: fsid
+        required: true
+        type: integer
+        value: 1
+      - name: directory
+        required: true
+        type: string
+        shortdesc: Mount point
+        longdesc: "The mount point for the filesystem, e.g.: /srv/nfs/home"
+      - name: options
+        required: true
+        shortdesc: Mount options
+        longdesc: "Any additional options to be given to the mount command, for example rw,mountpoint"
+        type: string
+      - name: wait_for_leasetime_on_stop
+        required: false
+        shortdesc: Wait for lease time on stop
+        longdesc: If set to true, wait for lease on stop.
+        type: boolean
+        value: true
+    ops: |
+      op monitor interval=30s
+actions:
+  - include: exportfs
diff --git a/scripts/filesystem/main.yml b/scripts/filesystem/main.yml
new file mode 100644
index 0000000..23b9479
--- /dev/null
+++ b/scripts/filesystem/main.yml
@@ -0,0 +1,30 @@
+version: 2.2
+category: Filesystem
+shortdesc: Filesystem (mount point)
+include:
+  - agent: ocf:heartbeat:Filesystem
+    name: filesystem
+    parameters:
+      - name: id
+        required: true
+        type: resource
+      - name: device
+        required: true
+        type: string
+      - name: directory
+        required: true
+        type: string
+      - name: fstype
+        required: true
+        type: string
+      - name: options
+        required: false
+        type: string
+    ops: |
+      meta target-role=Stopped
+      op start timeout=60s
+      op stop timeout=60s
+      op monitor interval=20s timeout=40s
+
+actions:
+  - include: filesystem
diff --git a/scripts/gfs2-base/main.yml b/scripts/gfs2-base/main.yml
new file mode 100644
index 0000000..1fc515e
--- /dev/null
+++ b/scripts/gfs2-base/main.yml
@@ -0,0 +1,27 @@
+# Copyright (C) 2009 Andrew Beekhof
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+category: Filesystem
+shortdesc: gfs2 filesystem base (cloned)
+longdesc: |
+  This template generates a cloned instance of the gfs2 filesystem.
+  The filesystem should be on the device, unless clvm is used.
+
+parameters:
+  - name: clvm-group
+    shortdesc: cLVM Resource Group ID
+    longdesc: Optional ID of a cLVM resource group.
+    required: False
+
+actions:
+  - cib: |
+      primitive gfs-controld ocf:pacemaker:controld
+
+      clone c-gfs gfs-controld
+        meta interleave="true" ordered="true"
+
+  - crm: configure modgroup {{clvm-group}} add c-gfs
+    shortdesc: Add gfs controld to cLVM group
+    when: clvm-group
diff --git a/scripts/gfs2/main.yml b/scripts/gfs2/main.yml
new file mode 100644
index 0000000..2df004f
--- /dev/null
+++ b/scripts/gfs2/main.yml
@@ -0,0 +1,51 @@
+# Copyright (C) 2009 Andrew Beekhof
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+shortdesc: gfs2 filesystem (cloned)
+longdesc: >- 
+  This template generates a cloned instance of the gfs2 filesystem.
+  The filesystem should be on the device, unless cLVM is used.
+
+category: Filesystem
+include:
+  - script: gfs2-base
+parameters:
+  - name: id
+    shortdesc: Name the gfs2 filesystem
+    longdesc: "NB: The clone is going to be named c-<id> (e.g. c-bigfs)"
+    example: bigfs
+    required: true
+    type: resource
+  - name: directory
+    shortdesc: The mount point
+    example: /mnt/bigfs
+    required: true
+    type: string
+  - name: device
+    shortdesc: The device
+    required: true
+    type: string
+  - name: options
+    shortdesc: mount options
+    type: string
+    required: false
+actions:
+  - include: gfs2-base
+  - cib: |
+      primitive {{id}} ocf:heartbeat:Filesystem
+        params
+        directory="{{directory}}"
+        fstype="gfs2"
+        device="{{device}}"
+        {{#options}}options="{{options}}"{{/options}}
+
+      monitor {{id}} 20:40
+
+      clone c-{{id}} {{id}}
+        meta interleave="true" ordered="true"
+
+  - crm: "configure modgroup {{gfs2-base:clvm-group}} add c-{{id}}"
+    shortdesc: Add cloned filesystem to cLVM group
+    when: "{{gfs2-base:clvm-group}}"
diff --git a/scripts/haproxy/haproxy.cfg b/scripts/haproxy/haproxy.cfg
new file mode 100644
index 0000000..50141a2
--- /dev/null
+++ b/scripts/haproxy/haproxy.cfg
@@ -0,0 +1,13 @@
+global
+  maxconn 256
+  daemon
+
+defaults
+  mode http
+  timeout connect 5000ms
+  timeout client 50000ms
+  timeout server 50000ms
+
+listen http-in
+  bind 0.0.0.0:80
+  stats enable
diff --git a/scripts/haproxy/main.yml b/scripts/haproxy/main.yml
new file mode 100644
index 0000000..3e784c6
--- /dev/null
+++ b/scripts/haproxy/main.yml
@@ -0,0 +1,37 @@
+version: 2.2
+category: Server
+shortdesc: HAProxy
+longdesc: |
+  HAProxy is a free, very fast and reliable solution offering
+  high availability, load balancing, and proxying for TCP and
+  HTTP-based applications. It is particularly suited for very
+  high traffic web sites and powers quite a number of the
+  world's most visited ones.
+
+  NOTE: Installs a basic haproxy.cfg configuration file.
+  This will overwrite any existing haproxy.cfg.
+
+include:
+  - agent: systemd:haproxy
+    name: haproxy
+    ops: |
+      op monitor interval=10s
+
+parameters:
+  - name: install
+    type: boolean
+    value: false
+    shortdesc: Install and configure HAProxy packages
+
+actions:
+  - install: haproxy
+    nodes: all
+    when: install
+  - service: "haproxy:disable"
+    nodes: all
+    when: install
+  - copy: haproxy.cfg
+    to: /etc/haproxy/haproxy.cfg
+    nodes: all
+    when: install
+  - include: haproxy
diff --git a/scripts/health/Makefile.am b/scripts/health/Makefile.am
deleted file mode 100644
index d683faf..0000000
--- a/scripts/health/Makefile.am
+++ /dev/null
@@ -1,27 +0,0 @@
-#
-# 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
index 8600973..fa5fe8c 100755
--- a/scripts/health/collect.py
+++ b/scripts/health/collect.py
@@ -8,7 +8,7 @@ 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']
+            'pacemaker', 'pacemaker-mgmt', 'resource-agents', 'sbd']
 
 def rpm_info():
     return crm_script.rpmcheck(PACKAGES)
diff --git a/scripts/health/main.yml b/scripts/health/main.yml
index e79c82a..327fa17 100644
--- a/scripts/health/main.yml
+++ b/scripts/health/main.yml
@@ -1,12 +1,11 @@
----
-- 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
+version: 2.2
+category: Script
+shortdesc: Check the health of the cluster
+longdesc: Runs various checks to verify the health of the cluster nodes
+actions:
+  - collect: collect.py
+    shortdesc: Collect cluster information
+  - apply_local: hahealth.py
+    shortdesc: Run HA health check
+  - report: report.py
+    shortdesc: Report cluster state
diff --git a/scripts/init/Makefile.am b/scripts/init/Makefile.am
deleted file mode 100644
index 3e05afb..0000000
--- a/scripts/init/Makefile.am
+++ /dev/null
@@ -1,28 +0,0 @@
-#
-# 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
index d6af222..11bd37a 100755
--- a/scripts/init/authkey.py
+++ b/scripts/init/authkey.py
@@ -12,8 +12,8 @@ COROSYNC_CONF = '/etc/corosync/corosync.conf'
 
 
 def make_opts():
-    from psshlib import api as pssh
-    opts = pssh.Options()
+    import parallax
+    opts = parallax.Options()
     opts.timeout = 60
     opts.recursive = True
     opts.user = 'root'
@@ -23,10 +23,10 @@ def make_opts():
     return opts
 
 
-def check_results(pssh, results):
+def check_results(parallax, results):
     failures = []
     for host, result in results.items():
-        if isinstance(result, pssh.Error):
+        if isinstance(result, parallax.Error):
             failures.add("%s: %s" % (host, str(result)))
     if failures:
         crm_script.exit_fail(', '.join(failures))
@@ -43,16 +43,16 @@ def gen_authkey():
 
 def run_copy():
     try:
-        from psshlib import api as pssh
+        import parallax
     except ImportError:
-        crm_script.exit_fail("Command node needs pssh installed")
+        crm_script.exit_fail("Command node needs parallax installed")
     opts = make_opts()
-    results = pssh.copy(others, COROSYNC_AUTH, COROSYNC_AUTH, opts)
-    check_results(pssh, results)
-    results = pssh.call(others,
+    results = parallax.copy(others, COROSYNC_AUTH, COROSYNC_AUTH, opts)
+    check_results(parallax, results)
+    results = parallax.call(others,
                         "chown root:root %s;chmod 400 %s" % (COROSYNC_AUTH, COROSYNC_AUTH),
                         opts)
-    check_results(pssh, results)
+    check_results(parallax, results)
 
 
 if __name__ == "__main__":
diff --git a/scripts/init/main.yml b/scripts/init/main.yml
index ef5d35b..85b578a 100644
--- a/scripts/init/main.yml
+++ b/scripts/init/main.yml
@@ -1,52 +1,58 @@
----
-- 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
+version: 2.2
+category: Script
+shortdesc: Initialize a new cluster
+longdesc: >
+  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
+    shortdesc: "Use the given interface. Try to auto-detect interface by default."
+    type: string
+    value: ""
+
+  - name: transport
+    shortdesc: "Corosync transport (mcast or udpu)"
+    type: string
+    value: "udpu"
+
+  - name: bindnetaddr
+    shortdesc: "Network address to bind to (e.g.: 192.168.1.0)"
+    type: ip_address
+    value: ""
+
+  - name: mcastaddr
+    shortdesc: "Multicast address (e.g.: 239.x.x.x)"
+    type: ip_address
+    value: ""
+
+  - name: mcastport
+    shortdesc: "Multicast port"
+    type: port
+    value: 5405
+
+actions:
+  - shortdesc: Configure SSH
+    apply_local: configure.py ssh
+
+  - shortdesc: Check state of nodes
+    collect: collect.py
+
+  - shortdesc: Verify parameters
+    validate: verify.py
+
+  - shortdesc: Install packages
+    apply: configure.py install
+
+  - shortdesc: Generate corosync authkey
+    apply_local: authkey.py
+
+  - shortdesc: Configure cluster nodes
+    apply: configure.py corosync
+
+  - shortdesc: Initialize cluster
+    apply_local: init.py
 
diff --git a/scripts/libvirt/main.yml b/scripts/libvirt/main.yml
new file mode 100644
index 0000000..a5b58e0
--- /dev/null
+++ b/scripts/libvirt/main.yml
@@ -0,0 +1,63 @@
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+shortdesc: STONITH for libvirt (kvm / Xen)
+longdesc: >
+  Uses libvirt as a STONITH device to fence a guest node.
+  Create a separate resource for each guest node in the cluster.
+category: Stonith
+parameters:
+  - name: id
+    shortdesc: The resource id (name)
+    example: stonith-libvirt
+    required: true
+    type: resource
+  - name: target
+    shortdesc: Node to manage with stonith device
+    type: resource
+    required: true
+  - name: hostlist
+    shortdesc: "List of controlled hosts: hostname[:domain_id].."
+    longdesc: >
+      The optional domain_id defaults to the hostname.
+    type: string
+    required: true
+  - name: hypervisor_uri
+    longdesc: >
+      URI for connection to the hypervisor.
+      driver[+transport]://[username@][hostlist][:port]/[path][?extraparameters]
+      e.g.
+      qemu+ssh://my_kvm_server.mydomain.my/system   (uses ssh for root)
+      xen://my_kvm_server.mydomain.my/              (uses TLS for client)
+
+      virsh must be installed (e.g. libvirt-client package) and access control must
+      be configured for your selected URI.
+    example: qemu+ssh://my_kvm_server.example.com/system
+    required: true
+  - name: reset_method
+    required: false
+    example: power_cycle
+    type: string
+    shortdesc: How to reset a guest.
+    longdesc: >
+      A guest reset may be done by a sequence of off and on commands
+      (power_cycle) or by the reboot command. Which method works
+      depend on the hypervisor and guest configuration management.
+  - name: install
+    shortdesc: Enable to install required packages
+    type: boolean
+    required: false
+    value: false
+actions:
+  - install: cluster-glue libvirt-client
+    nodes: all
+    when: install
+  - cib: |
+      primitive {{id}}-{{target}} stonith:external/libvirt
+        params
+          hostlist="{{hostlist}}"
+          hypervisor_uri="{{hypervisor_uri}}"
+          {{#reset_method}}reset_method="{{reset_method}}"{{/reset_method}}
+        op start timeout=60s
+      location l-{{id}}-{{target}} {{id}}-{{target}} -inf: {{target}}
diff --git a/scripts/lvm/main.yml b/scripts/lvm/main.yml
new file mode 100644
index 0000000..ecde524
--- /dev/null
+++ b/scripts/lvm/main.yml
@@ -0,0 +1,16 @@
+version: 2.2
+category: Script
+include:
+  - agent: ocf:heartbeat:LVM
+    name: lvm
+    parameters:
+      - name: id
+        required: true
+        value: lvm
+        type: resource
+      - name: volgrpname
+        required: true
+        type: string
+    ops: |
+      op monitor interval=130s timeout=130s
+      op stop timeout=130s on_fail=fence
diff --git a/scripts/mailto/main.yml b/scripts/mailto/main.yml
new file mode 100644
index 0000000..403ffc4
--- /dev/null
+++ b/scripts/mailto/main.yml
@@ -0,0 +1,27 @@
+version: 2.2
+shortdesc: MailTo
+category: Basic
+include:
+  - agent: ocf:heartbeat:MailTo
+    name: mailto
+    parameters:
+      - name: id
+        type: resource
+        required: true
+      - name: email
+        type: email
+        required: true
+      - name: subject
+        type: string
+        required: false
+    ops: |
+      op start timeout="10"
+      op stop timeout="10"
+      op monitor interval="10" timeout="10"
+actions:
+  - install:
+      - mailx
+    shortdesc: Ensure mail package is installed
+  - include: mailto
+  - cib: |
+      clone c-{{id}} {{id}}
diff --git a/scripts/nfsserver/main.yml b/scripts/nfsserver/main.yml
new file mode 100644
index 0000000..2199414
--- /dev/null
+++ b/scripts/nfsserver/main.yml
@@ -0,0 +1,73 @@
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+category: Server
+shortdesc: NFS Server
+longdesc: >
+  Configure an NFS server. Requires an existing filesystem resource,
+  for example a filesystem running on LVM on DRBD.
+
+parameters:
+  - name: base-id
+    required: true
+    shortdesc: Base filesystem resource ID
+    longdesc: The ID of an existing filesystem resource.
+    type: resource
+    value: base-fs
+
+include:
+  - name: rootfs
+    script: exportfs
+    required: false
+    shortdesc: NFSv4 Virtual File System root.
+    parameters:
+      - name: id
+        value: exportfs-root
+      - name: fsid
+        value: 0
+      - name: directory
+        value: /srv/nfs
+      - name: options
+        value: "rw,crossmnt"
+
+  - script: exportfs
+    required: true
+    shortdesc: Exported NFS mount point.
+    parameters:
+      - name: id
+        value: exportfs
+      - name: directory
+        value: /srv/nfs/example
+      - name: options
+        value: "rw,mountpoint"
+      - name: wait_for_leasetime_on_stop
+        value: true
+
+  - script: virtual-ip
+    required: false
+    shortdesc: Configure a Virtual IP address used to access the NFS mounts.
+
+actions:
+  - crm: "configure show {{base-id}}"
+    shortdesc: Ensure that the Filesystem resource exists
+  - install: nfs-client nfs-kernel-server
+    shortdesc: Install NFS packages
+  - service:
+      - nfsserver: enable
+      - nfsserver: start
+  - include: rootfs
+  - include: exportfs
+  - include: virtual-ip
+  - cib: |
+      group g-nfs {{exportfs:id}} {{virtual-ip:id}}
+      order base-then-nfs inf: {{base-id}} g-nfs
+      colocation nfs-with-base inf: g-nfs {{base-id}}
+      {{#rootfs}}
+      clone c-{{rootfs:id}} {{rootfs:id}}
+      order rootfs-before-nfs inf: c-{{rootfs:id}} g-nfs:start
+      colocation nfs-with-rootfs inf: g-nfs c-{{rootfs:id}}
+      {{/rootfs}}
+  - call: exportfs -v
+    error: Failed to configure NFS exportfs
+    shortdesc: Check result of exportfs -v
diff --git a/scripts/ocfs2/main.yml b/scripts/ocfs2/main.yml
new file mode 100644
index 0000000..436bde0
--- /dev/null
+++ b/scripts/ocfs2/main.yml
@@ -0,0 +1,56 @@
+# Copyright (C) 2009 Dejan Muhamedagic
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+category: Filesystem
+shortdesc: OCFS2 filesystem (cloned)
+longdesc: >
+  Configure a cloned cluster resource for an OCFS2 filesystem.
+
+  Note that the OCFS2 Filesystem will be stopped initially, in case
+  you need to run mkfs to create the filesystem after DLM is running.
+
+parameters:
+  - name: id
+    shortdesc: Name the ocfs2 filesystem resource
+    example: bigfs
+    type: resource
+    required: true
+  - name: directory
+    shortdesc: The mount point
+    example: /mnt/bigfs
+    type: string
+    required: true
+  - name: device
+    shortdesc: The device
+    type: string
+    required: true
+  - name: options
+    shortdesc: mount options
+    type: string
+  - name: clvm-group
+    shortdesc: cLVM Resource Group ID
+    longdesc: Optional ID of a cLVM resource group to add this filesystem to.
+    type: resource
+    required: False
+
+actions:
+  - cib: |
+      primitive {{id}} ocf:heartbeat:Filesystem
+          params
+              directory="{{directory}}"
+              fstype="ocfs2"
+              device="{{device}}"
+              {{#options}}options="{{options}}"{{/options}}
+          op start timeout=60s
+          op stop timeout=60s
+          op monitor interval=20s timeout=40s
+
+      clone c-{{id}} {{id}}
+        meta interleave=true target-role=Stopped
+
+  - crm: configure modgroup {{clvm-group}} add c-{{id}}
+    shortdesc: Add cloned OCFS2 filesystem to cLVM group
+    when: clvm-group
+
diff --git a/scripts/oracle/main.yml b/scripts/oracle/main.yml
new file mode 100644
index 0000000..325434d
--- /dev/null
+++ b/scripts/oracle/main.yml
@@ -0,0 +1,51 @@
+version: 2.2
+category: Database
+shortdesc: Oracle Database
+longdesc: Configure an Oracle Database cluster resource.
+parameters:
+  - name: id
+    required: true
+    shortdesc: Resource ID
+    longdesc: Unique ID for the database cluster resource.
+    type: resource
+    value: oracle
+  - name: sid
+    required: true
+    shortdesc: Database SID
+    type: string
+    value: OracleDB
+  - name: listener
+    shortdesc: Listener.
+    required: true
+    type: string
+    value: LISTENER
+  - name: home
+    required: true
+    shortdesc: Database Home.
+    type: string
+    value: /srv/oracledb
+  - name: user
+    required: true
+    shortdesc: Database User.
+    type: string
+    default: oracle
+actions:
+  - cib: |
+      primitive lsn-{{id}} ocf:heartbeat:oralsnr
+        params
+          sid="{{sid}}"
+          home="{{home}}"
+          user="{{user}}"
+          listener="{{listener}}"
+        op monitor interval="30" timeout="60" depth="0"
+
+      primitive {{id}} ocf:heartbeat:oracle
+        params
+          sid="{{sid}}"
+          home="{{home}}"
+          user="{{user}}"
+        op monitor interval="120s"
+
+      colocation lsn-with-{{id}} inf: {{id}} lsn-{{id}}
+      order lsn-before-{{id}} inf: lsn-{{id}} {{id}}
+    
\ No newline at end of file
diff --git a/scripts/raid-lvm/main.yml b/scripts/raid-lvm/main.yml
new file mode 100644
index 0000000..6a02368
--- /dev/null
+++ b/scripts/raid-lvm/main.yml
@@ -0,0 +1,25 @@
+version: 2.2
+category: Filesystem
+shortdesc: RAID hosting LVM
+longdesc: "Configure a RAID 1 host based mirror together with a cluster manager LVM volume group and LVM volumes."
+parameters:
+  - name: id
+    shortdesc: ID for the RAID and LVM group.
+    longdesc: Filesystems that should be mounted in the LVM can be added to this group resource.
+    type: resource
+    value: g-raid
+    required: true
+include:
+  - script: raid1
+    parameters:
+      - name: raidconf
+        value: /etc/mdadm.conf
+        type: string
+      - name: raiddev
+        value: /dev/md0
+        type: string
+  - script: lvm
+actions:
+  - include: lvm
+  - include: raid1
+  - cib: group {{id}} {{raid1:id}} {{lvm:id}} meta target-role=stopped
diff --git a/scripts/raid1/main.yml b/scripts/raid1/main.yml
new file mode 100644
index 0000000..75adffe
--- /dev/null
+++ b/scripts/raid1/main.yml
@@ -0,0 +1,17 @@
+version: 2.2
+category: Script
+include:
+  - agent: ocf:heartbeat:Raid1
+    name: raid1
+    parameters:
+      - name: id
+        required: true
+        value: raid1
+      - name: raidconf
+        required: true
+        type: string
+      - name: raiddev
+        required: true
+        type: string
+    ops: |
+      op monitor interval=60s timeout=130s on_fail=fence
diff --git a/scripts/remove/Makefile.am b/scripts/remove/Makefile.am
deleted file mode 100644
index 969a729..0000000
--- a/scripts/remove/Makefile.am
+++ /dev/null
@@ -1,28 +0,0 @@
-#
-# 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
index 41af2e3..1988be6 100644
--- a/scripts/remove/main.yml
+++ b/scripts/remove/main.yml
@@ -1,20 +1,24 @@
----
-- name: Remove node from cluster
-  description: >
-    Removes the node from the cluster. Resources currently
-    allocated to the node will be moved elsewhere if possible.
+version: 2.2
+category: Script
+shortdesc: Remove node from cluster
+longdesc: >
+  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
+  A more usable interface to this script is provided via
+  the cluster remove command.
 
-    - name: Validate parameters
-      validate: remove.py validate
+parameters:
+  - name: node
+    type: resource
+    shortdesc: Node to remove from the cluster
 
-    - name: Remove node from cluster
-      apply_local: remove.py apply
+actions:
+  - shortdesc: Check nodes
+    collect: remove.py collect
+
+  - shortdesc: Validate parameters
+    validate: remove.py validate
+
+  - shortdesc: Remove node from cluster
+    apply_local: remove.py apply
diff --git a/scripts/sap-as/main.yml b/scripts/sap-as/main.yml
new file mode 100644
index 0000000..08e6084
--- /dev/null
+++ b/scripts/sap-as/main.yml
@@ -0,0 +1,70 @@
+version: 2.2
+category: SAP
+shortdesc: SAP ASCS Instance
+longdesc: |
+  Configure a SAP ASCS instance including:
+
+  1) Virtual IP address for the SAP ASCS instance,
+
+  2) A filesystem on shared storage (/usr/sap/SID/ASCS##),
+
+  3) SAPInstance for ASCS.
+
+parameters:
+  - name: id
+    shortdesc: SAP ASCS Resource Group ID
+    longdesc: Unique ID for the SAP ASCS instance resource group in the cluster.
+    required: true
+    type: resource
+    value: grp_sap_NA0_sapna0as
+
+include:
+  - script: sapinstance
+    required: true
+    parameters:
+      - name: id
+        value: rsc_sapinst_NA0_ASCS00_sapna0as
+      - name: InstanceName
+        value: NA0_ASCS00_sapna0as
+      - name: START_PROFILE
+        value: "/usr/sap/NA0/SYS/profile/START_ASCS00_sapna0as"
+  - script: virtual-ip
+    shortdesc: The Virtual IP address configured here will be for the SAP ASCS instance.
+    required: true
+    parameters:
+      - name: id
+        value: rsc_ip_NA0_sapna0as
+      - name: ip
+        value: 172.17.2.53
+      - name: cidr_netmask
+        value: 24
+      - name: nic
+        value: eth0
+  - script: filesystem
+    shortdesc: "Filesystem resource for the /usr/sap/SID/ASCS## directory."
+    longdesc: >-
+      If a filesystem does not already exist on the block device 
+      specified here, you will need to run mkfs to create it, prior 
+      to starting the filesystem resource.  You will also need
+      to create the mountpoint directory on all cluster nodes.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_sapna0as
+      - name: directory
+        value: "/usr/sap/NA0/ASCS00"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130          
+
+actions:
+  - include: sapinstance
+  - include: virtual-ip
+  - include: filesystem
+  - cib:
+      group {{id}}
+        {{virtual-ip:id}}
+        {{filesystem:id}}
+        {{sapinstance:id}}
+        meta target-role=Stopped
diff --git a/scripts/sap-ci/main.yml b/scripts/sap-ci/main.yml
new file mode 100644
index 0000000..69c4e78
--- /dev/null
+++ b/scripts/sap-ci/main.yml
@@ -0,0 +1,70 @@
+version: 2.2
+category: SAP
+shortdesc: SAP Central Instance
+longdesc: |
+  Configure a SAP Central Instance including:
+
+  1) Virtual IP address for the SAP Central instance,
+
+  2) A filesystem on shared storage (/usr/sap/SID/DVEBMGS##),
+
+  3) SAPInstance for the Central Instance.
+
+parameters:
+  - name: id
+    shortdesc: SAP Central Resource Group ID
+    longdesc: Unique ID for the SAP Central instance resource group in the cluster.
+    required: true
+    type: resource
+    value: grp_sap_NA0_sapna0ci
+
+include:
+  - script: sapinstance
+    required: true
+    parameters:
+      - name: id
+        value: rsc_sapinst_NA0_DVEBMGS01_sapna0ci
+      - name: InstanceName
+        value: NA0_DVEBMGS01_sapna0ci
+      - name: START_PROFILE
+        value: "/usr/sap/NA0/SYS/profile/START_DVEBMGS01_sapna0ci"
+  - script: virtual-ip
+    shortdesc: The Virtual IP address configured here will be for the SAP Central instance.
+    required: true
+    parameters:
+      - name: id
+        value: rsc_ip_NA0_sapna0ci
+      - name: ip
+        value: 172.17.2.55
+      - name: cidr_netmask
+        value: 24
+      - name: nic
+        value: eth0
+  - script: filesystem
+    shortdesc: "Filesystem resource for the /usr/sap/SID/DVEBMGS## directory."
+    longdesc: >-
+      If a filesystem does not already exist on the block device 
+      specified here, you will need to run mkfs to create it, prior 
+      to starting the filesystem resource.  You will also need
+      to create the mountpoint directory on all cluster nodes.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_sapna0ci
+      - name: directory
+        value: "/usr/sap/NA0/DVEBMGS01"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130          
+
+actions:
+  - include: sapinstance
+  - include: virtual-ip
+  - include: filesystem
+  - cib:
+      group {{id}}
+        {{virtual-ip:id}}
+        {{filesystem:id}}
+        {{sapinstance:id}}
+        meta target-role=Stopped
diff --git a/scripts/sap-db/main.yml b/scripts/sap-db/main.yml
new file mode 100644
index 0000000..50ecad8
--- /dev/null
+++ b/scripts/sap-db/main.yml
@@ -0,0 +1,63 @@
+version: 2.2
+category: SAP
+shortdesc: SAP Database Instance
+longdesc: |
+  Configure a SAP database instance including:
+
+  1) A virtual IP address for the SAP database instance,
+
+  2) A filesystem on shared storage (/sapdb),
+
+  3) SAPinstance for the database.
+
+parameters:
+  - name: id
+    shortdesc: SAP Database Resource Group ID
+    longdesc: Unique ID for the SAP Database instance resource group in the cluster.
+    required: true
+    type: resource
+    value: grp_sapdb_NA0
+
+include:
+  - script: sapdb
+    required: true
+  - script: virtual-ip
+    shortdesc: The Virtual IP address configured here will be for the SAP Database instance.
+    required: true
+    parameters:
+      - name: id
+        value: rsc_ip_NA0_sapna0db
+      - name: ip
+        value: 172.17.2.54
+      - name: cidr_netmask
+        value: 24
+      - name: nic
+        value: eth0
+  - script: filesystem
+    shortdesc: "Filesystem resource for the SAP database (typically /sapdb)."
+    longdesc: >-
+      If a filesystem does not already exist on the block device 
+      specified here, you will need to run mkfs to create it, prior 
+      to starting the filesystem resource.  You will also need
+      to create the mountpoint directory on all cluster nodes.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_sapna0db
+      - name: directory
+        value: "/sapdb"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130          
+
+actions:
+  - include: sapdb
+  - include: virtual-ip
+  - include: filesystem
+  - cib:
+      group {{id}}
+        {{virtual-ip:id}}
+        {{filesystem:id}}
+        {{sapdb:id}}
+        meta target-role=Stopped
diff --git a/scripts/sap-simple-stack-plus/main.yml b/scripts/sap-simple-stack-plus/main.yml
new file mode 100644
index 0000000..237f59a
--- /dev/null
+++ b/scripts/sap-simple-stack-plus/main.yml
@@ -0,0 +1,220 @@
+version: 2.2
+category: SAP
+shortdesc: SAP SimpleStack+ Instance
+longdesc: |
+  Configure a SAP instance including:
+
+  1) Virtual IP addresses for each of the SAP instance services - ASCS, DB and CI,
+
+  2) A RAID 1 host based mirror,
+
+  3) A cluster manager LVM volume group and LVM volumes on the RAID 1 host based mirror,
+
+  4) Filesystems on shared storage for sapmnt, /sapbd, /usr/sap/SID/ASCS## and /usr/sap/SID/DVEBMGS##,
+
+  5) SAPinstance for - ASCS, a Database, a Central Instance.
+
+  The difference between this and the SimpleStack is that the ASCS and CI have their own
+  volumes/filesystems/mountpoints rather than just one volume/filesystem/mountpoint on /usr/sap.
+
+parameters:
+  - name: id
+    shortdesc: SAP SimpleStack+ Resource Group ID
+    longdesc: Unique ID for the SAP SimpleStack+ instance resource group in the cluster.
+    required: true
+    type: resource
+    value: grp_sap_NA0
+
+include:
+  - script: raid1
+    required: true
+    parameters:
+      - name: raidconf
+        value: "/etc/mdadm.conf"
+      - name: raiddev
+        value: "/dev/md0"
+
+  - script: lvm
+    required: true
+    shortdesc: LVM logical volumes for the SAP filesystems.
+    parameters:
+      - name: volgrpname
+        value: sapvg
+
+  - script: filesystem
+    name: filesystem-sapmnt
+    required: true
+    shortdesc: Filesystem resource for the sapmnt directory.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_sapmnt
+      - name: directory
+        value: "/sapmnt"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130
+
+  - script: filesystem
+    name: filesystem-usrsap
+    required: true
+    shortdesc: Filesystem resource for the /usr/sap directory.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_usrsap
+      - name: directory
+        value: "/usr/sap"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130
+
+  - script: sapdb
+    required: true
+
+  - script: virtual-ip
+    name: virtual-ip-db
+    shortdesc: The Virtual IP address configured here will be for the SAP Database instance.
+    required: true
+    parameters:
+      - name: id
+        value: rsc_ip_NA0_sapna0db
+      - name: ip
+        value: 172.17.2.54
+      - name: cidr_netmask
+        value: 24
+      - name: nic
+        value: eth0
+  - script: filesystem
+    name: filesystem-db
+    shortdesc: "Filesystem resource for the SAP database (typically /sapdb)."
+    longdesc: >-
+      If a filesystem does not already exist on the block device 
+      specified here, you will need to run mkfs to create it, prior 
+      to starting the filesystem resource.  You will also need
+      to create the mountpoint directory on all cluster nodes.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_sapna0db
+      - name: directory
+        value: "/sapdb"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130          
+
+  - script: sapinstance
+    name: sapinstance-as
+    required: true
+    parameters:
+      - name: id
+        value: rsc_sapinst_NA0_ASCS00_sapna0as
+      - name: InstanceName
+        value: NA0_ASCS00_sapna0as
+      - name: START_PROFILE
+        value: "/usr/sap/NA0/SYS/profile/START_ASCS00_sapna0as"
+  - script: virtual-ip
+    name: virtual-ip-as
+    shortdesc: The Virtual IP address configured here will be for the SAP ASCS instance.
+    required: true
+    parameters:
+      - name: id
+        value: rsc_ip_NA0_sapna0as
+      - name: ip
+        value: 172.17.2.53
+      - name: cidr_netmask
+        value: 24
+      - name: nic
+        value: eth0
+  - script: filesystem
+    name: filesystem-as
+    shortdesc: "Filesystem resource for the /usr/sap/SID/ASCS## directory."
+    longdesc: >-
+      If a filesystem does not already exist on the block device 
+      specified here, you will need to run mkfs to create it, prior 
+      to starting the filesystem resource.  You will also need
+      to create the mountpoint directory on all cluster nodes.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_sapna0as
+      - name: directory
+        value: "/usr/sap/NA0/ASCS00"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130          
+
+  - script: sapinstance
+    name: sapinstance-ci
+    required: true
+    parameters:
+      - name: id
+        value: rsc_sapinst_NA0_DVEBMGS01_sapna0ci
+      - name: InstanceName
+        value: NA0_DVEBMGS01_sapna0ci
+      - name: START_PROFILE
+        value: "/usr/sap/NA0/SYS/profile/START_DVEBMGS01_sapna0ci"
+  - script: virtual-ip
+    name: virtual-ip-ci
+    shortdesc: The Virtual IP address configured here will be for the SAP Central instance.
+    required: true
+    parameters:
+      - name: id
+        value: rsc_ip_NA0_sapna0ci
+      - name: ip
+        value: 172.17.2.55
+      - name: cidr_netmask
+        value: 24
+      - name: nic
+        value: eth0
+  - script: filesystem
+    name: filesystem-ci
+    shortdesc: "Filesystem resource for the /usr/sap/SID/DVEBMGS## directory."
+    longdesc: >-
+      If a filesystem does not already exist on the block device 
+      specified here, you will need to run mkfs to create it, prior 
+      to starting the filesystem resource.  You will also need
+      to create the mountpoint directory on all cluster nodes.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_sapna0ci
+      - name: directory
+        value: "/usr/sap/NA0/DVEBMGS01"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130          
+
+actions:
+  - include: raid1
+  - include: lvm
+  - include: filesystem-sapmnt
+  - include: filesystem-db
+  - include: filesystem-ci
+  - include: filesystem-as
+  - include: virtual-ip-ci
+  - include: virtual-ip-db
+  - include: virtual-ip-as
+  - include: sapdb
+  - include: sapinstance-as
+  - include: sapinstance-ci
+  - cib:
+      group {{id}}
+        {{raid1:id}}
+        {{lvm:id}}
+        {{virtual-ip-db:id}}
+        {{filesystem-sapmnt:id}}
+        {{filesystem-db:id}}
+        {{sapdb:id}}
+        {{virtual-ip-as:id}}
+        {{filesystem-as:id}}
+        {{sapinstance-as:id}}
+        {{virtual-ip-ci:id}}
+        {{filesystem-ci:id}}
+        {{sapinstance-ci:id}}
+        meta target-role=Stopped
diff --git a/scripts/sap-simple-stack/main.yml b/scripts/sap-simple-stack/main.yml
new file mode 100644
index 0000000..a6bf0e2
--- /dev/null
+++ b/scripts/sap-simple-stack/main.yml
@@ -0,0 +1,183 @@
+---
+version: 2.2
+category: SAP
+shortdesc: SAP Simple Stack Instance
+longdesc: |
+  Configure a SAP instance including:
+
+  1) Virtual IP addresses for each of the SAP instance services - ASCS, DB and CI,
+
+  2) A RAID 1 host based mirror,
+
+  3) A cluster manager LVM volume group and LVM volumes on the RAID 1 host based mirror,
+
+  4) Filesystems on shared storage for sapmnt, /sapbd and /usr/sap,
+
+  5) SAPinstance for - ASCS, a Database, a Central Instance.
+
+parameters:
+  - name: id
+    shortdesc: SAP Simple Stack Resource Group ID
+    longdesc: Unique ID for the SAP SimpleStack instance resource group in the cluster.
+    required: true
+    type: resource
+    value: grp_sap_NA0
+
+include:
+  - script: raid1
+    required: true
+    parameters:
+      - name: raidconf
+        value: "/etc/mdadm.conf"
+      - name: raiddev
+        value: "/dev/md0"
+
+  - script: lvm
+    required: true
+    shortdesc: LVM logical volumes for the SAP filesystems.
+    parameters:
+      - name: volgrpname
+        value: sapvg
+
+  - script: filesystem
+    name: filesystem-sapmnt
+    required: true
+    shortdesc: Filesystem resource for the sapmnt directory.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_sapmnt
+      - name: directory
+        value: "/sapmnt"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130
+
+  - script: filesystem
+    name: filesystem-usrsap
+    required: true
+    shortdesc: Filesystem resource for the /usr/sap directory.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_usrsap
+      - name: directory
+        value: "/usr/sap"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130
+
+  - script: sapdb
+    required: true
+
+  - script: virtual-ip
+    name: virtual-ip-db
+    shortdesc: The Virtual IP address configured here will be for the SAP Database instance.
+    required: true
+    parameters:
+      - name: id
+        value: rsc_ip_NA0_sapna0db
+      - name: ip
+        value: 172.17.2.54
+      - name: cidr_netmask
+        value: 24
+      - name: nic
+        value: eth0
+
+  - script: filesystem
+    name: filesystem-db
+    shortdesc: "Filesystem resource for the SAP database (typically /sapdb)."
+    longdesc: >-
+      If a filesystem does not already exist on the block device 
+      specified here, you will need to run mkfs to create it, prior 
+      to starting the filesystem resource.  You will also need
+      to create the mountpoint directory on all cluster nodes.
+    parameters:
+      - name: id
+        value: rsc_fs_NA0_sapna0db
+      - name: directory
+        value: "/sapdb"
+      - name: options
+        value: "noatime,barrier=0,data=writeback"
+    ops: |
+      op stop timeout=300
+      op monitor interval=30 timeout=130          
+
+  - script: sapinstance
+    name: sapinstance-as
+    required: true
+    parameters:
+      - name: id
+        value: rsc_sapinst_NA0_ASCS00_sapna0as
+      - name: InstanceName
+        value: NA0_ASCS00_sapna0as
+      - name: START_PROFILE
+        value: "/usr/sap/NA0/SYS/profile/START_ASCS00_sapna0as"
+
+  - script: virtual-ip
+    name: virtual-ip-as
+    shortdesc: The Virtual IP address configured here will be for the SAP ASCS instance.
+    required: true
+    parameters:
+      - name: id
+        value: rsc_ip_NA0_sapna0as
+      - name: ip
+        value: 172.17.2.53
+      - name: cidr_netmask
+        value: 24
+      - name: nic
+        value: eth0
+
+  - script: sapinstance
+    name: sapinstance-ci
+    required: true
+    parameters:
+      - name: id
+        value: rsc_sapinst_NA0_DVEBMGS01_sapna0ci
+      - name: InstanceName
+        value: NA0_DVEBMGS01_sapna0ci
+      - name: START_PROFILE
+        value: "/usr/sap/NA0/SYS/profile/START_DVEBMGS01_sapna0ci"
+
+  - script: virtual-ip
+    name: virtual-ip-ci
+    shortdesc: The Virtual IP address configured here will be for the SAP Central instance.
+    required: true
+    parameters:
+      - name: id
+        value: rsc_ip_NA0_sapna0ci
+      - name: ip
+        value: 172.17.2.55
+      - name: cidr_netmask
+        value: 24
+      - name: nic
+        value: eth0
+
+actions:
+  - include: raid1
+  - include: lvm
+  - include: filesystem-usrsap
+  - include: filesystem-sapmnt
+  - include: filesystem-db
+  - include: virtual-ip-ci
+  - include: virtual-ip-db
+  - include: virtual-ip-as
+  - include: sapdb
+  - include: sapinstance-as
+  - include: sapinstance-ci
+  - cib:
+      group {{id}}
+        {{raid1:id}}
+        {{lvm:id}}
+        {{virtual-ip-ci:id}}
+        {{virtual-ip-db:id}}
+        {{virtual-ip-as:id}}
+        {{filesystem-usrsap:id}}
+        {{filesystem-sapmnt:id}}
+        {{filesystem-db:id}}
+        {{sapdb:id}}
+        {{sapinstance-as:id}}
+        {{sapinstance-ci:id}}
+        meta target-role=Stopped
diff --git a/scripts/sapdb/main.yml b/scripts/sapdb/main.yml
new file mode 100644
index 0000000..391498e
--- /dev/null
+++ b/scripts/sapdb/main.yml
@@ -0,0 +1,32 @@
+version: 2.2
+category: Script
+shortdesc: SAP Database Instance
+longdesc: Create a single SAP Database Instance.
+
+parameters:
+  - name: id
+    required: true
+    shortdesc: Resource ID
+    longdesc: Unique ID for this SAP instance resource in the cluster.
+    type: resource
+    value: rsc_sabdb_NA0
+  - name: SID
+    required: true
+    shortdesc: Database SID
+    longdesc: The SID for the database.
+    type: string
+    value: NA0
+  - name: DBTYPE
+    required: true
+    shortdesc: Database Type
+    longdesc: The type of database.
+    value: ADA
+    type: string
+
+actions:
+  - cib: |
+      primitive {{id}} ocf:heartbeat:SAPDatabase
+        params SID="{{SID}}" DBTYPE="{{DBTYPE}}"
+        op monitor interval="120" timeout="60" start_delay="180"
+        op start timeout="1800"
+        op stop timeout="1800"
diff --git a/scripts/sapinstance/main.yml b/scripts/sapinstance/main.yml
new file mode 100644
index 0000000..7078fdf
--- /dev/null
+++ b/scripts/sapinstance/main.yml
@@ -0,0 +1,48 @@
+version: 2.2
+category: Script
+shortdesc: SAP Instance
+longdesc: Create a single SAP Instance.
+
+parameters:
+  - name: id
+    required: true
+    shortdesc: Resource ID
+    longdesc: Unique ID for this SAP instance resource in the cluster.
+    type: resource
+    value: sapinstance
+  - name: InstanceName
+    required: true
+    shortdesc: Instance Name
+    longdesc: The name of the SAP instance.
+    type: string
+    value: sapinstance
+  - name: START_PROFILE
+    required: true
+    shortdesc: Start Profile
+    longdesc: This defines the path and the file name of the SAP start profile of this particular instance.
+    type: string
+  - name: AUTOMATIC_RECOVER
+    required: true
+    shortdesc: Automatic Recover
+    longdesc: >-
+      The SAPInstance resource agent tries to recover a failed start
+      attempt automaticaly one time. This is done by killing runing
+      instance processes, removing the kill.sap file and executing
+      cleanipc. Sometimes a crashed SAP instance leaves some
+      processes and/or shared memory segments behind. Setting this
+      option to true will try to remove those leftovers during a
+      start operation. That is to reduce manual work for the
+      administrator.
+    type: boolean
+    value: true
+
+actions:
+  - cib: |
+      primitive {{id}} ocf:heartbeat:SAPInstance
+        params
+          InstanceName="{{InstanceName}}"
+          AUTOMATIC_RECOVER="{{AUTOMATIC_RECOVER}}"
+          START_PROFILE="{{START_PROFILE}}"
+        op monitor interval="180" timeout="60" start_delay="240"
+        op start timeout="240"
+        op stop timeout="240" on_fail="block"
diff --git a/scripts/sbd/main.yml b/scripts/sbd/main.yml
new file mode 100644
index 0000000..f24da70
--- /dev/null
+++ b/scripts/sbd/main.yml
@@ -0,0 +1,44 @@
+# Copyright (C) 2009 Dejan Muhamedagic
+# Copyright (C) 2015 Kristoffer Gronlund
+#
+# License: GNU General Public License (GPL)
+version: 2.2
+category: Stonith
+shortdesc: "SBD, Shared storage based fencing"
+longdesc: |
+  Create a SBD STONITH resource. SBD must be configured to use
+  a particular shared storage device using /etc/sysconfig/sbd.
+
+  You need to configure an SBD resource for each node to manage.
+
+  There is quite a bit more to do to make this stonith operational.
+  See http://www.linux-ha.org/wiki/SBD_Fencing for information, or
+  the sbd(8) manual page.
+
+parameters:
+  - name: id
+    shortdesc: The resource id (name)
+    example: stonith-sbd
+    required: true
+    type: resource
+  - name: node
+    shortdesc: The node id that this stonith resource manages.
+    required: true
+    type: resource
+  - name: sbd_device
+    shortdesc: Name of the device (shared disk)
+    longdesc: >
+      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?
+    type: string
+    required: true
+
+actions:
+  - cib: |
+      primitive {{id}} stonith:external/sbd
+        params sbd_device="{{sbd_device}}"
+        op monitor interval=15s timeout=60s
+        op start timeout=60s
+
+      location loc-{{id}}-fences-{{node}} {{id}} -inf: {{node}}
diff --git a/scripts/virtual-ip/main.yml b/scripts/virtual-ip/main.yml
new file mode 100644
index 0000000..697bf74
--- /dev/null
+++ b/scripts/virtual-ip/main.yml
@@ -0,0 +1,24 @@
+version: 2.2
+shortdesc: Virtual IP
+category: Basic
+include:
+  - agent: ocf:heartbeat:IPaddr2
+    name: virtual-ip
+    parameters:
+      - name: id
+        type: resource
+        required: true
+      - name: ip
+        type: ip_address
+        required: true
+      - name: cidr_netmask
+        type: integer
+        required: false
+      - name: broadcast
+        type: ip_address
+        required: false
+    ops: |
+      op start timeout="20" op stop timeout="20"
+      op monitor interval="10" timeout="20"
+actions:
+  - include: virtual-ip
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..2b4772e
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python
+# Note that this script only installs the python modules,
+# the other parts of crmsh are installed by autotools
+from distutils.core import setup
+import os
+
+SRC_PATH = os.path.relpath(os.path.join(os.path.dirname(__file__), "modules"))
+
+setup(name='crmsh',
+      version='2.2.0-rc3',
+      description='Command-line interface for High-Availability cluster management',
+      author='Dejan Muhamedagic',
+      author_email='dejan at suse.de',
+      url='http://crmsh.github.io/',
+      packages=['crmsh'],
+      package_dir={'crmsh': SRC_PATH})
diff --git a/templates/Makefile.am b/templates/Makefile.am
deleted file mode 100644
index c31ca3c..0000000
--- a/templates/Makefile.am
+++ /dev/null
@@ -1,26 +0,0 @@
-#
-# 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/test/Makefile.am b/test/Makefile.am
deleted file mode 100644
index 0ccc59d..0000000
--- a/test/Makefile.am
+++ /dev/null
@@ -1,28 +0,0 @@
-#
-# 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/bugs-test.txt b/test/bugs-test.txt
new file mode 100644
index 0000000..6d8184d
--- /dev/null
+++ b/test/bugs-test.txt
@@ -0,0 +1,11 @@
+node node1
+primitive st stonith:null params hostlist=node1
+property default-action-timeout=60s
+group g1 gr1 gr2
+group g2 gr3
+group g3 gr4
+primitive gr1 Dummy
+primitive gr2 Dummy
+primitive gr3 Dummy
+primitive gr4 Dummy
+location loc1 g1 rule 200: #uname eq node1
diff --git a/test/cib-tests.sh b/test/cib-tests.sh
index 89f6df5..4df8062 100755
--- a/test/cib-tests.sh
+++ b/test/cib-tests.sh
@@ -1,21 +1,6 @@
 #!/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
-#
+# See COPYING for license information.
 
 BASE=${1:-`pwd`}/cibtests
 AUTOCREATE=1
diff --git a/test/cibtests/002.exp.xml b/test/cibtests/002.exp.xml
index 1c9e497..13c017a 100644
--- a/test/cibtests/002.exp.xml
+++ b/test/cibtests/002.exp.xml
@@ -12,11 +12,9 @@
           <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">
+        <primitive id="testfs" class="ocf" provider="heartbeat" type="Dummy">
           <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"/>
+            <nvpair name="fake" value="1" id="testfs-instance_attributes-fake"/>
           </instance_attributes>
         </primitive>
       </clone>
diff --git a/test/cibtests/002.input b/test/cibtests/002.input
index a832f1b..7fd9acd 100644
--- a/test/cibtests/002.input
+++ b/test/cibtests/002.input
@@ -1,7 +1,7 @@
 configure
 property stonith-enabled=false
-primitive testfs ocf:heartbeat:Filesystem \
-	params directory="/mnt" fstype="ocfs2" device="/dev/sda1"
+primitive testfs ocf:heartbeat:Dummy \
+	params fake=1
 clone testfs-clone testfs \
 	meta ordered="true" interleave="true"
 commit
diff --git a/test/cibtests/003.exp.xml b/test/cibtests/003.exp.xml
index ba1fb6f..70356af 100644
--- a/test/cibtests/003.exp.xml
+++ b/test/cibtests/003.exp.xml
@@ -13,11 +13,9 @@
           <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">
+        <primitive id="testfs" class="ocf" provider="heartbeat" type="Dummy">
           <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"/>
+            <nvpair name="fake" value="2" id="testfs-instance_attributes-fake"/>
           </instance_attributes>
         </primitive>
       </clone>
diff --git a/test/cibtests/003.input b/test/cibtests/003.input
index 129f025..171f1cd 100644
--- a/test/cibtests/003.input
+++ b/test/cibtests/003.input
@@ -1,7 +1,7 @@
 configure
 property stonith-enabled=false
-primitive testfs ocf:heartbeat:Filesystem \
-	params directory="/mnt" fstype="ocfs2" device="/dev/sda1"
+primitive testfs ocf:heartbeat:Dummy \
+	params fake=2
 clone testfs-clone testfs \
 	meta ordered="true" interleave="true"
 commit
diff --git a/test/cibtests/004.exp.xml b/test/cibtests/004.exp.xml
index 1829c6e..2d4c618 100644
--- a/test/cibtests/004.exp.xml
+++ b/test/cibtests/004.exp.xml
@@ -13,11 +13,9 @@
           <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">
+        <primitive id="testfs" class="ocf" provider="heartbeat" type="Dummy">
           <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"/>
+            <nvpair name="fake" value="hello" id="testfs-instance_attributes-fake"/>
           </instance_attributes>
         </primitive>
       </clone>
diff --git a/test/cibtests/004.input b/test/cibtests/004.input
index 8454d5d..86839bc 100644
--- a/test/cibtests/004.input
+++ b/test/cibtests/004.input
@@ -1,7 +1,7 @@
 configure
 property stonith-enabled=false
-primitive testfs ocf:heartbeat:Filesystem \
-	params directory="/mnt" fstype="ocfs2" device="/dev/sda1"
+primitive testfs ocf:heartbeat:Dummy \
+	params fake=hello
 clone testfs-clone testfs \
 	meta ordered="true" interleave="true"
 commit
diff --git a/test/cibtests/Makefile.am b/test/cibtests/Makefile.am
deleted file mode 100644
index 0c288a3..0000000
--- a/test/cibtests/Makefile.am
+++ /dev/null
@@ -1,29 +0,0 @@
-#
-# 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/crm-interface b/test/crm-interface
index 6389e22..702b07b 100644
--- a/test/crm-interface
+++ b/test/crm-interface
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 CIB=__crmsh_regtest
 
diff --git a/test/descriptions b/test/descriptions
index 8c549c1..694a528 100644
--- a/test/descriptions
+++ b/test/descriptions
@@ -1,19 +1,5 @@
 # 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
-#
+# See COPYING for license information.
 
 lead=".TRY"
 describe_show() {
diff --git a/test/evaltest.sh b/test/evaltest.sh
index b715135..e39242c 100755
--- a/test/evaltest.sh
+++ b/test/evaltest.sh
@@ -1,21 +1,6 @@
 #!/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
- #
+# Copyright (C) 2007 Dejan Muhamedagic <dejan at suse.de>
+# See COPYING for license information.
 
 : ${TESTDIR:=testcases}
 : ${CRM:=/usr/sbin/crm}
diff --git a/test/list-undocumented-commands.py b/test/list-undocumented-commands.py
index 0bf04cb..60d37b6 100755
--- a/test/list-undocumented-commands.py
+++ b/test/list-undocumented-commands.py
@@ -13,7 +13,7 @@ if os.path.exists(os.path.join(parent, 'modules')):
 from modules.ui_root import Root
 import modules.help
 
-modules.help.HELP_FILE = "doc/crm.8.txt"
+modules.help.HELP_FILE = "doc/crm.8.adoc"
 modules.help._load_help()
 
 _IGNORED_COMMANDS = ('help', 'quit', 'cd', 'up', 'ls')
diff --git a/test/regression.sh b/test/regression.sh
index a395435..c449bb6 100755
--- a/test/regression.sh
+++ b/test/regression.sh
@@ -1,21 +1,6 @@
 #!/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
- #
+# Copyright (C) 2007 Dejan Muhamedagic <dmuhamedagic at suse.de>
+# See COPYING for license information.
 
 rootdir=`dirname $0`
 TESTDIR=${TESTDIR:-$rootdir/testcases}
@@ -144,6 +129,8 @@ runtestcase() {
 	./evaltest.sh $testargs
 	) < $TESTDIR/$testcase > $outf 2>&1
 
+	perl -pi -e 's/\<cib[^>]*\>/\<cib\>/g' $outf
+
 	filter_output < $outf |
 	if [ "$prepare" ]; then
 		echo " saving to expect file" >&3
diff --git a/test/run b/test/run
new file mode 100755
index 0000000..4a620d9
--- /dev/null
+++ b/test/run
@@ -0,0 +1,13 @@
+#!/bin/sh
+case `pwd` in
+	*/test/unittests)
+		PYTHONPATH=../.. nosetests -w . "$@"
+		;;
+	*/test)
+		PYTHONPATH=.. nosetests -w unittests "$@"
+		;;
+	*)
+		PYTHONPATH=. nosetests -w test/unittests "$@"
+		;;
+esac
+
diff --git a/test/testcases/Makefile.am b/test/testcases/Makefile.am
deleted file mode 100644
index 7cc44ef..0000000
--- a/test/testcases/Makefile.am
+++ /dev/null
@@ -1,36 +0,0 @@
-#
-# 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/basicset b/test/testcases/basicset
index fd0009c..5013205 100644
--- a/test/testcases/basicset
+++ b/test/testcases/basicset
@@ -13,3 +13,5 @@ acl
 history
 newfeatures
 commit
+bugs
+scripts
diff --git a/test/testcases/bugs b/test/testcases/bugs
new file mode 100644
index 0000000..6300623
--- /dev/null
+++ b/test/testcases/bugs
@@ -0,0 +1,42 @@
+session Configuration bugs
+options
+sort_elements false
+up
+configure
+erase
+primitive st stonith:null \
+	params hostlist='node1' \
+	meta description="some description here" \
+	op start requires=nothing \
+	op monitor interval=60m
+primitive p4 Dummy
+primitive p3 Dummy
+primitive p2 Dummy
+primitive p1 Dummy
+colocation c1 inf: p1 p2
+filter "sed 's/p1 p2/& p3/'" c1
+show c1
+delete c1
+colocation c2 inf: [ p1 p2 ] p3 p4
+filter "sed 's/\\\[/\\\(/;s/\\\]/\\\)/'" c2
+show c2
+primitive p5 Dummy
+primitive p6 Dummy
+clone cl-p5 p5
+show
+commit
+_test
+verify
+show
+.
+session Unordered load file
+options
+sort_elements false
+up
+configure
+load update bugs-test.txt
+show
+commit
+_test
+verify
+.
diff --git a/test/testcases/bugs.exp b/test/testcases/bugs.exp
new file mode 100644
index 0000000..9087de2
--- /dev/null
+++ b/test/testcases/bugs.exp
@@ -0,0 +1,98 @@
+.TRY Configuration bugs
+.INP: options
+.INP: sort_elements false
+.INP: up
+.INP: configure
+.INP: erase
+.INP: primitive st stonith:null 	params hostlist='node1' 	meta description="some description here" 	op start requires=nothing 	op monitor interval=60m
+.INP: primitive p4 Dummy
+.INP: primitive p3 Dummy
+.INP: primitive p2 Dummy
+.INP: primitive p1 Dummy
+.INP: colocation c1 inf: p1 p2
+.INP: filter "sed 's/p1 p2/& p3/'" c1
+.INP: show c1
+colocation c1 inf: p1 p2 p3
+.INP: delete c1
+.INP: colocation c2 inf: [ p1 p2 ] p3 p4
+.INP: filter "sed 's/\[/\(/;s/\]/\)/'" c2
+.INP: show c2
+colocation c2 inf: ( p1 p2 ) p3 p4
+.INP: primitive p5 Dummy
+.INP: primitive p6 Dummy
+.INP: clone cl-p5 p5
+.INP: show
+node node1
+primitive st stonith:null \
+	params hostlist=node1 \
+	meta description="some description here" \
+	op start requires=nothing interval=0 \
+	op monitor interval=60m
+primitive p4 Dummy
+primitive p3 Dummy
+primitive p2 Dummy
+primitive p1 Dummy
+primitive p5 Dummy
+primitive p6 Dummy
+clone cl-p5 p5
+colocation c2 inf: ( p1 p2 ) p3 p4
+.INP: commit
+.EXT crm_resource --show-metadata stonith:null
+.EXT stonithd metadata
+.EXT crm_resource --show-metadata ocf:heartbeat:Dummy
+.EXT pengine metadata
+.INP: _test
+.INP: verify
+.INP: show
+node node1
+primitive st stonith:null \
+	params hostlist=node1 \
+	meta description="some description here" \
+	op start requires=nothing interval=0 \
+	op monitor interval=60m
+primitive p4 Dummy
+primitive p3 Dummy
+primitive p2 Dummy
+primitive p1 Dummy
+primitive p6 Dummy
+primitive p5 Dummy
+clone cl-p5 p5
+colocation c2 inf: ( p1 p2 ) p3 p4
+.TRY Unordered load file
+.INP: options
+.INP: sort_elements false
+.INP: up
+.INP: configure
+.INP: load update bugs-test.txt
+.INP: show
+node node1
+primitive st stonith:null \
+	params hostlist=node1
+primitive p4 Dummy
+primitive p3 Dummy
+primitive p2 Dummy
+primitive p1 Dummy
+primitive p6 Dummy
+primitive p5 Dummy
+primitive gr1 Dummy
+primitive gr2 Dummy
+primitive gr3 Dummy
+primitive gr4 Dummy
+group g1 gr1 gr2
+group g2 gr3
+group g3 gr4
+clone cl-p5 p5
+colocation c2 inf: ( p1 p2 ) p3 p4
+location loc1 g1 \
+	rule 200: #uname eq node1
+property cib-bootstrap-options: \
+	default-action-timeout=60s
+.INP: commit
+.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: _test
+.INP: verify
diff --git a/test/testcases/commit.exp b/test/testcases/commit.exp
index f087d04..a57a1db 100644
--- a/test/testcases/commit.exp
+++ b/test/testcases/commit.exp
@@ -27,17 +27,17 @@ ERROR: 7: st: attribute yoyo-meta does not exist
 .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
+INFO: 21: modified colocation:cl1 from p3 to pp3
+INFO: 21: modified location:l1 from p3 to pp3
+INFO: 21: modified order:o1 from p3 to pp3
 .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
+INFO: 23: modified colocation:cl1 from pp3 to p3
+INFO: 23: modified location:l1 from pp3 to p3
+INFO: 23: modified order:o1 from pp3 to p3
 .INP: delete c1
-INFO: 24: resource references in colocation:cl1 updated
-INFO: 24: resource references in order:o1 updated
+INFO: 24: modified colocation:cl1 from c1 to g1
+INFO: 24: modified order:o1 from c1 to g1
 .INP: commit
 .INP: group g2 d1 d2
 .INP: commit
@@ -67,8 +67,8 @@ primitive st stonith:null \
 	op monitor interval=60m
 group g1 d1 p2
 group g2 d3
-location l1 p3 100: node1
 colocation cl1 inf: g1 p3
+location l1 p3 100: node1
 order o1 inf: p3 g1
 property cib-bootstrap-options: \
 	default-action-timeout=2m
diff --git a/test/testcases/common.excl b/test/testcases/common.excl
index 8b14622..aa0a36e 100644
--- a/test/testcases/common.excl
+++ b/test/testcases/common.excl
@@ -18,3 +18,4 @@ Error signing on to the CRMd service
 ^\.EXT crm_diff \-\-no\-version \-o [^ ]+ \-n \-
 ^\.EXT sed ['][^']+
 ^\.EXT sed ["][^"]+
+^\.EXT [a-zA-Z]+ validate-all
diff --git a/test/testcases/confbasic b/test/testcases/confbasic
index 872e9f8..73df58a 100644
--- a/test/testcases/confbasic
+++ b/test/testcases/confbasic
@@ -71,6 +71,7 @@ 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
+set d2.mondelay 45
 _test
 verify
 .
diff --git a/test/testcases/confbasic-xml.exp b/test/testcases/confbasic-xml.exp
index 3b52e6c..bf712ad 100644
--- a/test/testcases/confbasic-xml.exp
+++ b/test/testcases/confbasic-xml.exp
@@ -1,5 +1,5 @@
 <?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">
+<cib>
   <configuration>
     <crm_config>
       <cluster_property_set id="cib-bootstrap-options">
diff --git a/test/testcases/confbasic.exp b/test/testcases/confbasic.exp
index 36680bb..5de8e59 100644
--- a/test/testcases/confbasic.exp
+++ b/test/testcases/confbasic.exp
@@ -47,6 +47,7 @@
 .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: set d2.mondelay 45
 .INP: _test
 .INP: verify
 .EXT crm_resource --show-metadata stonith:ssh
@@ -55,8 +56,9 @@
 .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
+WARNING: 51: c2: resource d1 is grouped, constraints should apply to the group
 .EXT crmd metadata
+.EXT pengine metadata
 .EXT cib metadata
 .INP: show
 node node1 \
@@ -69,7 +71,7 @@ primitive d1 ocf:pacemaker:Dummy \
 	op monitor interval=120m OCF_CHECK_LEVEL=10 \
 	op monitor interval=60s timeout=30s
 primitive d2 Delay \
-	params mondelay=60 \
+	params mondelay=45 \
 	op start timeout=60s interval=0 \
 	op stop timeout=60s interval=0 \
 	op monitor role=Started interval=60s timeout=30s
@@ -95,6 +97,9 @@ ms m5 s5
 ms m6 s6
 clone c d3 \
 	meta clone-max=1
+tag t1 m5 m6
+colocation c1 inf: m6 m5
+colocation c2 inf: m5:Master d1:Started
 location l1 g1 100: node1
 location l2 c \
 	rule $id=l2-rule1 100: #uname eq node1
@@ -110,16 +115,14 @@ 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
+fencing_topology st st2
 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: \
@@ -129,5 +132,5 @@ rsc_defaults rsc-options: \
 op_defaults opsdef2: \
 	rule 100: #uname eq node1 \
 	record-pending=true
-tag t1: m5 m6
 .INP: commit
+WARNING: 53: c2: resource d1 is grouped, constraints should apply to the group
diff --git a/test/testcases/delete.exp b/test/testcases/delete.exp
index 47f7153..2cd1a26 100644
--- a/test/testcases/delete.exp
+++ b/test/testcases/delete.exp
@@ -21,7 +21,7 @@ primitive st stonith:ssh \
 location d1-pref d1 100: node1
 .INP: _test
 .INP: rename d1 p1
-INFO: 13: resource references in location:d1-pref updated
+INFO: 13: modified location:d1-pref from d1 to p1
 .INP: show
 node node1
 primitive d2 ocf:pacemaker:Dummy
@@ -62,7 +62,7 @@ primitive st stonith:ssh \
 .INP: primitive d2 ocf:pacemaker:Dummy
 .INP: _test
 .INP: group g1 d2 d1
-INFO: 29: resource references in location:d1-pref updated
+INFO: 29: modified location:d1-pref from d1 to g1
 .INP: delete d2
 .INP: show
 node node1
@@ -76,7 +76,7 @@ group g1 d1
 location d1-pref g1 100: node1
 .INP: _test
 .INP: delete g1
-INFO: 33: resource references in location:d1-pref updated
+INFO: 33: modified location:d1-pref from g1 to d1
 .INP: show
 node node1
 primitive d1 ocf:pacemaker:Dummy
@@ -94,12 +94,12 @@ location d1-pref d1 100: node1
 .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
+INFO: 38: modified location:d1-pref from d1 to g1
 .INP: clone c1 g1
-INFO: 39: resource references in location:d1-pref updated
+INFO: 39: modified location:d1-pref from g1 to c1
 .INP: delete g1
-INFO: 40: resource references in location:d1-pref updated
-INFO: 40: resource references in location:d1-pref updated
+INFO: 40: modified location:d1-pref from c1 to g1
+INFO: 40: modified location:d1-pref from g1 to d2
 .INP: show
 node node1
 primitive d1 ocf:pacemaker:Dummy
@@ -112,14 +112,14 @@ primitive st stonith:ssh \
 location d1-pref d2 100: node1
 .INP: _test
 .INP: group g1 d2 d1
-INFO: 43: resource references in location:d1-pref updated
+INFO: 43: modified location:d1-pref from d2 to g1
 .INP: clone c1 g1
-INFO: 44: resource references in location:d1-pref updated
+INFO: 44: modified location:d1-pref from g1 to c1
 .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
+INFO: 47: modified location:d1-pref from c1 to g1
+INFO: 47: modified location:d1-pref from g1 to d2
 .INP: show
 node node1
 primitive d1 ocf:pacemaker:Dummy
@@ -132,13 +132,13 @@ primitive st stonith:ssh \
 location d1-pref d2 100: node1
 .INP: _test
 .INP: group g1 d2 d1
-INFO: 50: resource references in location:d1-pref updated
+INFO: 50: modified location:d1-pref from d2 to g1
 .INP: clone c1 g1
-INFO: 51: resource references in location:d1-pref updated
+INFO: 51: modified location:d1-pref from g1 to c1
 .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: modified location:d1-pref from c1 to g1
+INFO: 53: modified location:d1-pref from g1 to d2
 INFO: 53: hanging location:d1-pref deleted
 .INP: show
 node node1
diff --git a/test/testcases/edit b/test/testcases/edit
index 2e2df15..093f4d5 100644
--- a/test/testcases/edit
+++ b/test/testcases/edit
@@ -12,7 +12,9 @@ primitive p1 ocf:heartbeat:Dummy \
 	op monitor interval=120m OCF_CHECK_LEVEL=10
 filter "sed '$aprimitive p2 ocf:heartbeat:Dummy'"
 filter "sed '$agroup g1 p1 p2'"
+show
 filter "sed 's/p2/p3/;$aprimitive p3 ocf:heartbeat:Dummy'" g1
+show
 filter "sed '$aclone c1 p2'"
 filter "sed 's/p2/g1/'" c1
 filter "sed '/clone/s/g1/p2/'" c1 g1
@@ -47,13 +49,20 @@ modgroup g1 remove nosuch
 modgroup g1 add c1
 modgroup g1 add nosuch
 filter "sed 's/^/# this is a comment\\n/'" loc-d1
+rsc_defaults $id="rsc_options" failure-timeout=10m
+filter "sed 's/2m/60s/'" cib-bootstrap-options
+show rsc_options
+property stonith-enabled=true
+show cib-bootstrap-options
+filter 'sed "s/stonith-enabled=true//"'
+show cib-bootstrap-options
+primitive d4 ocf:heartbeat:Dummy
+primitive d5 ocf:heartbeat:Dummy
+primitive d6 ocf:heartbeat:Dummy
+order o-d456 d4 d5 d6
+tag t-d45: d4 d5
+show type:order
+show related:d4
 _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.exp b/test/testcases/edit.exp
index b0b1e37..367e61a 100644
--- a/test/testcases/edit.exp
+++ b/test/testcases/edit.exp
@@ -9,7 +9,38 @@
 .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: show
+node node1 \
+	attributes mem=16G
+primitive p1 Dummy \
+	op monitor interval=60m \
+	op monitor interval=120m OCF_CHECK_LEVEL=10
+primitive p2 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
+property cib-bootstrap-options: \
+	default-action-timeout=2m
 .INP: filter "sed 's/p2/p3/;$aprimitive p3 ocf:heartbeat:Dummy'" g1
+.INP: show
+node node1 \
+	attributes mem=16G
+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 p3
+property cib-bootstrap-options: \
+	default-action-timeout=2m
 .INP: filter "sed '$aclone c1 p2'"
 .INP: filter "sed 's/p2/g1/'" c1
 .INP: filter "sed '/clone/s/g1/p2/'" c1 g1
@@ -26,7 +57,7 @@
 .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
+ERROR: 29: 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
@@ -41,16 +72,42 @@ ERROR: 27: Cannot create group:g1: Child primitive:d1 already in group:g2
 .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
+ERROR: 42: configure.modgroup: st is not member of g1
 .INP: modgroup g1 remove c1
-ERROR: 41: configure.modgroup: c1 is not member of g1
+ERROR: 43: configure.modgroup: c1 is not member of g1
 .INP: modgroup g1 remove nosuch
-ERROR: 42: configure.modgroup: nosuch is not member of g1
+ERROR: 44: configure.modgroup: nosuch is not member of g1
 .INP: modgroup g1 add c1
-ERROR: 43: a group may contain only primitives; c1 is clone
+ERROR: 45: a group may contain only primitives; c1 is clone
 .INP: modgroup g1 add nosuch
-ERROR: 44: g1 refers to missing object nosuch
+ERROR: 46: g1 refers to missing object nosuch
 .INP: filter "sed 's/^/# this is a comment\n/'" loc-d1
+.INP: rsc_defaults $id="rsc_options" failure-timeout=10m
+.INP: filter "sed 's/2m/60s/'" cib-bootstrap-options
+.INP: show rsc_options
+rsc_defaults rsc_options: \
+	failure-timeout=10m
+.INP: property stonith-enabled=true
+.INP: show cib-bootstrap-options
+property cib-bootstrap-options: \
+	default-action-timeout=60s \
+	stonith-enabled=true
+.INP: filter 'sed "s/stonith-enabled=true//"'
+.INP: show cib-bootstrap-options
+property cib-bootstrap-options: \
+	default-action-timeout=60s
+.INP: primitive d4 ocf:heartbeat:Dummy
+.INP: primitive d5 ocf:heartbeat:Dummy
+.INP: primitive d6 ocf:heartbeat:Dummy
+.INP: order o-d456 d4 d5 d6
+.INP: tag t-d45: d4 d5
+.INP: show type:order
+order o-d456 d4 d5 d6
+order o1 inf: p3 c1
+.INP: show related:d4
+primitive d4 Dummy
+tag t-d45 d4 d5
+order o-d456 d4 d5 d6
 .INP: _test
 .INP: verify
 .EXT crm_resource --show-metadata stonith:null
@@ -65,6 +122,9 @@ node node1 \
 primitive d1 Dummy
 primitive d2 Dummy
 primitive d3 Dummy
+primitive d4 Dummy
+primitive d5 Dummy
+primitive d6 Dummy
 primitive p1 Dummy \
 	op monitor interval=60m \
 	op monitor interval=120m OCF_CHECK_LEVEL=10
@@ -78,36 +138,17 @@ primitive st stonith:null \
 group g1 p1 p2 d3
 group g2 d1 d2
 clone c1 g1
+tag t-d45 d4 d5
 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 o-d456 d4 d5 d6
 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
+	default-action-timeout=60s
 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
+.INP: commit
diff --git a/test/testcases/history.exp b/test/testcases/history.exp
index 95fdeda..de3e13f 100644
--- a/test/testcases/history.exp
+++ b/test/testcases/history.exp
@@ -2,8 +2,8 @@
 .INP: history
 .INP: source history-test.tar.bz2
 .INP: info
+.EXT tar -tj < history-test.tar.bz2 2> /dev/null | head -1
 .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
@@ -11,7 +11,7 @@ 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
+Transitions: 43* 44 45 46 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
@@ -30,6 +30,8 @@ Dec 14 20:08:40 xen-e corosync[24218]:  [pcmk  ] info: pcmk_peer_update: memb: x
 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:06:57 xen-d crmd: [5660]: info: do_election_count_vote: Election 2 (owner: xen-e) lost: vote from xen-e (Uptime)
+Dec 14 20:07:19 xen-d crmd: [5660]: info: do_election_count_vote: Election 3 (owner: xen-e) lost: vote from xen-e (Uptime)
 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
@@ -183,7 +185,6 @@ 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
@@ -195,7 +196,6 @@ Date       Start    End       Filename      Client     User       Origin
 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
@@ -217,7 +217,6 @@ 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
@@ -270,7 +269,6 @@ 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
@@ -282,7 +280,7 @@ Report saved in '/root/_crmsh_regtest.tar.bz2'
 .TRY History 2
 .INP: history
 .INP: session load _crmsh_regtest
+.EXT tar -tj < history-test.tar.bz2 2> /dev/null | head -1
 .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/newfeatures b/test/testcases/newfeatures
index 2262a37..5985881 100644
--- a/test/testcases/newfeatures
+++ b/test/testcases/newfeatures
@@ -17,7 +17,9 @@ primitive p1 Dummy params \
 primitive p2 Dummy params @p0-state
 property rule #uname eq node1 stonith-enabled=no
 tag tag1: p0 p1 p2
+tag tag2 p0 p1 p2
 location l1 { p0 p1 p2 } inf: node1
+primitive node1 Dummy
 show
 _test
 verify
diff --git a/test/testcases/newfeatures.exp b/test/testcases/newfeatures.exp
index 8e84129..7e38cae 100644
--- a/test/testcases/newfeatures.exp
+++ b/test/testcases/newfeatures.exp
@@ -9,13 +9,14 @@
 .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: tag tag2 p0 p1 p2
 .INP: location l1 { p0 p1 p2 } inf: node1
+.INP: primitive node1 Dummy
 .INP: show
 node node1
+primitive node1 Dummy
 primitive p0 Dummy \
 	params state=1
 primitive p1 Dummy \
@@ -27,18 +28,18 @@ primitive st stonith:ssh \
 	meta target-role=Started \
 	op start requires=nothing timeout=60s interval=0 \
 	op monitor interval=60m timeout=60s
+tag tag1 p0 p1 p2
+tag tag2 p0 p1 p2
 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 pengine metadata
 .EXT cib metadata
 .INP: commit
-nvpair_ref: 'p0-state' None
diff --git a/test/testcases/node.exp b/test/testcases/node.exp
index f3be5e8..740d657 100644
--- a/test/testcases/node.exp
+++ b/test/testcases/node.exp
@@ -9,7 +9,7 @@ node1: normal
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes>
@@ -30,7 +30,7 @@ node1: normal
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes>
@@ -51,7 +51,7 @@ node1: normal
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes>
@@ -73,7 +73,7 @@ node1: normal
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes>
@@ -90,12 +90,12 @@ node1: normal
 </cib>
 
 .TRY node attribute node1 set a1 "1 2 3"
-.EXT crm_attribute -t nodes -U 'node1' -n 'a1' -v '1 2 3'
+.EXT crm_attribute -t nodes -N '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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes>
@@ -113,13 +113,13 @@ node1: normal
 </cib>
 
 .TRY node attribute node1 show a1
-.EXT crm_attribute -G -t nodes -U 'node1' -n 'a1'
+.EXT crm_attribute -G -t nodes -N '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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes>
@@ -137,14 +137,14 @@ scope=nodes  name=a1 value=1 2 3
 </cib>
 
 .TRY node attribute node1 delete a1
-.EXT crm_attribute -D -t nodes -U 'node1' -n 'a1'
+.EXT crm_attribute -D -t nodes -N '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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes>
diff --git a/test/testcases/ra.exp b/test/testcases/ra.exp
index c6999d8..59318f7 100644
--- a/test/testcases/ra.exp
+++ b/test/testcases/ra.exp
@@ -25,6 +25,9 @@ Parameters (*: required, []: default):
 state (string, [/var/run//Dummy-{OCF_RESOURCE_INSTANCE}.state]): State file
     Location to store the resource state in.
 
+passwd (string): Password
+    Fake password field
+
 fake (string, [dummy]): 
     Fake attribute that can be changed to cause a reload
 
@@ -61,9 +64,6 @@ livedangerously (enum): Live Dangerously!!
     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.
@@ -77,6 +77,10 @@ pcmk_host_list (string): A list of machines controlled by this device (Optional
 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_delay_max (time, [0s]): Enable random delay for stonith actions and specify the maximum of random delay
+    This prevents double fencing when using slow devices such as sbd.
+    Use this to enable random delay for stonith actions and specify the maximum of random delay.
+
 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.
diff --git a/test/testcases/resource b/test/testcases/resource
index e094a5a..7cb1480 100644
--- a/test/testcases/resource
+++ b/test/testcases/resource
@@ -20,6 +20,12 @@ resource param p0 delete a0
 resource meta p0 set m0 123
 resource meta p0 show m0
 resource meta p0 delete m0
+resource trace p0 probe
+resource trace p0 start
+resource trace p0 stop
+resource untrace p0 probe
+resource untrace p0 start
+resource untrace p0 stop
 configure group g p0 p3
 options manage-children never
 resource start g
diff --git a/test/testcases/resource.exp b/test/testcases/resource.exp
index e052132..d8cca97 100644
--- a/test/testcases/resource.exp
+++ b/test/testcases/resource.exp
@@ -1,5 +1,5 @@
 .TRY resource status p0
-.EXT crm_resource -W -r 'p0'
+.EXT crm_resource --locate -r 'p0'
 resource p0 is NOT running
 .SETENV showobj=p3
 .TRY resource start p3
@@ -7,7 +7,7 @@ resource p0 is NOT running
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -27,7 +27,7 @@ resource p0 is NOT running
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -48,7 +48,7 @@ resource p0 is NOT running
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -69,7 +69,7 @@ resource p0 is NOT running
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -87,12 +87,12 @@ resource p0 is NOT running
 
 .SETENV showobj=cli-prefer-p3
 .TRY resource migrate p3 node1
-.EXT crm_resource -M -r 'p3' --node='node1'
+.EXT crm_resource --quiet --move -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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -105,15 +105,15 @@ resource p0 is NOT running
 
 .SETENV showobj=
 .TRY resource unmigrate p3
-.EXT crm_resource -U -r 'p3'
+.EXT crm_resource --quiet --clear -r 'p3'
 .SETENV showobj=cli-prefer-p3
 .TRY resource migrate p3 node1 force
-.EXT crm_resource -M -r 'p3' --node='node1' --force
+.EXT crm_resource --quiet --move -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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -126,15 +126,17 @@ resource p0 is NOT running
 
 .SETENV showobj=
 .TRY resource unmigrate p3
-.EXT crm_resource -U -r 'p3'
+.EXT crm_resource --quiet --clear -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'
+
+Set 'p0' option: id=p0-instance_attributes-a0 set=p0-instance_attributes name=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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -156,7 +158,7 @@ resource p0 is NOT running
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -173,12 +175,12 @@ resource p0 is NOT running
 
 .TRY resource param p0 delete a0
 .EXT crm_resource -r 'p0' -d 'a0'
-Deleted p0 option: id=p0-instance_attributes-a0 name=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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -193,11 +195,13 @@ Deleted p0 option: id=p0-instance_attributes-a0 name=a0
 
 .TRY resource meta p0 set m0 123
 .EXT crm_resource --meta -r 'p0' -p 'm0' -v '123'
+
+Set 'p0' option: id=p0-meta_attributes-m0 set=p0-meta_attributes name=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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -220,7 +224,7 @@ Deleted p0 option: id=p0-instance_attributes-a0 name=a0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -238,12 +242,187 @@ Deleted p0 option: id=p0-instance_attributes-a0 name=a0
 
 .TRY resource meta p0 delete m0
 .EXT crm_resource --meta -r 'p0' -d 'm0'
-Deleted p0 option: id=p0-meta_attributes-m0 name=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>
+  <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 resource trace p0 probe
+INFO: Trace for p0:monitor is written to /var/lib/heartbeat/trace_ra/
+INFO: Trace set, restart p0 to trace non-monitor operations
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib>
+  <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"/>
+        <operations>
+          <op name="monitor" interval="0" id="p0-monitor-0">
+            <instance_attributes id="p0-monitor-0-instance_attributes">
+              <nvpair name="trace_ra" value="1" id="p0-monitor-0-instance_attributes-trace_ra"/>
+            </instance_attributes>
+          </op>
+        </operations>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource trace p0 start
+INFO: Trace for p0:start is written to /var/lib/heartbeat/trace_ra/
+INFO: Trace set, restart p0 to trace the start operation
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib>
+  <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"/>
+        <operations>
+          <op name="monitor" interval="0" id="p0-monitor-0">
+            <instance_attributes id="p0-monitor-0-instance_attributes">
+              <nvpair name="trace_ra" value="1" id="p0-monitor-0-instance_attributes-trace_ra"/>
+            </instance_attributes>
+          </op>
+          <op name="start" interval="0" id="p0-start-0">
+            <instance_attributes id="p0-start-0-instance_attributes">
+              <nvpair name="trace_ra" value="1" id="p0-start-0-instance_attributes-trace_ra"/>
+            </instance_attributes>
+          </op>
+        </operations>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource trace p0 stop
+INFO: Trace for p0:stop is written to /var/lib/heartbeat/trace_ra/
+INFO: Trace set, restart p0 to trace the stop operation
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib>
+  <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"/>
+        <operations>
+          <op name="monitor" interval="0" id="p0-monitor-0">
+            <instance_attributes id="p0-monitor-0-instance_attributes">
+              <nvpair name="trace_ra" value="1" id="p0-monitor-0-instance_attributes-trace_ra"/>
+            </instance_attributes>
+          </op>
+          <op name="start" interval="0" id="p0-start-0">
+            <instance_attributes id="p0-start-0-instance_attributes">
+              <nvpair name="trace_ra" value="1" id="p0-start-0-instance_attributes-trace_ra"/>
+            </instance_attributes>
+          </op>
+          <op name="stop" interval="0" id="p0-stop-0">
+            <instance_attributes id="p0-stop-0-instance_attributes">
+              <nvpair name="trace_ra" value="1" id="p0-stop-0-instance_attributes-trace_ra"/>
+            </instance_attributes>
+          </op>
+        </operations>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource untrace p0 probe
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib>
+  <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"/>
+        <operations>
+          <op name="start" interval="0" id="p0-start-0">
+            <instance_attributes id="p0-start-0-instance_attributes">
+              <nvpair name="trace_ra" value="1" id="p0-start-0-instance_attributes-trace_ra"/>
+            </instance_attributes>
+          </op>
+          <op name="stop" interval="0" id="p0-stop-0">
+            <instance_attributes id="p0-stop-0-instance_attributes">
+              <nvpair name="trace_ra" value="1" id="p0-stop-0-instance_attributes-trace_ra"/>
+            </instance_attributes>
+          </op>
+        </operations>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource untrace p0 start
+.INP: configure
+.INP: _regtest on
+.INP: show xml p0
+<?xml version="1.0" ?>
+<cib>
+  <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"/>
+        <operations>
+          <op name="stop" interval="0" id="p0-stop-0">
+            <instance_attributes id="p0-stop-0-instance_attributes">
+              <nvpair name="trace_ra" value="1" id="p0-stop-0-instance_attributes-trace_ra"/>
+            </instance_attributes>
+          </op>
+        </operations>
+      </primitive>
+    </resources>
+    <constraints/>
+  </configuration>
+</cib>
+
+.TRY resource untrace p0 stop
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -262,7 +441,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -288,7 +467,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -314,7 +493,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -340,7 +519,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -370,7 +549,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -400,7 +579,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -432,7 +611,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -464,7 +643,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -492,7 +671,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -516,7 +695,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -540,7 +719,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -568,7 +747,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -592,7 +771,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -616,7 +795,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -648,7 +827,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -681,7 +860,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
@@ -709,7 +888,7 @@ Deleted p0 option: id=p0-meta_attributes-m0 name=m0
 .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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes/>
diff --git a/test/testcases/rset-xml.exp b/test/testcases/rset-xml.exp
index 740fc6c..ffd1f45 100644
--- a/test/testcases/rset-xml.exp
+++ b/test/testcases/rset-xml.exp
@@ -1,5 +1,5 @@
 <?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">
+<cib>
   <configuration>
     <crm_config/>
     <nodes>
diff --git a/test/testcases/scripts b/test/testcases/scripts
new file mode 100644
index 0000000..6f350a3
--- /dev/null
+++ b/test/testcases/scripts
@@ -0,0 +1,12 @@
+session Cluster scripts
+script
+list
+list all
+list names
+list names all
+list all names
+list bogus
+show mailto
+verify mailto id=foo email=test at example.com subject=hello
+run mailto id=foo email=test at example.com subject=hello nodes=node1 dry_run=true
+.
diff --git a/test/testcases/scripts.exp b/test/testcases/scripts.exp
new file mode 100644
index 0000000..be4a6d3
--- /dev/null
+++ b/test/testcases/scripts.exp
@@ -0,0 +1,273 @@
+.TRY Cluster scripts
+.INP: script
+.INP: list
+.EXT crm_resource --show-metadata ocf:heartbeat:apache
+.EXT crm_resource --show-metadata ocf:heartbeat:IPaddr2
+.EXT crm_resource --show-metadata ocf:heartbeat:Filesystem
+.EXT crm_resource --show-metadata ocf:heartbeat:mysql
+.EXT crm_resource --show-metadata ocf:heartbeat:db2
+.EXT crm_resource --show-metadata ocf:heartbeat:exportfs
+.EXT crm_resource --show-metadata systemd:haproxy
+ERROR: 2: Error when loading script haproxy: No meta-data for agent: systemd:haproxy
+.EXT crm_resource --show-metadata ocf:heartbeat:LVM
+.EXT crm_resource --show-metadata ocf:heartbeat:MailTo
+.EXT crm_resource --show-metadata ocf:heartbeat:Raid1
+Basic:
+
+mailto           MailTo
+virtual-ip       Virtual IP
+
+Database:
+
+database         MySQL/MariaDB Database
+db2              IBM DB2 Database
+db2-hadr         IBM DB2 Database with HADR
+oracle           Oracle Database
+
+Filesystem:
+
+clvm             Cluster-aware LVM
+clvm-vg          Cluster-aware LVM (Volume Group)
+drbd             DRBD Block Device
+filesystem       Filesystem (mount point)
+gfs2             gfs2 filesystem (cloned)
+gfs2-base        gfs2 filesystem base (cloned)
+ocfs2            OCFS2 filesystem (cloned)
+raid-lvm         RAID hosting LVM
+
+SAP:
+
+sap-as           SAP ASCS Instance
+sap-ci           SAP Central Instance
+sap-db           SAP Database Instance
+sap-simple-stack SAP Simple Stack Instance
+sap-simple-stack-plus SAP SimpleStack+ Instance
+
+Server:
+
+apache           Apache Webserver
+exportfs         NFS Exported File System
+nfsserver        NFS Server
+
+Stonith:
+
+libvirt          STONITH for libvirt (kvm / Xen)
+sbd              SBD, Shared storage based fencing
+
+.INP: list all
+.EXT crm_resource --show-metadata systemd:haproxy
+ERROR: 3: Error when loading script haproxy: No meta-data for agent: systemd:haproxy
+Basic:
+
+mailto           MailTo
+virtual-ip       Virtual IP
+
+Database:
+
+database         MySQL/MariaDB Database
+db2              IBM DB2 Database
+db2-hadr         IBM DB2 Database with HADR
+oracle           Oracle Database
+
+Filesystem:
+
+clvm             Cluster-aware LVM
+clvm-vg          Cluster-aware LVM (Volume Group)
+drbd             DRBD Block Device
+filesystem       Filesystem (mount point)
+gfs2             gfs2 filesystem (cloned)
+gfs2-base        gfs2 filesystem base (cloned)
+ocfs2            OCFS2 filesystem (cloned)
+raid-lvm         RAID hosting LVM
+
+SAP:
+
+sap-as           SAP ASCS Instance
+sap-ci           SAP Central Instance
+sap-db           SAP Database Instance
+sap-simple-stack SAP Simple Stack Instance
+sap-simple-stack-plus SAP SimpleStack+ Instance
+
+Script:
+
+add              Add a new node to an already existing cluster
+check-uptime     Check uptime of nodes
+health           Check the health of the cluster
+init             Initialize a new cluster
+lvm              Controls the availability of an LVM Volume Group
+raid1            Manages Linux software RAID (MD) devices on shared storage
+remove           Remove node from cluster
+sapdb            SAP Database Instance
+sapinstance      SAP Instance
+
+Server:
+
+apache           Apache Webserver
+exportfs         NFS Exported File System
+nfsserver        NFS Server
+
+Stonith:
+
+libvirt          STONITH for libvirt (kvm / Xen)
+sbd              SBD, Shared storage based fencing
+
+.INP: list names
+add
+apache
+check-uptime
+clvm
+clvm-vg
+database
+db2
+db2-hadr
+drbd
+exportfs
+filesystem
+gfs2
+gfs2-base
+.EXT crm_resource --show-metadata systemd:haproxy
+ERROR: 4: Error when loading script haproxy: No meta-data for agent: systemd:haproxy
+health
+init
+libvirt
+lvm
+mailto
+nfsserver
+ocfs2
+oracle
+raid-lvm
+raid1
+remove
+sap-as
+sap-ci
+sap-db
+sap-simple-stack
+sap-simple-stack-plus
+sapdb
+sapinstance
+sbd
+virtual-ip
+.INP: list names all
+add
+apache
+check-uptime
+clvm
+clvm-vg
+database
+db2
+db2-hadr
+drbd
+exportfs
+filesystem
+gfs2
+gfs2-base
+haproxy
+health
+init
+libvirt
+lvm
+mailto
+nfsserver
+ocfs2
+oracle
+raid-lvm
+raid1
+remove
+sap-as
+sap-ci
+sap-db
+sap-simple-stack
+sap-simple-stack-plus
+sapdb
+sapinstance
+sbd
+virtual-ip
+.INP: list all names
+add
+apache
+check-uptime
+clvm
+clvm-vg
+database
+db2
+db2-hadr
+drbd
+exportfs
+filesystem
+gfs2
+gfs2-base
+haproxy
+health
+init
+libvirt
+lvm
+mailto
+nfsserver
+ocfs2
+oracle
+raid-lvm
+raid1
+remove
+sap-as
+sap-ci
+sap-db
+sap-simple-stack
+sap-simple-stack-plus
+sapdb
+sapinstance
+sbd
+virtual-ip
+.INP: list bogus
+ERROR: 7: script.list: Unexpected argument 'bogus': expected  [all|names]
+.INP: show mailto
+mailto (Basic)
+MailTo
+
+ This is a resource agent for MailTo. It sends email to a sysadmin
+whenever  a takeover occurs.
+
+1. Notifies recipients by email in the event of resource takeover
+
+  id (required)  (unique) 
+      Identifier for the cluster resource
+  email (required) 
+      Email address
+  subject
+      Subject
+
+
+.INP: verify mailto id=foo email=test at example.com subject=hello
+1. Ensure mail package is installed
+
+	mailx
+
+2. Configure cluster resources
+
+	primitive foo ocf:heartbeat:MailTo
+		email="test at example.com"
+		subject="hello"
+		op start timeout="10"
+		op stop timeout="10"
+		op monitor interval="10" timeout="10"
+
+	clone c-foo foo
+
+.INP: run mailto id=foo email=test at example.com subject=hello nodes=node1 dry_run=true
+INFO: 10: MailTo
+INFO: 10: Nodes: node1
+** all - #!/usr/bin/env python
+import crm_script
+import crm_init
+
+crm_init.install_packages(['mailx'])
+crm_script.exit_ok(True)
+        
+OK: 10: Ensure mail package is installed
+** localhost - temporary file <<END
+primitive foo ocf:heartbeat:MailTo	email="test at example.com"	subject="hello"	op start timeout="10"	op stop timeout="10"	op monitor interval="10" timeout="10"
+clone c-foo foo
+
+END
+
+** localhost - crm --wait --no configure load update <<temporary file>>
+OK: 10: Configure cluster resources
diff --git a/test/testcases/scripts.filter b/test/testcases/scripts.filter
new file mode 100755
index 0000000..05e098a
--- /dev/null
+++ b/test/testcases/scripts.filter
@@ -0,0 +1,4 @@
+#!/usr/bin/awk -f
+# 1. replace .EXT [path/]<cmd> <parameter> with .EXT <cmd> <parameter>
+/\*\* localhost - crm --wait --no configure load update (\/tmp\/crm-tmp-.+)/ { gsub(/.*/, "<<temporary file>>", $NF) }
+{ print }
diff --git a/test/testcases/shadow.exp b/test/testcases/shadow.exp
index 759d6a0..7498958 100644
--- a/test/testcases/shadow.exp
+++ b/test/testcases/shadow.exp
@@ -1,18 +1,18 @@
 .TRY Shadow CIB management
 .INP: cib
 .INP: new regtest force
-.EXT >/dev/null </dev/null crm_shadow -c 'regtest' --force
+.EXT >/dev/null </dev/null crm_shadow -b -c 'regtest' --force
 INFO: 2: cib.new: regtest shadow CIB created
 .INP: reset regtest
-.EXT >/dev/null </dev/null crm_shadow -r 'regtest'
+.EXT >/dev/null </dev/null crm_shadow -b -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
+.EXT >/dev/null </dev/null crm_shadow -b -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
+.EXT >/dev/null </dev/null crm_shadow -b -D 'regtest' --force
 INFO: 8: cib.delete: regtest shadow CIB deleted
diff --git a/test/unit-tests.sh b/test/unit-tests.sh
deleted file mode 100755
index 635c6aa..0000000
--- a/test/unit-tests.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/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
index e8be176..2d696ce 100644
--- a/test/unittests/__init__.py
+++ b/test/unittests/__init__.py
@@ -1,15 +1,27 @@
 import os
-import msg
-import config
+import sys
+
+try:
+    import modules
+    sys.modules['crmsh'] = sys.modules['modules']
+except ImportError, e:
+    pass
+
+from crmsh import msg
+from crmsh import config
+from crmsh import options
 msg.ERR_STREAM = None
 config.core.debug = True
+options.regression_tests = True
 _here = os.path.dirname(__file__)
 config.path.sharedir = os.path.join(_here, "../../doc")
 config.path.crm_dtd_dir = os.path.join(_here, "schemas")
 
+os.environ["CIB_file"] = "test"
+
 
 # install a basic CIB
-import cibconfig
+from crmsh 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">
@@ -25,9 +37,9 @@ _CIB = """
       </cluster_property_set>
     </crm_config>
     <nodes>
-      <node id="1" uname="ha-one"/>
-      <node id="2" uname="ha-two"/>
-      <node id="3" uname="ha-three"/>
+      <node id="ha-one" uname="ha-one"/>
+      <node id="ha-two" uname="ha-two"/>
+      <node id="ha-three" uname="ha-three"/>
     </nodes>
     <resources>
     </resources>
@@ -52,3 +64,4 @@ _CIB = """
 """
 
 cibconfig.cib_factory.initialize(cib=_CIB)
+
diff --git a/test/unittests/scripts/inc1/main.yml b/test/unittests/scripts/inc1/main.yml
new file mode 100644
index 0000000..8c290d3
--- /dev/null
+++ b/test/unittests/scripts/inc1/main.yml
@@ -0,0 +1,22 @@
+version: 2.2
+shortdesc: Include test script 1
+longdesc: Test if includes work ok
+parameters:
+  - name: foo
+    type: boolean
+    shortdesc: An optional feature
+  - name: bar
+    type: string
+    shortdesc: A string of characters
+    value: the name is the game
+  - name: is-required
+    type: int
+    required: true
+actions:
+  - call: ls /tmp
+    when: foo
+    shortdesc: ls
+  - call: "echo '{{foo}}'"
+    shortdesc: foo
+  - call: "echo '{{bar}}'"
+    shortdesc: bar
diff --git a/test/unittests/scripts/inc2/main.yml b/test/unittests/scripts/inc2/main.yml
new file mode 100644
index 0000000..4910696
--- /dev/null
+++ b/test/unittests/scripts/inc2/main.yml
@@ -0,0 +1,26 @@
+---
+- version: 2.2
+  shortdesc: Includes another script
+  longdesc: This one includes another script
+  parameters:
+    - name: wiz
+      type: string
+    - name: foo
+      type: boolean
+      shortdesc: A different foo
+  include:
+    - script: inc1
+      name: included-script
+      parameters:
+        - name: is-required
+          value: 33
+  actions:
+    - call: "echo 'before {{wiz}}'"
+      shortdesc: before wiz
+    - include: included-script
+    - call: "echo 'after {{foo}}'"
+      shortdesc: after foo
+    - cib: |
+        {{included-script:is-required}}
+    - cib: |
+        {{wiz}}
diff --git a/scripts/init/main.yml b/test/unittests/scripts/legacy/main.yml
similarity index 100%
copy from scripts/init/main.yml
copy to test/unittests/scripts/legacy/main.yml
diff --git a/test/unittests/scripts/templates/apache.xml b/test/unittests/scripts/templates/apache.xml
new file mode 100644
index 0000000..faf3ef0
--- /dev/null
+++ b/test/unittests/scripts/templates/apache.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<template name="apache">
+
+<shortdesc lang="en">Apache Web Server</shortdesc>
+<longdesc lang="en">
+Create a single primitive resource of type apache.
+</longdesc>
+
+<parameters>
+
+<parameter name="id" required="1">
+<shortdesc lang="en">Resource ID</shortdesc>
+<longdesc lang="en">
+Unique ID for this Apache resource in the cluster.
+</longdesc>
+<content type="string" default="apache"/>
+</parameter>
+
+<parameter name="configfile" required="1">
+<shortdesc lang="en">Apache config file</shortdesc>
+<longdesc lang="en">
+Full pathname of the Apache configuration file</longdesc>
+<content type="string" default="/etc/apache2/httpd.conf"/>
+</parameter>
+
+</parameters>
+
+<crm_script>
+primitive <insert param="id"/> ocf:heartbeat:apache
+  params
+    configfile="<insert param="configfile"/>"
+  op start timeout="40" op stop timeout="60"
+  op monitor interval="10" timeout="20"
+</crm_script>
+
+</template>
diff --git a/test/unittests/scripts/templates/virtual-ip.xml b/test/unittests/scripts/templates/virtual-ip.xml
new file mode 100644
index 0000000..22ab5bf
--- /dev/null
+++ b/test/unittests/scripts/templates/virtual-ip.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+<template name="virtual-ip">
+
+<shortdesc lang="en">Virtual IP Address</shortdesc>
+<longdesc lang="en">
+Create a single primitive resource of type IPaddr2.
+</longdesc>
+
+<parameters>
+
+<parameter name="id" required="1">
+<shortdesc lang="en">Resource ID</shortdesc>
+<longdesc lang="en">
+Unique ID for this virtual IP address resource in the cluster.
+</longdesc>
+<content type="string" default="virtual-ip"/>
+</parameter>
+
+<parameter name="ip" required="1">
+<shortdesc lang="en">IP address</shortdesc>
+<longdesc lang="en">
+The IPv4 address to be configured in dotted quad notation,
+for example "192.168.1.1".
+</longdesc>
+<content type="string" default=""/>
+</parameter>
+
+<parameter name="netmask">
+<shortdesc lang="en">Netmask</shortdesc>
+<longdesc lang="en">
+The netmask for the interface in CIDR format
+(e.g., 24 and not 255.255.255.0).
+
+If unspecified, it will be determined automatically.
+</longdesc>
+<content type="string"/>
+</parameter>
+
+<parameter name="lvs_support">
+<shortdesc lang="en">LVS support</shortdesc>
+<longdesc lang="en">
+Enable support for LVS Direct Routing configurations. In case a IP
+address is stopped, only move it to the loopback device to allow the
+local node to continue to service requests, but no longer advertise it
+on the network.
+</longdesc>
+<content type="boolean"/>
+</parameter>
+
+</parameters>
+
+<crm_script>
+primitive <insert param="id"/> ocf:heartbeat:IPaddr2
+  params
+    ip="<insert param="ip"/>"
+    <if set="netmask">cidr_netmask="<insert param="netmask"/>"</if>
+    <if set="lvs_support">lvs_support="<insert param="lvs_support"/>"</if>
+  op start timeout="20" op stop timeout="20"
+  op monitor interval="10" timeout="20"
+</crm_script>
+
+</template>
diff --git a/test/unittests/scripts/unified/main.yml b/test/unittests/scripts/unified/main.yml
new file mode 100644
index 0000000..29f5d07
--- /dev/null
+++ b/test/unittests/scripts/unified/main.yml
@@ -0,0 +1,26 @@
+version: 2.2
+shortdesc: Unified Script
+longdesc: >
+  Test if we can define multiple steps in a single script
+category: test
+steps:
+  - parameters:
+      - name: id
+        type: resource
+        required: true
+        shortdesc: Identifier
+  - name: vip
+    shortdesc: Configure the virtual IP
+    parameters:
+      - name: id
+        type: resource
+        required: true
+        shortdesc: IP Identifier
+      - name: ip
+        type: ip_address
+        required: true
+        shortdesc: The IP Address
+actions:
+  - cib: |
+      primitive {{vip:id}} IPaddr2 ip={{vip:ip}}
+      group g-{{id}} {{id}} {{vip:id}}
diff --git a/test/unittests/scripts/v2/main.yml b/test/unittests/scripts/v2/main.yml
new file mode 100644
index 0000000..41822a2
--- /dev/null
+++ b/test/unittests/scripts/v2/main.yml
@@ -0,0 +1,46 @@
+---
+- version: 2.2
+  shortdesc: Apache Webserver
+  longdesc: >
+    Configure a resource group containing a virtual IP address and
+    an instance of the Apache web server.
+  category: Server
+  parameters:
+    - name: id
+      shortdesc: The ID specified here is for the web server resource group.
+    - name: install
+      type: boolean
+      value: true
+      shortdesc: Disable if no installation should be performed
+  include:
+    - agent: test:apache
+      parameters:
+        - name: id
+          value: "{{id}}-server"
+        - name: configfile
+          type: file
+      ops: |
+        op monitor interval=20s timeout=20s
+    - agent: test:virtual-ip
+      name: virtual-ip
+      parameters:
+        - name: id
+          value: "{{id}}-ip"
+        - name: ip
+          type: ip_address
+      ops: |
+        op monitor interval=20s timeout=20s
+  actions:
+    - install:
+        - apache2
+      when: install
+    - call: a2enable mod_status
+      shortdesc: Enable status module
+      nodes: all
+      when: install
+    - cib: |
+        {{virtual-ip}}
+        {{apache}}
+        group {{id}}
+          {{virtual-ip:id}}
+          {{apache:id}}
diff --git a/test/unittests/scripts/vip/main.yml b/test/unittests/scripts/vip/main.yml
new file mode 100644
index 0000000..4f3bde1
--- /dev/null
+++ b/test/unittests/scripts/vip/main.yml
@@ -0,0 +1,28 @@
+---
+- version: 2.2
+  shortdesc: Virtual IP
+  category: Basic
+  include:
+    - agent: test:virtual-ip
+      name: virtual-ip
+      parameters:
+        - name: id
+          type: resource
+          required: true
+        - name: ip
+          type: ip_address
+          required: true
+        - name: cidr_netmask
+          type: integer
+          required: false
+        - name: broadcast
+          type: ipaddress
+          required: false
+        - name: lvs_support
+          required: false
+          type: boolean
+      ops: |
+        op start timeout="20" op stop timeout="20"
+        op monitor interval="10" timeout="20"
+  actions:
+    - include: virtual-ip
diff --git a/test/unittests/scripts/vipinc/main.yml b/test/unittests/scripts/vipinc/main.yml
new file mode 100644
index 0000000..6741885
--- /dev/null
+++ b/test/unittests/scripts/vipinc/main.yml
@@ -0,0 +1,14 @@
+version: 2.2
+category: Test
+shortdesc: Test script include
+include:
+  - script: vip
+    parameters:
+      - name: id
+        value: vip1
+      - name: ip
+        value: 192.168.200.100
+actions:
+  - include: vip
+  - cib: |
+      clone c-{{vip:id}} {{vip:id}}
diff --git a/test/unittests/scripts/workflows/10-webserver.xml b/test/unittests/scripts/workflows/10-webserver.xml
new file mode 100644
index 0000000..f18d55a
--- /dev/null
+++ b/test/unittests/scripts/workflows/10-webserver.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<workflow name="10-webserver">
+
+<shortdesc lang="en">Web Server</shortdesc>
+<longdesc lang="en">
+Configure a resource group containing a virtual IP address and
+an instance of the Apache web server.  You may wish to use this
+in conjunction with a filesystem resource; in this case you will
+need to separately configure the filesystem then add colocation
+and ordering constraints to have it start before the resource
+group you create here.
+</longdesc>
+
+<parameters>
+<stepdesc lang="en">
+The ID specified here is for the web server resource group.
+</stepdesc>
+<parameter name="id" required="1">
+<shortdesc lang="en">Group ID</shortdesc>
+<longdesc lang="en">
+Unique ID for the web server resource group in the cluster.
+</longdesc>
+<content type="string" default="web-server"/>
+</parameter>
+</parameters>
+
+<templates>
+<template name="virtual-ip" required="1">
+<stepdesc lang="en">
+The IP address configured here will start before the Apache instance.
+</stepdesc>
+</template>
+<template name="apache" required="1">
+<stepdesc lang="en">
+The Apache configuration file specified here must be available via the
+same path on all cluster nodes, and Apache must be configured with
+mod_status enabled.  If in doubt, try running Apache manually via
+its init script first, and ensure http://localhost:80/server-status is
+accessible.
+</stepdesc>
+</template>
+</templates>
+
+<crm_script>
+group <insert param="id"/>
+  <insert param="id" from_template="virtual-ip"/>
+  <insert param="id" from_template="apache"/>
+</crm_script>
+
+</workflow>
diff --git a/test/unittests/test_bugs.py b/test/unittests/test_bugs.py
index 6408ec5..e5b3149 100644
--- a/test/unittests/test_bugs.py
+++ b/test/unittests/test_bugs.py
@@ -1,35 +1,27 @@
 # 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
+# See COPYING for license information.
+
+
+from crmsh import cibconfig
 from lxml import etree
-from nose.tools import eq_
-import xmlutil
+from nose.tools import eq_, with_setup
+from crmsh import xmlutil
 
 factory = cibconfig.cib_factory
 
 
 def setup_func():
     "set up test fixtures"
-    import idmgmt
+    from crmsh import idmgmt
     idmgmt.clear()
+    factory._push_state()
+
+
+def teardown_func():
+    factory._pop_state()
 
 
+ at with_setup(setup_func, teardown_func)
 def test_bug41660_1():
     xml = """<primitive id="bug41660" class="ocf" provider="pacemaker" type="Dummy"> \
     <meta_attributes id="bug41660-meta"> \
@@ -49,7 +41,7 @@ def test_bug41660_1():
     commit_holder = factory.commit
     try:
         factory.commit = lambda *args: True
-        from ui_resource import set_deep_meta_attr
+        from crmsh.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'))
@@ -57,6 +49,7 @@ def test_bug41660_1():
         factory.commit = commit_holder
 
 
+ at with_setup(setup_func, teardown_func)
 def test_bug41660_2():
     xml = """
 <clone id="libvirtd-clone">
@@ -89,7 +82,7 @@ def test_bug41660_2():
     commit_holder = factory.commit
     try:
         factory.commit = lambda *args: True
-        from ui_resource import set_deep_meta_attr
+        from crmsh.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)
@@ -99,6 +92,7 @@ def test_bug41660_2():
         factory.commit = commit_holder
 
 
+ at with_setup(setup_func, teardown_func)
 def test_bug41660_3():
     xml = """
 <clone id="libvirtd-clone">
@@ -127,7 +121,7 @@ def test_bug41660_3():
     commit_holder = factory.commit
     try:
         factory.commit = lambda *args: True
-        from ui_resource import set_deep_meta_attr
+        from crmsh.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'))
@@ -135,6 +129,7 @@ def test_bug41660_3():
         factory.commit = commit_holder
 
 
+ at with_setup(setup_func, teardown_func)
 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>
@@ -178,6 +173,7 @@ def test_comments():
     assert etree.tostring(elems).count("COMMENT TEXT") == 3
 
 
+ at with_setup(setup_func, teardown_func)
 def test_eq1():
     xml1 = """<cluster_property_set id="cib-bootstrap-options">
     <nvpair id="cib-bootstrap-options-stonith-enabled" name="stonith-enabled" value="true"></nvpair>
@@ -208,6 +204,7 @@ def test_eq1():
     assert xmlutil.xml_equals(e1, e2, show=True)
 
 
+ at with_setup(setup_func, teardown_func)
 def test_pcs_interop_1():
     """
     pcs<>crmsh interop bug
@@ -223,7 +220,7 @@ def test_pcs_interop_1():
         <primitive id="dummy-1" class="ocf" provider="heartbeat" type="Dummy"/>
       </clone>"""
     elem = etree.fromstring(xml)
-    from ui_resource import set_deep_meta_attr_node
+    from crmsh.ui_resource import set_deep_meta_attr_node
 
     assert len(elem.xpath(".//meta_attributes/nvpair[@name='target-role']")) == 1
 
@@ -236,6 +233,7 @@ def test_pcs_interop_1():
     assert len(elem.xpath(".//meta_attributes/nvpair[@name='target-role']")) == 1
 
 
+ at with_setup(setup_func, teardown_func)
 def test_bnc878128():
     """
     L3: "crm configure show" displays XML information instead of typical crm output.
@@ -259,6 +257,7 @@ end="2014-05-17 17:56:11Z"/>
     assert obj.cli_use_validate()
 
 
+ at with_setup(setup_func, teardown_func)
 def test_order_without_score_kind():
     """
     Spec says order doesn't require score or kind to be set
@@ -275,21 +274,23 @@ def test_order_without_score_kind():
 
 
 
+ at with_setup(setup_func, teardown_func)
 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
+    assert obj1 is True
     obj2 = factory.create_object('group', 'g1', 'p1')
-    assert obj2 is not None
+    assert obj2 is True
     obj3 = factory.create_object('group', 'g2', 'p1')
     print obj3
     assert obj3 is False
 
 
+ at with_setup(setup_func, teardown_func)
 def test_copy_nvpairs():
-    from cibconfig import copy_nvpairs
+    from crmsh.cibconfig import copy_nvpairs
 
     to = etree.fromstring('''
     <node>
@@ -315,6 +316,7 @@ def test_copy_nvpairs():
     eq_(['true'], to.xpath('./nvpair/@value'))
 
 
+ at with_setup(setup_func, teardown_func)
 def test_pengine_test():
     xml = '''<primitive class="ocf" id="rsc1" provider="pacemaker" type="Dummy">
         <instance_attributes id="rsc1-instance_attributes-1">
@@ -343,6 +345,7 @@ def test_pengine_test():
     assert obj.cli_use_validate()
 
 
+ at with_setup(setup_func, teardown_func)
 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>'''
@@ -353,6 +356,7 @@ def test_tagset():
     elems = factory.get_elems_on_tag("tag:t0")
     assert set(x.obj_id for x in elems) == set(['r1', 'r2'])
 
+ at with_setup(setup_func, teardown_func)
 def test_ratrace():
     xml = '''<primitive class="ocf" id="%s" provider="pacemaker" type="Dummy"/>'''
     factory.create_from_node(etree.fromstring(xml % ('r1')))
@@ -361,7 +365,7 @@ def test_ratrace():
 
     context = object()
 
-    from ui_resource import RscMgmt
+    from crmsh.ui_resource import RscMgmt
     obj = factory.find_object('r1')
     RscMgmt()._trace_resource(context, 'r1', obj)
 
@@ -372,6 +376,7 @@ def test_ratrace():
     assert set(obj.node.xpath('./operations/op/@name')) == set(['start', 'stop'])
 
 
+ at with_setup(setup_func, teardown_func)
 def test_op_role():
     xml = '''<primitive class="ocf" id="rsc2" provider="pacemaker" type="Dummy">
         <operations>
@@ -388,6 +393,7 @@ def test_op_role():
     assert obj.cli_use_validate()
 
 
+ at with_setup(setup_func, teardown_func)
 def test_nvpair_no_value():
     xml = '''<primitive class="ocf" id="rsc3" provider="heartbeat" type="Dummy">
         <instance_attributes id="rsc3-instance_attributes-1">
@@ -406,6 +412,7 @@ def test_nvpair_no_value():
     assert obj.cli_use_validate()
 
 
+ at with_setup(setup_func, teardown_func)
 def test_delete_ticket():
     xml0 = '<primitive id="daa0" class="ocf" provider="heartbeat" type="Dummy"/>'
     xml1 = '<primitive id="daa1" class="ocf" provider="heartbeat" type="Dummy"/>'
@@ -426,6 +433,7 @@ def test_delete_ticket():
     assert factory.find_object('taa0') is not None
 
 
+ at with_setup(setup_func, teardown_func)
 def test_quotes():
     """
     Parsing escaped quotes
@@ -444,3 +452,475 @@ def test_quotes():
     exp = 'primitive q1 ocf:pacemaker:Dummy params state="foo\\"foo\\""'
     assert data == exp
     assert obj.cli_use_validate()
+
+
+ at with_setup(setup_func, teardown_func)
+def test_nodeattrs():
+    """
+    bug with parsing node attrs
+    """
+    xml = '''<node id="1" uname="dell71"> \
+  <instance_attributes id="dell71-instance_attributes"> \
+    <nvpair name="staging-0-0-placement" value="true" id="dell71-instance_attributes-staging-0-0-placement"/> \
+    <nvpair name="meta-0-0-placement" value="true" id="dell71-instance_attributes-meta-0-0-placement"/> \
+  </instance_attributes> \
+  <instance_attributes id="nodes-1"> \
+    <nvpair id="nodes-1-standby" name="standby" value="off"/> \
+  </instance_attributes> \
+</node>'''
+
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    data = obj.repr_cli(format=-1)
+    exp = 'node 1: dell71 attributes staging-0-0-placement=true meta-0-0-placement=true attributes standby=off'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+
+ at with_setup(setup_func, teardown_func)
+def test_nodeattrs2():
+    xml = """<node id="h04" uname="h04"> \
+ <utilization id="h04-utilization"> \
+   <nvpair id="h04-utilization-utl_ram" name="utl_ram" value="1200"/> \
+   <nvpair id="h04-utilization-utl_cpu" name="utl_cpu" value="200"/> \
+ </utilization> \
+ <instance_attributes id="nodes-h04"> \
+   <nvpair id="nodes-h04-standby" name="standby" value="off"/> \
+ </instance_attributes> \
+</node>"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    assert obj is not None
+    data = obj.repr_cli(format=-1)
+    exp = 'node h04 utilization utl_ram=1200 utl_cpu=200 attributes standby=off'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+
+ at with_setup(setup_func, teardown_func)
+def test_group_constraint_location():
+    """
+    configuring a location constraint on a grouped resource is OK
+    """
+    factory.create_object('node', 'node1')
+    factory.create_object('primitive', 'p1', 'Dummy')
+    factory.create_object('primitive', 'p2', 'Dummy')
+    factory.create_object('group', 'g1', 'p1', 'p2')
+    factory.create_object('location', 'loc-p1', 'p1', 'inf:', 'node1')
+    c = factory.find_object('loc-p1')
+    assert c and c.check_sanity() == 0
+
+
+ at with_setup(setup_func, teardown_func)
+def test_group_constraint_colocation():
+    """
+    configuring a colocation constraint on a grouped resource is bad
+    """
+    factory.create_object('primitive', 'p1', 'Dummy')
+    factory.create_object('primitive', 'p2', 'Dummy')
+    factory.create_object('group', 'g1', 'p1', 'p2')
+    factory.create_object('colocation', 'coloc-p1-p2', 'inf:', 'p1', 'p2')
+    c = factory.find_object('coloc-p1-p2')
+    assert c and c.check_sanity() > 0
+
+
+ at with_setup(setup_func, teardown_func)
+def test_group_constraint_colocation_rscset():
+    """
+    configuring a constraint on a grouped resource is bad
+    """
+    factory.create_object('primitive', 'p1', 'Dummy')
+    factory.create_object('primitive', 'p2', 'Dummy')
+    factory.create_object('primitive', 'p3', 'Dummy')
+    factory.create_object('group', 'g1', 'p1', 'p2')
+    factory.create_object('colocation', 'coloc-p1-p2-p3', 'inf:', 'p1', 'p2', 'p3')
+    c = factory.find_object('coloc-p1-p2-p3')
+    assert c and c.check_sanity() > 0
+
+
+ at with_setup(setup_func, teardown_func)
+def test_clone_constraint_colocation_rscset():
+    """
+    configuring a constraint on a cloned resource is bad
+    """
+    factory.create_object('primitive', 'p1', 'Dummy')
+    factory.create_object('primitive', 'p2', 'Dummy')
+    factory.create_object('primitive', 'p3', 'Dummy')
+    factory.create_object('clone', 'c1', 'p1')
+    factory.create_object('colocation', 'coloc-p1-p2-p3', 'inf:', 'p1', 'p2', 'p3')
+    c = factory.find_object('coloc-p1-p2-p3')
+    assert c and c.check_sanity() > 0
+
+
+ at with_setup(setup_func, teardown_func)
+def test_existing_node_resource():
+    factory.create_object('primitive', 'ha-one', 'Dummy')
+
+    n = factory.find_node('ha-one')
+    assert factory.test_element(n)
+
+    r = factory.find_resource('ha-one')
+    assert factory.test_element(r)
+
+    assert n != r
+
+    assert factory.check_structure()
+    factory.cli_use_validate_all()
+
+    ok, s = factory.mkobj_set('ha-one')
+    assert ok
+
+
+ at with_setup(setup_func, teardown_func)
+def test_existing_node_resource_2():
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+
+    from crmsh import clidisplay
+    with clidisplay.nopretty():
+        text = obj.repr()
+    text += "\nprimitive ha-one Dummy"
+    ok = obj.save(text)
+    assert ok
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    with clidisplay.nopretty():
+        text2 = obj.repr()
+
+    assert sorted(text.split('\n')) == sorted(text2.split('\n'))
+
+
+ at with_setup(setup_func, teardown_func)
+def test_id_collision_breakage_1():
+    from crmsh import clidisplay
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    with clidisplay.nopretty():
+        original_cib = obj.repr()
+    print original_cib
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+
+    ok = obj.save("""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
+""")
+    assert ok
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    ok = obj.save("""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
+""")
+    assert ok
+
+    obj = cibconfig.mkset_obj()
+    with clidisplay.nopretty():
+        text = obj.repr()
+    text = text + "\nprimitive p2 ocf:heartbeat:Dummy"
+    ok = obj.save(text)
+    assert ok
+
+    obj = cibconfig.mkset_obj()
+    with clidisplay.nopretty():
+        text = obj.repr()
+    text = text + "\ngroup g1 p1 p2"
+    ok = obj.save(text)
+    assert ok
+
+    obj = cibconfig.mkset_obj("g1")
+    with clidisplay.nopretty():
+        text = obj.repr()
+    text = text.replace("group g1 p1 p2", "group g1 p1 p3")
+    text = text + "\nprimitive p3 ocf:heartbeat:Dummy"
+    ok = obj.save(text)
+    assert ok
+
+    obj = cibconfig.mkset_obj("g1")
+    with clidisplay.nopretty():
+        print obj.repr().strip()
+        assert obj.repr().strip() == "group g1 p1 p3"
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    ok = obj.save(original_cib)
+    assert ok
+    obj = cibconfig.mkset_obj()
+    with clidisplay.nopretty():
+        print "*** ORIGINAL"
+        print original_cib
+        print "*** NOW"
+        print obj.repr()
+        assert original_cib == obj.repr()
+
+
+ at with_setup(setup_func, teardown_func)
+def test_id_collision_breakage_3():
+    from crmsh import clidisplay
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    with clidisplay.nopretty():
+        original_cib = obj.repr()
+    print original_cib
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    ok = obj.save("""node node1
+primitive node1 Dummy params fake=something
+    """)
+    assert ok
+
+    print "** baseline"
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    with clidisplay.nopretty():
+        print obj.repr()
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    ok = obj.save("""primitive node1 Dummy params fake=something-else
+    """, no_remove=True, method='update')
+    assert ok
+
+    print "** end"
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    ok = obj.save(original_cib, no_remove=False, method='replace')
+    assert ok
+    obj = cibconfig.mkset_obj()
+    with clidisplay.nopretty():
+        print "*** ORIGINAL"
+        print original_cib
+        print "*** NOW"
+        print obj.repr()
+        assert original_cib == obj.repr()
+
+
+ at with_setup(setup_func, teardown_func)
+def test_id_collision_breakage_2():
+    from crmsh import clidisplay
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    with clidisplay.nopretty():
+        original_cib = obj.repr()
+    print original_cib
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+
+    ok = obj.save("""node 168633610: webui
+node 168633611: node1
+rsc_template web-server apache \
+	params port=8000 \
+	op monitor interval=10s
+primitive d0 Dummy \
+	meta target-role=Started
+primitive d1 Dummy
+primitive d2 Dummy
+# Never use this STONITH agent in production!
+primitive development-stonith stonith:null \
+	params hostlist="webui node1 node2 node3"
+primitive proxy systemd:haproxy \
+	op monitor interval=10s
+primitive proxy-vip IPaddr2 \
+	params ip=10.13.37.20
+primitive srv1 @web-server
+primitive srv2 @web-server
+primitive vip1 IPaddr2 \
+	params ip=10.13.37.21 \
+	op monitor interval=20s
+primitive vip2 IPaddr2 \
+	params ip=10.13.37.22 \
+	op monitor interval=20s
+primitive virtual-ip IPaddr2 \
+	params ip=10.13.37.77 lvs_support=false \
+	op start timeout=20 interval=0 \
+	op stop timeout=20 interval=0 \
+	op monitor interval=10 timeout=20
+primitive yet-another-virtual-ip IPaddr2 \
+	params ip=10.13.37.72 cidr_netmask=24 \
+	op start interval=0 timeout=20 \
+	op stop interval=0 timeout=20 \
+	op monitor interval=10 timeout=20 \
+	meta target-role=Started
+group dovip d0 virtual-ip \
+	meta target-role=Stopped
+group g-proxy proxy-vip proxy
+group g-serv1 vip1 srv1
+group g-serv2 vip2 srv2
+clone d2-clone d2 \
+	meta target-role=Started
+tag dummytag d0 d1 d1-on-node1 d2 d2-clone
+# Never put the two web servers on the same node
+colocation co-serv -inf: g-serv1 g-serv2
+location d1-on-node1 d1 inf: node1
+# Never put any web server or haproxy on webui
+location l-avoid-webui { g-proxy g-serv1 g-serv2 } -inf: webui
+# Prever to spread groups across nodes
+location l-proxy g-proxy 200: node1
+location l-serv1 g-serv1 200: node2
+location l-serv2 g-serv2 200: node3
+property cib-bootstrap-options: \
+	have-watchdog=false \
+	dc-version="1.1.13+git20150917.20c2178-224.2-1.1.13+git20150917.20c2178" \
+	cluster-infrastructure=corosync \
+	cluster-name=hacluster \
+	stonith-enabled=true \
+	no-quorum-policy=ignore \
+	placement-strategy=balanced
+rsc_defaults rsc-options: \
+	resource-stickiness=1 \
+	migration-threshold=3
+op_defaults op-options: \
+	timeout=600 \
+	record-pending=true
+""")
+    assert ok
+
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+    ok = obj.save(original_cib)
+    assert ok
+    obj = cibconfig.mkset_obj()
+    with clidisplay.nopretty():
+        print "*** ORIGINAL"
+        print original_cib
+        print "*** NOW"
+        print obj.repr()
+        assert original_cib == obj.repr()
+
+
+ at with_setup(setup_func, teardown_func)
+def test_bug_110():
+    """
+    configuring attribute-based fencing-topology
+    """
+    factory.create_object(*"primitive stonith-libvirt stonith:null".split())
+    factory.create_object(*"primitive fence-nova stonith:null".split())
+    cmd = "fencing_topology attr:OpenStack-role=compute stonith-libvirt,fence-nova".split()
+    ok = factory.create_object(*cmd)
+    assert ok
+    obj = cibconfig.mkset_obj()
+    assert obj is not None
+
+    for o in obj.obj_set:
+        if o.node.tag == 'fencing-topology':
+            assert o.check_sanity() == 0
+
+
+ at with_setup(setup_func, teardown_func)
+def test_reordering_resource_sets():
+    """
+    Can we reorder resource sets?
+    """
+    from crmsh import clidisplay
+    obj1 = factory.create_object('primitive', 'p1', 'Dummy')
+    assert obj1 is True
+    obj2 = factory.create_object('primitive', 'p2', 'Dummy')
+    assert obj2 is True
+    obj3 = factory.create_object('primitive', 'p3', 'Dummy')
+    assert obj3 is True
+    obj4 = factory.create_object('primitive', 'p4', 'Dummy')
+    assert obj4 is True
+    o1 = factory.create_object('order', 'o1', 'p1', 'p2', 'p3', 'p4')
+    assert o1 is True
+
+    obj = cibconfig.mkset_obj('o1')
+    assert obj is not None
+    rc = obj.save('order o1 p4 p3 p2 p1')
+    assert rc == True
+
+    obj2 = cibconfig.mkset_obj('o1')
+    with clidisplay.nopretty():
+        assert "order o1 p4 p3 p2 p1" == obj2.repr().strip()
+
+
+ at with_setup(setup_func, teardown_func)
+def test_bug959895():
+    """
+    Allow importing XML with cloned groups
+    """
+    xml = """<clone id="c-bug959895">
+    <group id="g-bug959895">
+    <primitive id="p-bug959895-a" class="ocf" provider="pacemaker" type="Dummy" />
+    <primitive id="p-bug959895-b" class="ocf" provider="pacemaker" type="Dummy" />
+    </group>
+</clone>
+"""
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    print etree.tostring(obj.node)
+    data = obj.repr_cli(format=-1)
+    print data
+    exp = 'clone c-bug959895 g-bug959895'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+    commit_holder = factory.commit
+    try:
+        factory.commit = lambda *args: True
+        from crmsh.ui_resource import set_deep_meta_attr
+        set_deep_meta_attr("c-bug959895", "target-role", "Started")
+        eq_(['Started'],
+            obj.node.xpath('.//nvpair[@name="target-role"]/@value'))
+    finally:
+        factory.commit = commit_holder
+
+
+ at with_setup(setup_func, teardown_func)
+def test_node_util_attr():
+    """
+    Handle node with utitilization before attributes correctly
+    """
+    xml = """<node id="aberfeldy" uname="aberfeldy">
+  <utilization id="nodes-aberfeldy-utilization">
+    <nvpair id="nodes-aberfeldy-utilization-cpu" name="cpu" value="2"/>
+    <nvpair id="nodes-aberfeldy-utilization-memory" name="memory" value="500"/>
+  </utilization>
+  <instance_attributes id="nodes-aberfeldy">
+    <nvpair id="nodes-aberfeldy-standby" name="standby" value="on"/>
+  </instance_attributes>
+</node>"""
+
+    data = etree.fromstring(xml)
+    obj = factory.create_from_node(data)
+    print etree.tostring(obj.node)
+    data = obj.repr_cli(format=-1)
+    print data
+    exp = 'node aberfeldy utilization cpu=2 memory=500 attributes standby=on'
+    assert data == exp
+    assert obj.cli_use_validate()
+
+
+ at with_setup(setup_func, teardown_func)
+def test_dup_create():
+    """
+    Creating two objects with the same name
+    """
+    ok = factory.create_object(*"primitive dup1 Dummy".split())
+    assert ok
+    ok = factory.create_object(*"primitive dup1 Dummy".split())
+    assert not ok
diff --git a/test/unittests/test_cib.py b/test/unittests/test_cib.py
index 7bdefcc..fbd1173 100644
--- a/test/unittests/test_cib.py
+++ b/test/unittests/test_cib.py
@@ -1,22 +1,8 @@
 # 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
+# See COPYING for license information.
+from crmsh import cibconfig
 from lxml import etree
-from nose.tools import eq_
+from nose.tools import eq_, with_setup
 import copy
 
 factory = cibconfig.cib_factory
@@ -24,10 +10,15 @@ factory = cibconfig.cib_factory
 
 def setup_func():
     "set up test fixtures"
-    import idmgmt
+    from crmsh import idmgmt
     idmgmt.clear()
 
 
+def teardown_func():
+    pass
+
+
+ at with_setup(setup_func, teardown_func)
 def test_cib_schema_change():
     "Changing the validate-with CIB attribute"
     copy_of_cib = copy.copy(factory.cib_orig)
diff --git a/test/unittests/test_cliformat.py b/test/unittests/test_cliformat.py
index e9a40bf..a96927a 100644
--- a/test/unittests/test_cliformat.py
+++ b/test/unittests/test_cliformat.py
@@ -1,25 +1,12 @@
 # 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
+# See COPYING for license information.
 #
 # unit tests for cliformat.py
 
-import cibconfig
+from crmsh import cibconfig
 from lxml import etree
-from test_parse import MockValidation
-from nose.tools import eq_
+from .test_parse import MockValidation
+from nose.tools import eq_, with_setup
 
 factory = cibconfig.cib_factory
 
@@ -31,6 +18,9 @@ def assert_is_not_none(thing):
 def roundtrip(cli, debug=False, expected=None):
     node, _, _ = cibconfig.parse_cli_to_xml(cli, validation=MockValidation())
     assert_is_not_none(node)
+    obj = factory.find_object(node.get("id"))
+    if obj:
+        factory.delete(node.get("id"))
     obj = factory.create_from_node(node)
     assert_is_not_none(obj)
     obj.nocli = True
@@ -51,7 +41,7 @@ def roundtrip(cli, debug=False, expected=None):
 
 def setup_func():
     "set up test fixtures"
-    import idmgmt
+    from crmsh import idmgmt
     idmgmt.clear()
 
 
@@ -59,24 +49,30 @@ def teardown_func():
     "tear down test fixtures"
 
 
+ at with_setup(setup_func, teardown_func)
 def test_rscset():
     roundtrip('colocation foo inf: a b')
     roundtrip('order order_2 Mandatory: [ A B ] C')
     roundtrip('rsc_template public_vm Xen')
 
 
+ at with_setup(setup_func, teardown_func)
 def test_group():
     factory.create_from_cli('primitive p1 Dummy')
     roundtrip('group g1 p1 params target-role=Stopped')
 
 
+ at with_setup(setup_func, teardown_func)
 def test_bnc863736():
     roundtrip('order order_3 Mandatory: [ A B ] C symmetrical=true')
 
 
+ at with_setup(setup_func, teardown_func)
 def test_sequential():
     roundtrip('colocation rsc_colocation-master inf: [ vip-master vip-rep sequential=true ] [ msPostgresql:Master sequential=true ]')
 
+
+ at with_setup(setup_func, teardown_func)
 def test_broken_colo():
     xml = """<rsc_colocation id="colo-2" score="INFINITY">
   <resource_set id="colo-2-0" require-all="false">
@@ -95,24 +91,29 @@ def test_broken_colo():
     assert obj.cli_use_validate()
 
 
+ at with_setup(setup_func, teardown_func)
 def test_comment():
     roundtrip("# comment 1\nprimitive d0 ocf:pacemaker:Dummy")
 
 
+ at with_setup(setup_func, teardown_func)
 def test_comment2():
     roundtrip("# comment 1\n# comment 2\n# comment 3\nprimitive d0 ocf:pacemaker:Dummy")
 
 
+ at with_setup(setup_func, teardown_func)
 def test_nvpair_ref1():
     factory.create_from_cli("primitive dummy-0 Dummy params $fiz:buz=bin")
     roundtrip('primitive dummy-1 Dummy params @fiz:boz')
 
 
+ at with_setup(setup_func, teardown_func)
 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')
 
 
+ at with_setup(setup_func, teardown_func)
 def test_ordering():
     xml = """<primitive id="dummy" class="ocf" provider="pacemaker" type="Dummy"> \
   <operations> \
@@ -135,6 +136,7 @@ value="Stopped"/> \
     assert obj.cli_use_validate()
 
 
+ at with_setup(setup_func, teardown_func)
 def test_ordering2():
     xml = """<primitive id="dummy2" class="ocf" provider="pacemaker" type="Dummy"> \
   <meta_attributes id="dummy2-meta_attributes"> \
@@ -158,6 +160,8 @@ value="Stopped"/> \
     eq_(exp, data)
     assert obj.cli_use_validate()
 
+
+ at with_setup(setup_func, teardown_func)
 def test_fencing():
     xml = """<fencing-topology>
     <fencing-level devices="st1" id="fencing" index="1"
@@ -177,6 +181,7 @@ target="ha-one"></fencing-level>
     assert obj.cli_use_validate()
 
 
+ at with_setup(setup_func, teardown_func)
 def test_master():
     xml = """<master id="ms-1">
     <crmsh-ref id="dummy3" />
@@ -191,6 +196,7 @@ def test_master():
     assert obj.cli_use_validate()
 
 
+ at with_setup(setup_func, teardown_func)
 def test_param_rules():
     roundtrip('primitive foo Dummy ' +
               'params rule #uname eq wizbang laser=yes ' +
@@ -202,26 +208,56 @@ def test_param_rules():
               'params 1: interface=eth0 port=9999')
 
 
+ at with_setup(setup_func, teardown_func)
+def test_multiple_attrsets():
+    roundtrip('primitive mySpecialRsc me:Special ' +
+              'params 3: interface=eth1 ' +
+              'params 2: port=8888')
+    roundtrip('primitive mySpecialRsc me:Special ' +
+              'meta 3: interface=eth1 ' +
+              'meta 2: port=8888')
+
+
+ at with_setup(setup_func, teardown_func)
 def test_new_acls():
     roundtrip('role fum description=test read description=test2 xpath:"*[@name=karl]"')
 
 
+ at with_setup(setup_func, teardown_func)
 def test_acls_reftype():
     roundtrip('role boo deny ref:d0 type:nvpair',
               expected='role boo deny ref:d0 deny type:nvpair')
 
 
+ at with_setup(setup_func, teardown_func)
 def test_acls_oldsyntax():
     roundtrip('role boo deny ref:d0 tag:nvpair',
               expected='role boo deny ref:d0 deny type:nvpair')
 
+
+ at with_setup(setup_func, teardown_func)
 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')
 
 
+ at with_setup(setup_func, teardown_func)
 def test_new_role():
     roundtrip('role silly-role-2 read xpath:"//nodes//attributes" ' +
               'deny type:nvpair deny ref:d0 deny type:nvpair')
 
+
+ at with_setup(setup_func, teardown_func)
+def test_topology_1114():
+    roundtrip('fencing_topology attr:rack=1 node1,node2')
+
+
+ at with_setup(setup_func, teardown_func)
+def test_is_value_sane():
+    roundtrip('''primitive p1 dummy params state="bo'o"''')
+
+
+ at with_setup(setup_func, teardown_func)
+def test_is_value_sane_2():
+    roundtrip('primitive p1 dummy params state="bo\\"o"')
diff --git a/test/unittests/test_corosync.py b/test/unittests/test_corosync.py
index af2bb16..db8dd8c 100644
--- a/test/unittests/test_corosync.py
+++ b/test/unittests/test_corosync.py
@@ -1,29 +1,17 @@
 # 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
+# See COPYING for license information.
 #
 # unit tests for parse.py
 
+import os
 import unittest
-import corosync
-from corosync import Parser, make_section, make_value
+from crmsh import corosync
+from crmsh.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()
+F1 = open(os.path.join(os.path.dirname(__file__), 'corosync.conf.1')).read()
+F2 = open(os.path.join(os.path.dirname(__file__), 'corosync.conf.2')).read()
+F3 = open(os.path.join(os.path.dirname(__file__), 'bug-862577_corosync.conf')).read()
 
 
 def _valid(parser):
@@ -87,7 +75,7 @@ class TestCorosyncParser(unittest.TestCase):
 
     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
+        from crmsh.corosync import make_section, make_value, next_nodeid
 
         p = Parser(F1)
         _valid(p)
@@ -101,7 +89,7 @@ class TestCorosyncParser(unittest.TestCase):
         self.assertEqual(p.count('nodelist.node'), nid - 1)
 
     def test_add_node_nodelist(self):
-        from corosync import make_section, make_value, next_nodeid
+        from crmsh.corosync import make_section, make_value, next_nodeid
 
         p = Parser(F2)
         _valid(p)
diff --git a/test/unittests/test_gv.py b/test/unittests/test_gv.py
new file mode 100644
index 0000000..4a81950
--- /dev/null
+++ b/test/unittests/test_gv.py
@@ -0,0 +1,26 @@
+# Copyright (C) 2015 Kristoffer Gronlund <kgronlund at suse.com>
+# See COPYING for license information.
+
+
+from crmsh import crm_gv
+from crmsh import cibconfig
+from nose.tools import eq_
+
+
+def test_digits_ident():
+    g = crm_gv.gv_types["dot"]()
+    cibconfig.set_graph_attrs(g, ".")
+
+    g.new_node("1a", top_node=True)
+    g.new_attr("1a", 'label', "1a")
+    g.new_node("a", top_node=True)
+    g.new_attr("a", 'label', "a")
+
+    eq_("""digraph G {
+
+fontname="Helvetica";
+fontsize="11";
+compound="true";
+"1a" [label="1a"];
+a [label="a"];
+}""", '\n'.join(g.repr()).replace('\t', ''))
diff --git a/test/unittests/test_handles.py b/test/unittests/test_handles.py
new file mode 100644
index 0000000..0d62ec9
--- /dev/null
+++ b/test/unittests/test_handles.py
@@ -0,0 +1,166 @@
+# Copyright (C) 2015 Kristoffer Gronlund <kgronlund at suse.com>
+# See COPYING for license information.
+
+
+from crmsh import handles
+from nose.tools import eq_
+
+
+def test_basic():
+    t = """{{foo}}"""
+    eq_("hello", handles.parse(t, {'foo': 'hello'}))
+    t = """{{foo:bar}}"""
+    eq_("hello", handles.parse(t, {'foo': {'bar': 'hello'}}))
+    t = """{{wiz}}"""
+    eq_("", handles.parse(t, {'foo': {'bar': 'hello'}}))
+    t = """{{foo}}.{{wiz}}"""
+    eq_("a.b", handles.parse(t, {'foo': "a", 'wiz': "b"}))
+    t = """Here's a line of text
+    followed by another line
+    followed by some {{foo}}.{{wiz}}
+    and then some at the end"""
+    eq_("""Here's a line of text
+    followed by another line
+    followed by some a.b
+    and then some at the end""", handles.parse(t, {'foo': "a", 'wiz': "b"}))
+
+
+def test_weird_chars():
+    t = "{{foo#_bar}}"
+    eq_("hello", handles.parse(t, {'foo#_bar': 'hello'}))
+    t = "{{_foo$bar_}}"
+    eq_("hello", handles.parse(t, {'_foo$bar_': 'hello'}))
+
+
+def test_conditional():
+    t = """{{#foo}}before{{foo:bar}}after{{/foo}}"""
+    eq_("beforehelloafter", handles.parse(t, {'foo': {'bar': 'hello'}}))
+    eq_("", handles.parse(t, {'faa': {'bar': 'hello'}}))
+
+    t = """{{#cond}}before{{foo:bar}}after{{/cond}}"""
+    eq_("beforehelloafter", handles.parse(t, {'foo': {'bar': 'hello'}, 'cond': True}))
+    eq_("", handles.parse(t, {'foo': {'bar': 'hello'}, 'cond': False}))
+
+
+def test_iteration():
+    t = """{{#foo}}!{{foo:bar}}!{{/foo}}"""
+    eq_("!hello!!there!", handles.parse(t, {'foo': [{'bar': 'hello'}, {'bar': 'there'}]}))
+
+
+def test_result():
+    t = """{{obj}}
+    group g1 {{obj:id}}
+"""
+    eq_("""primitive d0 Dummy
+    group g1 d0
+""", handles.parse(t, {'obj': handles.value({'id': 'd0'}, 'primitive d0 Dummy')}))
+    eq_("\n    group g1 \n", handles.parse(t, {}))
+
+
+def test_result2():
+    t = """{{obj}}
+    group g1 {{obj:id}}
+{{#obj}}
+{{obj}}
+{{/obj}}
+"""
+    eq_("""primitive d0 Dummy
+    group g1 d0
+primitive d0 Dummy
+""", handles.parse(t, {'obj': handles.value({'id': 'd0'}, 'primitive d0 Dummy')}))
+    eq_("\n    group g1 \n", handles.parse(t, {}))
+
+
+def test_mustasche():
+    t = """Hello {{name}}
+You have just won {{value}} dollars!
+{{#in_ca}}
+Well, {{taxed_value}} dollars, after taxes.
+{{/in_ca}}
+"""
+    v = {
+        "name": "Chris",
+        "value": 10000,
+        "taxed_value": 10000 - (10000 * 0.4),
+        "in_ca": True
+    }
+
+    eq_("""Hello Chris
+You have just won 10000 dollars!
+Well, 6000.0 dollars, after taxes.
+""", handles.parse(t, v))
+
+
+def test_invert():
+    t = """{{#repo}}
+<b>{{name}}</b>
+{{/repo}}
+{{^repo}}
+No repos :(
+{{/repo}}
+"""
+    v = {
+        "repo": []
+    }
+
+    eq_("""
+No repos :(
+""", handles.parse(t, v))
+
+
+def test_invert_2():
+    t = """foo
+{{#repo}}
+<b>{{name}}</b>
+{{/repo}}
+{{^repo}}
+No repos :(
+{{/repo}}
+bar
+"""
+    v = {
+        "repo": []
+    }
+
+    eq_("""foo
+No repos :(
+bar
+""", handles.parse(t, v))
+
+
+def test_cib():
+    t = """{{filesystem}}
+{{exportfs}}
+{{rootfs}}
+{{virtual-ip}}
+clone c-{{rootfs:id}} {{rootfs:id}}
+group g-nfs
+  {{exportfs:id}}
+  {{virtual-ip:id}}
+order base-then-nfs inf: {{filesystem:id}} g-nfs
+colocation nfs-with-base inf: g-nfs {{filesystem:id}}
+order rootfs-before-nfs inf: c-{{rootfs:id}} g-nfs:start
+colocation nfs-with-rootfs inf: g-nfs c-{{rootfs:id}}
+"""
+    r = """primitive fs1 Filesystem
+primitive efs exportfs
+primitive rfs rootfs
+primitive vip IPaddr2
+  params ip=192.168.0.2
+clone c-rfs rfs
+group g-nfs
+  efs
+  vip
+order base-then-nfs inf: fs1 g-nfs
+colocation nfs-with-base inf: g-nfs fs1
+order rootfs-before-nfs inf: c-rfs g-nfs:start
+colocation nfs-with-rootfs inf: g-nfs c-rfs
+"""
+    v = {
+        'filesystem': handles.value({'id': 'fs1'}, 'primitive fs1 Filesystem'),
+        'exportfs': handles.value({'id': 'efs'}, 'primitive efs exportfs'),
+        'rootfs': handles.value({'id': 'rfs'}, 'primitive rfs rootfs'),
+        'virtual-ip': handles.value({'id': 'vip'},
+                                    'primitive vip IPaddr2\n  params ip=192.168.0.2'),
+    }
+    eq_(r, handles.parse(t, v))
diff --git a/test/unittests/test_objset.py b/test/unittests/test_objset.py
index 4029660..b63ab3b 100644
--- a/test/unittests/test_objset.py
+++ b/test/unittests/test_objset.py
@@ -1,23 +1,9 @@
 # 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_
+# See COPYING for license information.
+
+
+from crmsh import cibconfig
+from nose.tools import eq_, with_setup
 
 factory = cibconfig.cib_factory
 
@@ -30,10 +16,15 @@ def assert_in(needle, haystack):
 
 def setup_func():
     "set up test fixtures"
-    import idmgmt
+    from crmsh import idmgmt
     idmgmt.clear()
 
 
+def teardown_func():
+    pass
+
+
+ at with_setup(setup_func, teardown_func)
 def test_nodes_nocli():
     for n in factory.node_id_list():
         obj = factory.find_object(n)
@@ -43,8 +34,9 @@ def test_nodes_nocli():
             eq_(False, obj.nocli)
 
 
+ at with_setup(setup_func, teardown_func)
 def test_show():
     setobj = cibconfig.mkset_obj()
     s = setobj.repr_nopretty()
     sp = s.splitlines()
-    assert_in("node 1: ha-one", sp[0:3])
+    assert_in("node ha-one", sp[0:3])
diff --git a/test/unittests/test_parse.py b/test/unittests/test_parse.py
index 896a5b1..e770c3c 100644
--- a/test/unittests/test_parse.py
+++ b/test/unittests/test_parse.py
@@ -1,25 +1,12 @@
 # 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
+# See COPYING for license information.
 #
 # unit tests for parse.py
 
-import parse
+from crmsh import parse
 import unittest
 import shlex
-from utils import lines2cli
+from crmsh.utils import lines2cli
 from lxml import etree
 from nose.tools import ok_, eq_
 
@@ -99,7 +86,7 @@ class TestBaseParser(unittest.TestCase):
         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}
+        retdict = dict([(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')
@@ -158,6 +145,11 @@ class TestCliParser(unittest.TestCase):
         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')
 
+        out2 = self.parser.parse('primitive st stonith:ssh hostlist=node1 meta target-role=Started op start requires=nothing timeout=60s op monitor interval=60m timeout=60s')
+        self.assertEqual(out2.get('id'), 'st')
+
+        self.assertEqual(etree.tostring(out), etree.tostring(out2))
+
         out = self.parser.parse('primitive st stonith:ssh params hostlist= meta')
         self.assertEqual(out.get('id'), 'st')
 
@@ -170,6 +162,10 @@ class TestCliParser(unittest.TestCase):
         self.assertEqual(['resource'], out.xpath('./crmsh-ref/@id'))
         self.assertEqual(['b'], out.xpath('instance_attributes/nvpair[@name="a"]/@value'))
 
+        out2 = self.parser.parse('ms m0 resource a=b')
+        self.assertEqual(out.get('id'), 'm0')
+        self.assertEqual(etree.tostring(out), etree.tostring(out2))
+
         out = self.parser.parse('master ma resource meta a=b')
         self.assertEqual(out.get('id'), 'ma')
         self.assertEqual(['resource'], out.xpath('./crmsh-ref/@id'))
@@ -438,7 +434,8 @@ class TestCliParser(unittest.TestCase):
         # num test nodes are 3
 
         out = self.parser.parse('fencing_topology poison-pill power')
-        self.assertEqual(6, len(out))
+        expect = '<fencing-topology><fencing-level devices="poison-pill" index="1" target="ha-one"/><fencing-level devices="power" index="2" target="ha-one"/><fencing-level devices="poison-pill" index="1" target="ha-three"/><fencing-level devices="power" index="2" target="ha-three"/><fencing-level devices="poison-pill" index="1" target="ha-two"/><fencing-level devices="power" index="2" target="ha-two"/></fencing-topology>'
+        self.assertEqual(expect, etree.tostring(out))
 
         out = self.parser.parse('fencing_topology node-a: poison-pill power node-b: ipmi serial')
         self.assertEqual(4, len(out))
@@ -446,8 +443,21 @@ class TestCliParser(unittest.TestCase):
         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))
+        print etree.tostring(out)
         self.assertEqual(1, len(out))
 
+    def test_fencing_1114(self):
+        """
+        Test node attribute fence target assignment
+        """
+        out = self.parser.parse('fencing_topology attr:rack=1 poison-pill power')
+        expect = """<fencing-topology><fencing-level devices="poison-pill" index="1" target-attribute="rack" target-value="1"/><fencing-level devices="power" index="2" target-attribute="rack" target-value="1"/></fencing-topology>"""
+        self.assertEqual(expect, etree.tostring(out))
+
+        out = self.parser.parse('fencing_topology attr:rack=1 poison-pill,power')
+        expect = '<fencing-topology><fencing-level devices="poison-pill,power" index="1" target-attribute="rack" target-value="1"/></fencing-topology>'
+        self.assertEqual(expect, etree.tostring(out))
+
     def test_tag(self):
         out = self.parser.parse('tag tag1: one two three')
         self.assertEqual(out.get('id'), 'tag1')
@@ -459,6 +469,10 @@ class TestCliParser(unittest.TestCase):
         out = self.parser.parse('tag tag1:: foo')
         self.assertFalse(out)
 
+        out = self.parser.parse('tag tag1 foo bar')
+        self.assertEqual(out.get('id'), 'tag1')
+        self.assertEqual(['foo', 'bar'], out.xpath('/tag/obj_ref/@id'))
+
     def _parse_lines(self, lines):
         out = []
         for line in lines2cli(lines):
@@ -648,7 +662,7 @@ class TestCliParser(unittest.TestCase):
             '<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>',
+            '<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-three"/><fencing-level devices="st2" index="2" target="ha-three"/><fencing-level devices="st" index="1" target="ha-two"/><fencing-level devices="st2" index="2" target="ha-two"/></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>',
diff --git a/test/unittests/test_resource.py b/test/unittests/test_resource.py
index 035bd5c..becfeeb 100644
--- a/test/unittests/test_resource.py
+++ b/test/unittests/test_resource.py
@@ -1,22 +1,9 @@
 # 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
+# See COPYING for license information.
 
 
-import ui_resource
-import utils
+from crmsh import ui_resource
+from crmsh import utils
 
 
 def test_maintenance():
diff --git a/test/unittests/test_scripts.py b/test/unittests/test_scripts.py
new file mode 100644
index 0000000..84240c7
--- /dev/null
+++ b/test/unittests/test_scripts.py
@@ -0,0 +1,826 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+# See COPYING for license information.
+
+
+from os import path
+from pprint import pprint
+from nose.tools import eq_, with_setup, assert_raises
+from lxml import etree
+from crmsh import scripts
+from crmsh import ra
+from crmsh import utils
+
+scripts._script_dirs = lambda: [path.join(path.dirname(__file__), 'scripts')]
+
+_apache = '''<?xml version="1.0"?>
+<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
+<resource-agent name="apache">
+<version>1.0</version>
+
+<longdesc lang="en">
+This is the resource agent for the Apache Web server.
+This resource agent operates both version 1.x and version 2.x Apache
+servers.
+
+The start operation ends with a loop in which monitor is
+repeatedly called to make sure that the server started and that
+it is operational. Hence, if the monitor operation does not
+succeed within the start operation timeout, the apache resource
+will end with an error status.
+
+The monitor operation by default loads the server status page
+which depends on the mod_status module and the corresponding
+configuration file (usually /etc/apache2/mod_status.conf).
+Make sure that the server status page works and that the access
+is allowed *only* from localhost (address 127.0.0.1).
+See the statusurl and testregex attributes for more details.
+
+See also http://httpd.apache.org/
+</longdesc>
+<shortdesc lang="en">Manages an Apache Web server instance</shortdesc>
+
+<parameters>
+<parameter name="configfile" required="0" unique="1">
+<longdesc lang="en">
+The full pathname of the Apache configuration file.
+This file is parsed to provide defaults for various other
+resource agent parameters.
+</longdesc>
+<shortdesc lang="en">configuration file path</shortdesc>
+<content type="string" default="$(detect_default_config)" />
+</parameter>
+
+<parameter name="httpd">
+<longdesc lang="en">
+The full pathname of the httpd binary (optional).
+</longdesc>
+<shortdesc lang="en">httpd binary path</shortdesc>
+<content type="string" default="/usr/sbin/httpd" />
+</parameter>
+
+<parameter name="port" >
+<longdesc lang="en">
+A port number that we can probe for status information
+using the statusurl.
+This will default to the port number found in the
+configuration file, or 80, if none can be found
+in the configuration file.
+
+</longdesc>
+<shortdesc lang="en">httpd port</shortdesc>
+<content type="integer" />
+</parameter>
+
+<parameter name="statusurl">
+<longdesc lang="en">
+The URL to monitor (the apache server status page by default).
+If left unspecified, it will be inferred from
+the apache configuration file.
+
+If you set this, make sure that it succeeds *only* from the
+localhost (127.0.0.1). Otherwise, it may happen that the cluster
+complains about the resource being active on multiple nodes.
+</longdesc>
+<shortdesc lang="en">url name</shortdesc>
+<content type="string" />
+</parameter>
+
+<parameter name="testregex">
+<longdesc lang="en">
+Regular expression to match in the output of statusurl.
+Case insensitive.
+</longdesc>
+<shortdesc lang="en">monitor regular expression</shortdesc>
+<content type="string" default="exists, but impossible to show in a human readable format (try grep testregex)"/>
+</parameter>
+
+<parameter name="client">
+<longdesc lang="en">
+Client to use to query to Apache. If not specified, the RA will
+try to find one on the system. Currently, wget and curl are
+supported. For example, you can set this parameter to "curl" if
+you prefer that to wget.
+</longdesc>
+<shortdesc lang="en">http client</shortdesc>
+<content type="string" default=""/>
+</parameter>
+
+<parameter name="testurl">
+<longdesc lang="en">
+URL to test. If it does not start with "http", then it's
+considered to be relative to the Listen address.
+</longdesc>
+<shortdesc lang="en">test url</shortdesc>
+<content type="string" />
+</parameter>
+
+<parameter name="testregex10">
+<longdesc lang="en">
+Regular expression to match in the output of testurl.
+Case insensitive.
+</longdesc>
+<shortdesc lang="en">extended monitor regular expression</shortdesc>
+<content type="string" />
+</parameter>
+
+<parameter name="testconffile">
+<longdesc lang="en">
+A file which contains test configuration. Could be useful if
+you have to check more than one web application or in case sensitive
+info should be passed as arguments (passwords). Furthermore,
+using a config file is the only way to specify certain
+parameters.
+
+Please see README.webapps for examples and file description.
+</longdesc>
+<shortdesc lang="en">test configuration file</shortdesc>
+<content type="string" />
+</parameter>
+
+<parameter name="testname">
+<longdesc lang="en">
+Name of the test within the test configuration file.
+</longdesc>
+<shortdesc lang="en">test name</shortdesc>
+<content type="string" />
+</parameter>
+
+<parameter name="options">
+<longdesc lang="en">
+Extra options to apply when starting apache. See man httpd(8).
+</longdesc>
+<shortdesc lang="en">command line options</shortdesc>
+<content type="string" />
+</parameter>
+
+<parameter name="envfiles">
+<longdesc lang="en">
+Files (one or more) which contain extra environment variables.
+If you want to prevent script from reading the default file, set
+this parameter to empty string.
+</longdesc>
+<shortdesc lang="en">environment settings files</shortdesc>
+<content type="string" default="/etc/apache2/envvars"/>
+</parameter>
+
+<parameter name="use_ipv6">
+<longdesc lang="en">
+We will try to detect if the URL (for monitor) is IPv6, but if
+that doesn't work set this to true to enforce IPv6.
+</longdesc>
+<shortdesc lang="en">use ipv6 with http clients</shortdesc>
+<content type="boolean" default="false"/>
+</parameter>
+
+</parameters>
+
+<actions>
+<action name="start"   timeout="40s" />
+<action name="stop"    timeout="60s" />
+<action name="status"  timeout="30s" />
+<action name="monitor" depth="0"  timeout="20s" interval="10" />
+<action name="meta-data"  timeout="5" />
+<action name="validate-all"  timeout="5" />
+</actions>
+</resource-agent>
+'''
+
+_virtual_ip = '''<?xml version="1.0"?>
+<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
+<resource-agent name="IPaddr2">
+<version>1.0</version>
+
+<longdesc lang="en">
+This Linux-specific resource manages IP alias IP addresses.
+It can add an IP alias, or remove one.
+In addition, it can implement Cluster Alias IP functionality
+if invoked as a clone resource.
+
+If used as a clone, you should explicitly set clone-node-max >= 2,
+and/or clone-max < number of nodes. In case of node failure,
+clone instances need to be re-allocated on surviving nodes.
+This would not be possible if there is already an instance on those nodes,
+and clone-node-max=1 (which is the default).
+</longdesc>
+
+<shortdesc lang="en">Manages virtual IPv4 and IPv6 addresses (Linux specific version)</shortdesc>
+
+<parameters>
+<parameter name="ip" unique="1" required="1">
+<longdesc lang="en">
+The IPv4 (dotted quad notation) or IPv6 address (colon hexadecimal notation)
+example IPv4 "192.168.1.1".
+example IPv6 "2001:db8:DC28:0:0:FC57:D4C8:1FFF".
+</longdesc>
+<shortdesc lang="en">IPv4 or IPv6 address</shortdesc>
+<content type="string" default="" />
+</parameter>
+<parameter name="nic" unique="0">
+<longdesc lang="en">
+The base network interface on which the IP address will be brought
+online. 
+If left empty, the script will try and determine this from the
+routing table.
+
+Do NOT specify an alias interface in the form eth0:1 or anything here;
+rather, specify the base interface only.
+If you want a label, see the iflabel parameter.
+
+Prerequisite:
+
+There must be at least one static IP address, which is not managed by
+the cluster, assigned to the network interface.
+If you can not assign any static IP address on the interface,
+modify this kernel parameter:
+
+sysctl -w net.ipv4.conf.all.promote_secondaries=1 # (or per device)
+</longdesc>
+<shortdesc lang="en">Network interface</shortdesc>
+<content type="string"/>
+</parameter>
+
+<parameter name="cidr_netmask">
+<longdesc lang="en">
+The netmask for the interface in CIDR format
+(e.g., 24 and not 255.255.255.0)
+
+If unspecified, the script will also try to determine this from the
+routing table.
+</longdesc>
+<shortdesc lang="en">CIDR netmask</shortdesc>
+<content type="string" default=""/>
+</parameter>
+
+<parameter name="broadcast">
+<longdesc lang="en">
+Broadcast address associated with the IP. If left empty, the script will
+determine this from the netmask.
+</longdesc>
+<shortdesc lang="en">Broadcast address</shortdesc>
+<content type="string" default=""/>
+</parameter>
+
+<parameter name="iflabel">
+<longdesc lang="en">
+You can specify an additional label for your IP address here.
+This label is appended to your interface name.
+
+The kernel allows alphanumeric labels up to a maximum length of 15
+characters including the interface name and colon (e.g. eth0:foobar1234)
+
+A label can be specified in nic parameter but it is deprecated.
+If a label is specified in nic name, this parameter has no effect.
+</longdesc>
+<shortdesc lang="en">Interface label</shortdesc>
+<content type="string" default=""/>
+</parameter>
+
+<parameter name="lvs_support">
+<longdesc lang="en">
+Enable support for LVS Direct Routing configurations. In case a IP
+address is stopped, only move it to the loopback device to allow the
+local node to continue to service requests, but no longer advertise it
+on the network.
+
+Notes for IPv6:
+It is not necessary to enable this option on IPv6.
+Instead, enable 'lvs_ipv6_addrlabel' option for LVS-DR usage on IPv6.
+</longdesc>
+<shortdesc lang="en">Enable support for LVS DR</shortdesc>
+<content type="boolean" default="${OCF_RESKEY_lvs_support_default}"/>
+</parameter>
+
+<parameter name="lvs_ipv6_addrlabel">
+<longdesc lang="en">
+Enable adding IPv6 address label so IPv6 traffic originating from
+the address's interface does not use this address as the source.
+This is necessary for LVS-DR health checks to realservers to work. Without it,
+the most recently added IPv6 address (probably the address added by IPaddr2)
+will be used as the source address for IPv6 traffic from that interface and
+since that address exists on loopback on the realservers, the realserver
+response to pings/connections will never leave its loopback.
+See RFC3484 for the detail of the source address selection.
+
+See also 'lvs_ipv6_addrlabel_value' parameter.
+</longdesc>
+<shortdesc lang="en">Enable adding IPv6 address label.</shortdesc>
+<content type="boolean" default="${OCF_RESKEY_lvs_ipv6_addrlabel_default}"/>
+</parameter>
+
+<parameter name="lvs_ipv6_addrlabel_value">
+<longdesc lang="en">
+Specify IPv6 address label value used when 'lvs_ipv6_addrlabel' is enabled.
+The value should be an unused label in the policy table
+which is shown by 'ip addrlabel list' command.
+You would rarely need to change this parameter.
+</longdesc>
+<shortdesc lang="en">IPv6 address label value.</shortdesc>
+<content type="integer" default="${OCF_RESKEY_lvs_ipv6_addrlabel_value_default}"/>
+</parameter>
+
+<parameter name="mac">
+<longdesc lang="en">
+Set the interface MAC address explicitly. Currently only used in case of
+the Cluster IP Alias. Leave empty to chose automatically.
+
+</longdesc>
+<shortdesc lang="en">Cluster IP MAC address</shortdesc>
+<content type="string" default=""/>
+</parameter>
+
+<parameter name="clusterip_hash">
+<longdesc lang="en">
+Specify the hashing algorithm used for the Cluster IP functionality.
+
+</longdesc>
+<shortdesc lang="en">Cluster IP hashing function</shortdesc>
+<content type="string" default="${OCF_RESKEY_clusterip_hash_default}"/>
+</parameter>
+
+<parameter name="unique_clone_address">
+<longdesc lang="en">
+If true, add the clone ID to the supplied value of IP to create
+a unique address to manage 
+</longdesc>
+<shortdesc lang="en">Create a unique address for cloned instances</shortdesc>
+<content type="boolean" default="${OCF_RESKEY_unique_clone_address_default}"/>
+</parameter>
+
+<parameter name="arp_interval">
+<longdesc lang="en">
+Specify the interval between unsolicited ARP packets in milliseconds.
+</longdesc>
+<shortdesc lang="en">ARP packet interval in ms</shortdesc>
+<content type="integer" default="${OCF_RESKEY_arp_interval_default}"/>
+</parameter>
+
+<parameter name="arp_count">
+<longdesc lang="en">
+Number of unsolicited ARP packets to send.
+</longdesc>
+<shortdesc lang="en">ARP packet count</shortdesc>
+<content type="integer" default="${OCF_RESKEY_arp_count_default}"/>
+</parameter>
+
+<parameter name="arp_bg">
+<longdesc lang="en">
+Whether or not to send the ARP packets in the background.
+</longdesc>
+<shortdesc lang="en">ARP from background</shortdesc>
+<content type="string" default="${OCF_RESKEY_arp_bg_default}"/>
+</parameter>
+
+<parameter name="arp_mac">
+<longdesc lang="en">
+MAC address to send the ARP packets to.
+
+You really shouldn't be touching this.
+
+</longdesc>
+<shortdesc lang="en">ARP MAC</shortdesc>
+<content type="string" default="${OCF_RESKEY_arp_mac_default}"/>
+</parameter>
+
+<parameter name="arp_sender">
+<longdesc lang="en">
+The program to send ARP packets with on start. For infiniband
+interfaces, default is ipoibarping. If ipoibarping is not
+available, set this to send_arp.
+</longdesc>
+<shortdesc lang="en">ARP sender</shortdesc>
+<content type="string" default=""/>
+</parameter>
+
+<parameter name="flush_routes">
+<longdesc lang="en">
+Flush the routing table on stop. This is for
+applications which use the cluster IP address
+and which run on the same physical host that the
+IP address lives on. The Linux kernel may force that
+application to take a shortcut to the local loopback
+interface, instead of the interface the address
+is really bound to. Under those circumstances, an
+application may, somewhat unexpectedly, continue
+to use connections for some time even after the
+IP address is deconfigured. Set this parameter in
+order to immediately disable said shortcut when the
+IP address goes away.
+</longdesc>
+<shortdesc lang="en">Flush kernel routing table on stop</shortdesc>
+<content type="boolean" default="false"/>
+</parameter>
+
+</parameters>
+<actions>
+<action name="start"   timeout="20s" />
+<action name="stop"    timeout="20s" />
+<action name="status" depth="0"  timeout="20s" interval="10s" />
+<action name="monitor" depth="0"  timeout="20s" interval="10s" />
+<action name="meta-data"  timeout="5s" />
+<action name="validate-all"  timeout="20s" />
+</actions>
+</resource-agent>
+'''
+
+_saved_get_ra = ra.get_ra
+_saved_cluster_nodes = utils.list_cluster_nodes
+
+
+def setup_func():
+    "hijack ra.get_ra to add new resource class (of sorts)"
+    class Agent(object):
+        def __init__(self, name):
+            self.name = name
+
+        def meta(self):
+            if self.name == 'apache':
+                return etree.fromstring(_apache)
+            else:
+                return etree.fromstring(_virtual_ip)
+
+    def _get_ra(agent):
+        if agent.startswith('test:'):
+            return Agent(agent[5:])
+        return _saved_get_ra(agent)
+    ra.get_ra = _get_ra
+
+    utils.list_cluster_nodes = lambda: [utils.this_node(), 'a', 'b', 'c']
+
+
+def teardown_func():
+    ra.get_ra = _saved_get_ra
+    utils.list_cluster_nodes = _saved_cluster_nodes
+
+
+def test_list():
+    eq_(set(['v2', 'legacy', '10-webserver', 'inc1', 'inc2', 'vip', 'vipinc', 'unified']),
+        set(s for s in scripts.list_scripts()))
+
+
+ at with_setup(setup_func, teardown_func)
+def test_load_legacy():
+    script = scripts.load_script('legacy')
+    assert script is not None
+    eq_('legacy', script['name'])
+    assert len(script['shortdesc']) > 0
+    pprint(script)
+    actions = scripts.verify(script, {}, external_check=False)
+    pprint(actions)
+    eq_([{'longdesc': '',
+          'name': 'apply_local',
+          'shortdesc': 'Configure SSH',
+          'text': '',
+          'value': 'configure.py ssh'},
+         {'longdesc': '',
+          'name': 'collect',
+          'shortdesc': 'Check state of nodes',
+          'text': '',
+          'value': 'collect.py'},
+         {'longdesc': '',
+          'name': 'validate',
+          'shortdesc': 'Verify parameters',
+          'text': '',
+          'value': 'verify.py'},
+         {'longdesc': '',
+          'name': 'apply',
+          'shortdesc': 'Install packages',
+          'text': '',
+          'value': 'configure.py install'},
+         {'longdesc': '',
+          'name': 'apply_local',
+          'shortdesc': 'Generate corosync authkey',
+          'text': '',
+          'value': 'authkey.py'},
+         {'longdesc': '',
+          'name': 'apply',
+          'shortdesc': 'Configure cluster nodes',
+          'text': '',
+          'value': 'configure.py corosync'},
+         {'longdesc': '',
+          'name': 'apply_local',
+          'shortdesc': 'Initialize cluster',
+          'text': '',
+          'value': 'init.py'}], actions)
+
+
+def test_load_workflow():
+    script = scripts.load_script('10-webserver')
+    assert script is not None
+    eq_('10-webserver', script['name'])
+    assert len(script['shortdesc']) > 0
+
+
+ at with_setup(setup_func, teardown_func)
+def test_v2():
+    script = scripts.load_script('v2')
+    assert script is not None
+    eq_('v2', script['name'])
+    assert len(script['shortdesc']) > 0
+
+    actions = scripts.verify(
+        script,
+        {'id': 'www',
+         'apache': {'id': 'apache'},
+         'virtual-ip': {'id': 'www-vip', 'ip': '192.168.1.100'},
+         'install': False}, external_check=False)
+    pprint(actions)
+    eq_(len(actions), 1)
+    assert str(actions[0]['text']).find('group www') >= 0
+
+    actions = scripts.verify(
+        script,
+        {'id': 'www',
+         'apache': {'id': 'apache'},
+         'virtual-ip': {'id': 'www-vip', 'ip': '192.168.1.100'},
+         'install': True}, external_check=False)
+    pprint(actions)
+    eq_(len(actions), 3)
+
+
+ at with_setup(setup_func, teardown_func)
+def test_agent_include():
+    inc2 = scripts.load_script('inc2')
+    actions = scripts.verify(
+        inc2,
+        {'wiz': 'abc',
+         'foo': 'cde',
+         'included-script': {'foo': True, 'bar': 'bah bah'}}, external_check=False)
+    pprint(actions)
+    eq_(len(actions), 6)
+    eq_('33\n\nabc', actions[-1]['text'].strip())
+
+
+ at with_setup(setup_func, teardown_func)
+def test_vipinc():
+    script = scripts.load_script('vipinc')
+    assert script is not None
+    actions = scripts.verify(
+        script,
+        {'vip': {'id': 'vop', 'ip': '10.0.0.4'}}, external_check=False)
+    eq_(len(actions), 1)
+    pprint(actions)
+    assert actions[0]['text'].find('primitive vop test:virtual-ip\n\tip="10.0.0.4"') >= 0
+    assert actions[0]['text'].find("clone c-vop vop") >= 0
+
+
+ at with_setup(setup_func, teardown_func)
+def test_value_replace_handles():
+    a = '''---
+- version: 2.2
+  category: Script
+  parameters:
+    - name: foo
+      value: bar
+'''
+    b = '''---
+- version: 2.2
+  category: Script
+  include:
+    - script: test-a
+      parameters:
+        - name: foo
+          value: "{{wiz}}+{{wiz}}"
+  parameters:
+    - name: wiz
+      required: true
+  actions:
+    - cib: "{{test-a:foo}}"
+'''
+
+    script_a = scripts.load_script_string('test-a', a)
+    script_b = scripts.load_script_string('test-b', b)
+    assert script_a is not None
+    assert script_b is not None
+    actions = scripts.verify(script_b,
+                             {'wiz': "SARUMAN"}, external_check=False)
+    eq_(len(actions), 1)
+    pprint(actions)
+    assert actions[0]['text'] == "SARUMAN+SARUMAN"
+
+
+ at with_setup(setup_func, teardown_func)
+def test_optional_step_ref():
+    """
+    It seems I have a bug in referencing ids from substeps.
+    """
+    a = '''---
+- version: 2.2
+  category: Script
+  include:
+    - agent: test:apache
+      name: apache
+      parameters:
+        - name: id
+          required: true
+'''
+    b = '''---
+- version: 2.2
+  category: Script
+  include:
+    - script: apache
+      required: false
+  parameters:
+    - name: wiz
+      required: true
+  actions:
+    - cib: "primitive {{wiz}} {{apache:id}}"
+'''
+
+    script_a = scripts.load_script_string('apache', a)
+    script_b = scripts.load_script_string('test-b', b)
+    assert script_a is not None
+    assert script_b is not None
+
+    actions = scripts.verify(script_a,
+                             {"id": "apacho"}, external_check=False)
+    eq_(len(actions), 1)
+    pprint(actions)
+    assert actions[0]['text'] == "primitive apacho test:apache"
+
+    #import ipdb
+    #ipdb.set_trace()
+    actions = scripts.verify(script_b,
+                             {'wiz': "SARUMAN", "apache": {"id": "apacho"}}, external_check=False)
+    eq_(len(actions), 1)
+    pprint(actions)
+    assert actions[0]['text'] == "primitive SARUMAN apacho"
+
+
+ at with_setup(setup_func, teardown_func)
+def test_enums_basic():
+    a = '''---
+- version: 2.2
+  category: Script
+  parameters:
+    - name: foo
+      required: true
+      type: enum
+      values:
+        - one
+        - two
+        - three
+  actions:
+    - cib: "{{foo}}"
+'''
+
+    script_a = scripts.load_script_string('test-a', a)
+    assert script_a is not None
+
+    actions = scripts.verify(script_a,
+                             {"foo": "one"}, external_check=False)
+    eq_(len(actions), 1)
+    pprint(actions)
+    assert actions[0]['text'] == "one"
+
+    actions = scripts.verify(script_a,
+                             {"foo": "three"}, external_check=False)
+    eq_(len(actions), 1)
+    pprint(actions)
+    assert actions[0]['text'] == "three"
+
+
+ at with_setup(setup_func, teardown_func)
+def test_enums_fail():
+    a = '''---
+- version: 2.2
+  category: Script
+  parameters:
+    - name: foo
+      required: true
+      type: enum
+      values:
+        - one
+        - two
+        - three
+  actions:
+    - cib: "{{foo}}"
+'''
+    script_a = scripts.load_script_string('test-a', a)
+    assert script_a is not None
+
+    def ver():
+        return scripts.verify(script_a, {"foo": "wrong"}, external_check=False)
+    assert_raises(ValueError, ver)
+
+
+ at with_setup(setup_func, teardown_func)
+def test_enums_fail2():
+    a = '''---
+- version: 2.2
+  category: Script
+  parameters:
+    - name: foo
+      required: true
+      type: enum
+  actions:
+    - cib: "{{foo}}"
+'''
+    script_a = scripts.load_script_string('test-a', a)
+    assert script_a is not None
+
+    def ver():
+        return scripts.verify(script_a, {"foo": "one"}, external_check=False)
+    assert_raises(ValueError, ver)
+
+
+ at with_setup(setup_func, teardown_func)
+def test_two_substeps():
+    """
+    There is a scoping bug
+    """
+    a = '''---
+- version: 2.2
+  category: Script
+  include:
+    - agent: test:apache
+      name: apache
+      parameters:
+        - name: id
+          required: true
+'''
+    b = '''---
+- version: 2.2
+  category: Script
+  include:
+    - script: apache
+      name: apache-a
+      required: true
+    - script: apache
+      name: apache-b
+      required: true
+  parameters:
+    - name: wiz
+      required: true
+  actions:
+    - include: apache-a
+    - include: apache-b
+    - cib: "primitive {{wiz}} {{apache-a:id}} {{apache-b:id}}"
+'''
+
+    script_a = scripts.load_script_string('apache', a)
+    script_b = scripts.load_script_string('test-b', b)
+    assert script_a is not None
+    assert script_b is not None
+
+    actions = scripts.verify(script_b,
+                             {'wiz': "head", "apache-a": {"id": "one"}, "apache-b": {"id": "two"}}, external_check=False)
+    eq_(len(actions), 1)
+    pprint(actions)
+    assert actions[0]['text'] == "primitive one test:apache\n\nprimitive two test:apache\n\nprimitive head one two"
+
+
+ at with_setup(setup_func, teardown_func)
+def test_required_subscript_params():
+    """
+    If an optional subscript has multiple required parameters,
+    excluding all = ok
+    excluding one = fail
+    """
+
+    a = '''---
+- version: 2.2
+  category: Script
+  parameters:
+    - name: foo
+      required: true
+      type: string
+    - name: bar
+      required: true
+      type: string
+  actions:
+    - cib: "{{foo}} {{bar}}"
+'''
+
+    b = '''---
+- version: 2.2
+  category: Script
+  include:
+    - script: foofoo
+      required: false
+  actions:
+    - include: foofoo
+    - cib: "{{foofoo:foo}} {{foofoo:bar}"
+'''
+
+    script_a = scripts.load_script_string('foofoo', a)
+    script_b = scripts.load_script_string('test-b', b)
+    assert script_a is not None
+    assert script_b is not None
+
+    def ver():
+        actions = scripts.verify(script_b,
+                                 {"foofoo": {"foo": "one"}}, external_check=False)
+        pprint(actions)
+    assert_raises(ValueError, ver)
+
+
+ at with_setup(setup_func, teardown_func)
+def test_unified():
+    unified = scripts.load_script('unified')
+    actions = scripts.verify(
+        unified,
+        {'id': 'foo',
+         'vip': {'id': 'bar', 'ip': '192.168.0.15'}}, external_check=False)
+    pprint(actions)
+    eq_(len(actions), 1)
+    eq_('primitive bar IPaddr2 ip=192.168.0.15\ngroup g-foo foo bar', actions[-1]['text'].strip())
diff --git a/test/unittests/test_time.py b/test/unittests/test_time.py
new file mode 100644
index 0000000..4d0cab9
--- /dev/null
+++ b/test/unittests/test_time.py
@@ -0,0 +1,17 @@
+# Copyright (C) 2014 Kristoffer Gronlund <kgronlund at suse.com>
+# See COPYING for license information.
+
+
+from crmsh import utils
+from crmsh import history
+from nose.tools import eq_
+import time
+import datetime
+import dateutil.tz
+
+
+def test_time_convert1():
+    loctz = dateutil.tz.tzlocal()
+    tm = time.localtime(utils.datetime_to_timestamp(utils.make_datetime_naive(datetime.datetime(2015, 6, 1, 10, 0, 0).replace(tzinfo=loctz))))
+    dt = utils.parse_time('Jun 01, 2015 10:00:00')
+    eq_(history.human_date(dt), time.strftime('%Y-%m-%d %H:%M:%S', tm))
diff --git a/test/unittests/test_utils.py b/test/unittests/test_utils.py
index 3634658..f2af61d 100644
--- a/test/unittests/test_utils.py
+++ b/test/unittests/test_utils.py
@@ -1,25 +1,12 @@
 # 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
+# See COPYING for license information.
 #
 # unit tests for utils.py
 
 import os
 from itertools import chain
-import utils
-import config
+from crmsh import utils
+from crmsh import config
 
 
 def test_systeminfo():
@@ -111,10 +98,8 @@ def test_sanity():
     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():
diff --git a/scripts/add/Makefile.am b/update-data-manifest.sh
old mode 100644
new mode 100755
similarity index 62%
rename from scripts/add/Makefile.am
rename to update-data-manifest.sh
index d01f5b8..b86e229
--- a/scripts/add/Makefile.am
+++ b/update-data-manifest.sh
@@ -1,28 +1,28 @@
-#
-# crmsh init script
-#
-# Copyright (C) 2014 Kristoffer Gronlund
+#!/bin/sh
+# Copyright (C) 2015 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)
-
+# Generate the data-manifest file which lists
+# all files which should be installed to /usr/share/crmsh
+target=data-manifest
+[ -f $target ] && (printf "Removing $target..."; rm $target)
+printf "Generating $target..."
+cat <<EOF | sort > $target
+version
+$(git ls-files scripts templates utils test)
+EOF
+[ ! -f $target ] && printf "FAILED\n"
+[ -f $target ] && printf "OK\n"
diff --git a/utils/Makefile.am b/utils/Makefile.am
deleted file mode 100644
index a8af219..0000000
--- a/utils/Makefile.am
+++ /dev/null
@@ -1,27 +0,0 @@
-#
-# 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_init.py b/utils/crm_init.py
index 50b85b8..3538453 100644
--- a/utils/crm_init.py
+++ b/utils/crm_init.py
@@ -82,7 +82,7 @@ def net_info():
     try:
         ip = socket.gethostbyname(hostname)
         ret['hostname'] = {'name': hostname, 'ip': ip}
-    except Exception, e:
+    except Exception as e:
         ret['hostname'] = {'error': str(e)}
     return ret
 
@@ -203,7 +203,7 @@ def install_packages(packages):
     for pkg in packages:
         try:
             crm_script.package(pkg, 'latest')
-        except Exception, e:
+        except Exception as e:
             crm_script.exit_fail("Failed to install %s: %s" % (pkg, e))
 
 
diff --git a/utils/crm_pkg.py b/utils/crm_pkg.py
index 2ffffe8..b77d7bf 100755
--- a/utils/crm_pkg.py
+++ b/utils/crm_pkg.py
@@ -1,20 +1,6 @@
 #!/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
-#
+# See COPYING for license information.
 
 import os
 import sys
@@ -178,7 +164,7 @@ class Yum(PackageManager):
 
         cmd = [self._yum,
                '--assumeyes',
-               '-d', 2,
+               '-d', '2',
                'install',
                name]
         rc, stdout, stderr = run(cmd)
@@ -195,7 +181,7 @@ class Yum(PackageManager):
         pre_version = self.get_version(name)
         cmd = [self._yum,
                '--assumeyes',
-               '-d', 2,
+               '-d', '2',
                'update',
                name]
         rc, stdout, stderr = run(cmd)
@@ -212,7 +198,7 @@ class Yum(PackageManager):
 
         cmd = [self._yum,
                '--assumeyes',
-               '-d', 2,
+               '-d', '2',
                'erase',
                name]
         rc, stdout, stderr = run(cmd)
diff --git a/utils/crm_rpmcheck.py b/utils/crm_rpmcheck.py
index acf91c5..aa81e75 100755
--- a/utils/crm_rpmcheck.py
+++ b/utils/crm_rpmcheck.py
@@ -1,20 +1,6 @@
 #!/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
-#
+# See COPYING for license information.
 
 import sys
 import json
diff --git a/version.in b/version.in
index a2446cf..d78bda9 100644
--- a/version.in
+++ b/version.in
@@ -1,2 +1 @@
 @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