[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 @@
+[32mcrm(live)history# [0;10mtransition <1>
+[36mINFO[0;10m: running ptest with /var/cache/crm/history/live/sle12-c/pengine/pe-input-1907.bz2
+[36mINFO[0;10m: 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
+[32mApr 15 20:30:14 sle12-c crmd[1136]: notice: te_rsc_command: Initiating action 60: start small-apache_start_0 on sle12-a[0;10m
+[0;10mApr 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[0;10m
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 @@
+[32mcrm(live)history# [0;10mdiff -1 0 <1>
+--- -1
++++ 0
+@@ -11 +11 @@
+-[33mprimitive[0;10m [0;10msmall-apache[0;10m apache [33mparams[0;10m [36mconfigfile[0;10m=[31m"/etc/apache2/small.conf"[0;10m [33mmeta[0;10m [36mtarget-role[0;10m=[31mStopped[0;10m
++[33mprimitive[0;10m [0;10msmall-apache[0;10m apache [33mparams[0;10m [36mconfigfile[0;10m=[31m"/etc/apache2/small.conf"[0;10m [33mmeta[0;10m [36mtarget-role[0;10m=[31mStarted[0;10m
+[32mcrm(live)history# [0;10mdiff -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
+[32mcrm(live)history# [0;10mtimeframe "Apr 15 20:25" "Apr 15 20:35"
+[32mcrm(live)history# [0;10minfo
+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: [0;10msle12-a[0;10m [32msle12-c[0;10m
+Groups: nfs-srv nfs-disk
+Resources: s-libvirt p_drbd_nfs nfs-vg fs1 virtual-ip nfs-server websrv websrv-ip small-apache
+Transitions: [32m1906[0;10m [32m1907[0;10m
+[32mcrm(live)history# [0;10mpeinputs v
+Date Start End Filename Client User Origin
+==== ===== === ======== ====== ==== ======
+2015-04-15 20:29:59 20:30:01 [32mpe-input-1906[0;10m no-client no-user no-origin
+2015-04-15 20:30:14 20:30:15 [32mpe-input-1907[0;10m 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
+[36mINFO[0;10m: fetching new logs, please wait ...
+[32mDec 16 11:53:23 sle12-c nfsserver(nfs-server)[14911]: <1> ERROR: NFS server is up, but the locking daemons are down[0;10m
+[32mDec 16 11:53:23 sle12-c crmd[2823]: notice: te_rsc_command: Initiating action 54: stop nfs-server_stop_0 on sle12-a[0;10m
+[32mDec 16 11:53:23 sle12-c crmd[2823]: notice: te_rsc_command: Initiating action 3: stop nfs-server_stop_0 on sle12-c (local)[0;10m
+[32mDec 16 11:53:23 sle12-c nfsserver(nfs-server)[14944]: INFO: Stopping NFS server ...[0;10m
+[32mDec 16 11:53:23 sle12-c nfsserver(nfs-server)[14944]: INFO: Stopping sm-notify[0;10m
+[32mDec 16 11:53:23 sle12-c nfsserver(nfs-server)[14944]: INFO: Stopping rpc.statd[0;10m
+[32mDec 16 11:53:23 sle12-c nfsserver(nfs-server)[14944]: INFO: NFS server stopped[0;10m
+[32mDec 16 11:53:23 sle12-c crmd[2823]: notice: te_rsc_command: Initiating action 55: start nfs-server_start_0 on sle12-a[0;10m
+[0;10mDec 16 11:53:23 sle12-a nfsserver(nfs-server)[23255]: INFO: Stopping NFS server ...[0;10m
+[0;10mDec 16 11:53:23 sle12-a nfsserver(nfs-server)[23255]: INFO: Stopping sm-notify[0;10m
+[0;10mDec 16 11:53:23 sle12-a nfsserver(nfs-server)[23255]: INFO: Stopping rpc.statd[0;10m
+[0;10mDec 16 11:53:23 sle12-a nfsserver(nfs-server)[23255]: INFO: NFS server stopped[0;10m
+[0;10mDec 16 11:53:23 sle12-a nfsserver(nfs-server)[23320]: INFO: Starting NFS server ...[0;10m
+[0;10mDec 16 11:53:23 sle12-a nfsserver(nfs-server)[23320]: INFO: Starting rpc.statd.[0;10m
+[0;10mDec 16 11:53:24 sle12-a nfsserver(nfs-server)[23320]: INFO: executing sm-notify[0;10m
+[0;10mDec 16 11:53:24 sle12-a nfsserver(nfs-server)[23320]: INFO: NFS server started[0;10m
+[0;10mDec 16 11:53:24 sle12-a lrmd[6904]: <2> notice: operation_finished: nfs-server_start_0:23320:stderr [ id: rpcuser: no such user ][0;10m
+[0;10mDec 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 ]][0;10m
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
+[36mINFO[0;10m: Trace for nfs-server:monitor is written to /var/lib/heartbeat/trace_ra/
+[36mINFO[0;10m: 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 @@
+[32mcrm(live)history# [0;10mresource small-apache
+[32mApr 15 20:29:59 sle12-c crmd[1136]: notice: te_rsc_command: Initiating action 60: stop small-apache_stop_0 on sle12-a[0;10m
+[0;10mApr 15 20:29:59 sle12-a apache(small-apache)[1366]: INFO: Attempting graceful stop of apache PID 9155[0;10m
+[0;10mApr 15 20:30:01 sle12-a apache(small-apache)[1366]: INFO: apache stopped.[0;10m
+[0;10mApr 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[0;10m
+[32mApr 15 20:30:14 sle12-c crmd[1136]: notice: te_rsc_command: Initiating action 60: start small-apache_start_0 on sle12-a[0;10m
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
+[36mINFO[0;10m: fetching new logs, please wait ...
+[32mDec 19 14:36:18 sle12-c corosync[29551]: [MAIN ] Corosync Cluster Engine ('2.3.3'): started and ready to provide service.[0;10m
+[32mDec 19 14:36:19 sle12-c corosync[29545]: Starting Corosync Cluster Engine (corosync): [ OK ][0;10m
+[0;10mDec 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[0;10m
+[0;10mDec 19 14:36:20 sle12-a pengine[6906]: warning: stage6: Scheduling Node sle12-c for STONITH[0;10m
+[0;10mDec 19 14:36:20 sle12-a crmd[6907]: notice: te_fence_node: Executing reboot fencing operation (65) on sle12-c (timeout=60000)[0;10m
+[0;10mDec 19 14:36:20 sle12-a crmd[6907]: notice: peer_update_callback: Node return implies stonith of sle12-c (action 65) completed[0;10m
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 @@
+[32mcrm(live)history# [0;10mtransition log
+[36mINFO[0;10m: retrieving information from cluster nodes, please wait ...
+[32mApr 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 ][0;10m
+[32mApr 15 20:30:14 sle12-c stonithd[1132]: notice: unpack_config: On loss of CCM Quorum: Ignore[0;10m
+[32mApr 15 20:30:14 sle12-c pengine[1135]: notice: unpack_config: On loss of CCM Quorum: Ignore[0;10m
+[32mApr 15 20:30:14 sle12-c pengine[1135]: notice: LogActions: Start small-apache#011(sle12-a)[0;10m
+[32mApr 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[0;10m
+[32mApr 15 20:30:14 sle12-c crmd[1136]: notice: te_rsc_command: Initiating action 60: start small-apache_start_0 on sle12-a[0;10m
+[32mApr 15 20:30:14 sle12-c pengine[1135]: notice: process_pe_message: Calculated Transition 123: /var/lib/pacemaker/pengine/pe-input-1907.bz2[0;10m
+[0;10mApr 15 20:30:14 sle12-a stonithd[1160]: notice: unpack_config: On loss of CCM Quorum: Ignore[0;10m
+[0;10mApr 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[0;10m
+[0;10mApr 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)[0;10m
+[32mApr 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[0;10m
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