[Debian-ha-commits] [crmsh] 01/03: New upstream version 3.0.1

Valentin Vidic vvidic-guest at moszumanska.debian.org
Sun Jul 23 10:06:44 UTC 2017


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

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

commit 811d51b16797d323e1a34eaef5d0c9194b801a64
Author: Valentin Vidic <Valentin.Vidic at CARNet.hr>
Date:   Sun Jul 23 09:38:28 2017 +0200

    New upstream version 3.0.1
---
 .travis.yml                                       |   1 +
 ChangeLog                                         |  24 +++
 configure.ac                                      |   2 +-
 crmsh/bootstrap.py                                | 191 ++++++++++++++++------
 crmsh/cibconfig.py                                |   2 +-
 crmsh/corosync.py                                 |   9 +-
 crmsh/history.py                                  |   2 +-
 crmsh/parse.py                                    |  10 +-
 crmsh/ui_cluster.py                               | 136 +++++++++++----
 crmsh/utils.py                                    |  27 ++-
 doc/crm.8.adoc                                    |  26 +--
 doc/website-v1/index.adoc                         |   6 +-
 doc/website-v1/news.adoc                          |   5 +-
 doc/website-v1/news/2017-07-21-release-3_0_1.adoc |  44 +++++
 scripts/drbd/main.yml                             |   2 +
 scripts/health/collect.py                         |   6 +-
 scripts/health/main.yml                           |  15 +-
 scripts/lvm/main.yml                              |   5 +
 scripts/nfsserver-lvm-drbd/main.yml               |   2 +-
 scripts/virtual-ip/main.yml                       |   2 +-
 setup.py                                          |   2 +-
 test/unittests/test_parse.py                      |   4 +
 utils/crm_init.py                                 |   5 +-
 23 files changed, 399 insertions(+), 129 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 60defc8..cc6cd16 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,7 @@
 ---
 sudo: required
 dist: trusty
+group: deprecated-2017Q2
 language: python
 python:
   - "2.7_with_system_site_packages"
diff --git a/ChangeLog b/ChangeLog
index 9518ff1..af2c9d6 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,27 @@
+* Fri Jul 21 2017 Kristoffer Grönlund <kgronlund at suse.com> and many others
+- high: bootstrap: Add option to enable diskless SBD mode to cluster init (bsc#1045118)
+- high: cibconfig: Graph file output option was reversed (bsc#1036595)
+- medium: bootstrap: replace 'nodename' to 'seed_host'
+- medium: bootstrap: Fix watchdog SBD envvars (bsc#1045118)
+- medium: scripts: Relax broadcast IP validation (bsc#1044233)
+- medium: scripts: Clarify help text for NFS wizard (bsc#1044244)
+- medium: ui_cluster: Add --force to ha-cluster-remove (bsc#1044071)
+- medium: history: Revert preference of messages over ha-log.txt (bsc#1031138)
+- medium: bootstrap: Make arbitrator argument optional (bsc#1038386)
+- medium: bootstrap: Check required arguments to geo-join (bsc#1037421)
+- medium: bootstrap: Handle failure to fetch config gracefully (bsc#1037423)
+- medium: bootstrap: Enable "help geo-init" etc. (bsc#1037417)
+- medium: bootstrap: Set expected_votes based on actual node count (bsc#1033288)
+- medium: scripts/health: Make health script available as wizard (fate#320848) (fate#320866)
+- medium: ui_cluster: Fix init with no arguments (bsc#1028735)
+- low: utils: Use /proc for process discovery
+- low: bootstrap: Fix warning for formatting SBD device (bsc#1028704)
+- low: ui_cluster: when have an error for optparse, just return and stay at shell
+- low: ui_cluster: when use help option, do not exit, just print help messages and return
+- doc: geo-join requires --clusters argument (bsc#1037442)
+- remove bindnetaddr for unicast(bsc#1030437)
+- Allow empty fencing_topology (bsc#1025393)
+
 * Tue Jan 31 2017 Kristoffer Grönlund <kgronlund at suse.com> and many others
 - Release 3.0.0
 - high: bootstrap: Add bootstrap commands (fate#321114)
diff --git a/configure.ac b/configure.ac
index 10b25ab..28495fa 100644
--- a/configure.ac
+++ b/configure.ac
@@ -8,7 +8,7 @@ dnl License: GNU General Public License (GPL)
 
 AC_PREREQ([2.53])
 
-AC_INIT([crmsh],[3.0.0],[users at clusterlabs.org])
+AC_INIT([crmsh],[3.0.1],[users at clusterlabs.org])
 
 AC_ARG_WITH(version,
     [  --with-version=version   Override package version (if you're a packager needing to pretend) ],
diff --git a/crmsh/bootstrap.py b/crmsh/bootstrap.py
index 2b2e167..ce89c0d 100644
--- a/crmsh/bootstrap.py
+++ b/crmsh/bootstrap.py
@@ -53,6 +53,7 @@ class Context(object):
         self.ocfs2_device = None
         self.shared_device = None
         self.sbd_device = None
+        self.diskless_sbd = False  # if True, enable SBD for diskless operation
         self.unicast = None
         self.admin_ip = None
         self.watchdog = None
@@ -514,7 +515,9 @@ def init_cluster_local():
     if pass_msg:
         warn("You should change the hacluster password to something more secure!")
 
-    if configured_sbd_device():
+    # for cluster join, diskless_sbd flag is set in join_cluster() if
+    # sbd is running on seed host
+    if configured_sbd_device() or _context.diskless_sbd:
         invoke("systemctl enable sbd.service")
     else:
         invoke("systemctl disable sbd.service")
@@ -677,19 +680,13 @@ Configure Corosync (unicast):
         if not confirm("%s already exists - overwrite?" % (corosync.conf())):
             return
 
-    bindnetaddr = prompt_for_string(
-        'Network address to bind to (e.g.: 192.168.1.0)',
-        r'([0-9]+\.){3}[0-9]+', _context.ip_network)
-    if not bindnetaddr:
-        error("No value for bindnetaddr")
-
     mcastport = prompt_for_string('Port', '[0-9]+', "5405")
     if not mcastport:
         error("No value for mcastport")
 
     corosync.create_configuration(
         clustername=_context.cluster_name,
-        bindnetaddr=bindnetaddr,
+        bindnetaddr=None,
         mcastport=mcastport,
         transport="udpu")
     csync2_update(corosync.conf())
@@ -881,9 +878,9 @@ Configure Shared Storage:
     status("Created %s for OCFS2 partition" % (_context.ocfs2_device))
 
 
-def check_watchdog():
+def detect_watchdog_device():
     """
-    Verify watchdog device. Fall back to /dev/watchdog.
+    Find the watchdog device. Fall back to /dev/watchdog.
     """
     wdconf = "/etc/modules-load.d/watchdog.conf"
     watchdog_dev = "/dev/watchdog"
@@ -893,15 +890,65 @@ def check_watchdog():
             m = re.match(r'^\s*watchdog-device\s*=\s*(.*)$', line)
             if m:
                 watchdog_dev = m.group(1)
+    return watchdog_dev
+
+
+def check_watchdog():
+    """
+    Verify watchdog device. Fall back to /dev/watchdog.
+    """
+    watchdog_dev = detect_watchdog_device()
     rc, _out, _err = utils.get_stdout_stderr("wdctl %s" % (watchdog_dev))
     return rc == 0
 
 
+def sysconfig_comment_out(scfile, key):
+    """
+    Comments out the given key in the sysconfig file
+    """
+    matcher = re.compile(r'^\s*{}\s*='.format(key))
+    outp, ncomments = "", 0
+    for line in scfile.readlines():
+        if matcher.match(line):
+            outp += '#' + line
+            ncomments += 1
+        else:
+            outp += line
+    return outp, ncomments
+
+
+def init_sbd_diskless():
+    """
+    Initialize SBD in diskless mode.
+    """
+    status_long("Initializing diskless SBD...")
+    if os.path.isfile(SYSCONFIG_SBD):
+        log("Overwriting {} with diskless configuration".format(SYSCONFIG_SBD))
+        scfg, nmatches = sysconfig_comment_out(open(SYSCONFIG_SBD), "SBD_DEVICE")
+        if nmatches > 0:
+            utils.str2file(scfg, SYSCONFIG_SBD)
+    else:
+        log("Creating {} with diskless configuration".format(SYSCONFIG_SBD))
+    utils.sysconfig_set(SYSCONFIG_SBD,
+                        SBD_PACEMAKER="yes",
+                        SBD_STARTMODE="always",
+                        SBD_DELAY_START="no",
+                        SBD_WATCHDOG_DEV=detect_watchdog_device())
+    csync2_update(SYSCONFIG_SBD)
+    status_done()
+
+
 def init_sbd():
     """
     Configure SBD (Storage-based fencing).
+
+    SBD can also run in diskless mode if no device
+    is configured.
     """
-    if not _context.sbd_device:
+    if not _context.sbd_device and _context.diskless_sbd:
+        init_sbd_diskless()
+        return
+    elif not _context.sbd_device:
         # SBD device not set up by init_storage (ocfs2 template) and
         # also not passed in as command line argument - prompt user
         if _context.yes_to_all:
@@ -926,21 +973,25 @@ Configure SBD:
         if not confirm("Do you wish to use SBD?"):
             warn("Not configuring SBD - STONITH will be disabled.")
             # Comment out SBD devices if present
-            scfg = open(SYSCONFIG_SBD).read()
-            scfg, nmatches = re.subn(r'^\([^#].*\)$', r'#\1', scfg)
-            if nmatches > 0:
-                utils.str2file(scfg, SYSCONFIG_SBD)
-                csync2_update(SYSCONFIG_SBD)
+            if os.path.isfile(SYSCONFIG_SBD):
+                scfg, nmatches = sysconfig_comment_out(open(SYSCONFIG_SBD), "SBD_DEVICE")
+                if nmatches > 0:
+                    utils.str2file(scfg, SYSCONFIG_SBD)
+                    csync2_update(SYSCONFIG_SBD)
             return
 
         dev = ""
         dev_looks_sane = False
         while not dev_looks_sane:
-            dev = prompt_for_string('Path to storage device (e.g. /dev/disk/by-id/...)', r'\/.*', dev)
+            dev = prompt_for_string('Path to storage device (e.g. /dev/disk/by-id/...), or "none"', r'none|\/.*', dev)
+            if dev == "none":
+                _context.diskless_sbd = True
+                init_sbd_diskless()
+                return
             if not is_block_device(dev):
                 print >>sys.stderr, "    That doesn't look like a block device"
             else:
-                status("All data on $dev will be destroyed")
+                warn("All data on {} will be destroyed!".format(dev))
                 if confirm('Are you sure you wish to use this device'):
                     dev_looks_sane = True
                 else:
@@ -983,11 +1034,15 @@ op_defaults op-options: timeout=600 record-pending=true
 rsc_defaults rsc-options: resource-stickiness=1 migration-threshold=3
 """)
 
-    if utils.parse_sysconfig(SYSCONFIG_SBD).get("SBD_DEVICE"):
+    if configured_sbd_device():
         if not invoke("crm configure primitive stonith-sbd stonith:external/sbd pcmk_delay_max=30s"):
             error("Can't create stonith-sbd primitive")
         if not invoke("crm configure property stonith-enabled=true"):
-            error("Can't enable STONITH")
+            error("Can't enable STONITH for SBD")
+    elif _context.diskless_sbd:
+        # TODO: configure stonith-watchdog-timeout correctly
+        if not invoke("crm configure property stonith-enabled=true stonith-watchdog-timeout=5s"):
+            error("Can't enable STONITH for diskless SBD")
 
 
 def init_vgfs():
@@ -1240,17 +1295,44 @@ def join_cluster(seed_host):
     if is_unicast:
         corosync.add_node(utils.this_node())
 
-    # Increase expected_votes
-    new_quorum = 0
-    for v in corosync.get_values("quorum.expected_votes"):
-        new_quorum = int(v) + 1
-        corosync.set_value("quorum.expected_votes", str(new_quorum))
-    corosync.set_value("quorum.two_node", 1 if new_quorum == 2 else 0)
-    csync2_update(corosync.conf())
+    # if no SBD devices are configured,
+    # check the existing cluster if the sbd service is enabled
+    if not configured_sbd_device() and invoke("ssh root@{} systemctl is-enabled sbd.service".format(seed_host)):
+        _context.diskless_sbd = True
 
-    # ...now that that's out of the way, let's initialize the cluster.
+    # Initialize the cluster before adjusting quorum. This is so
+    # that we can query the cluster to find out how many nodes
+    # there are (so as not to adjust multiple times if a previous
+    # attempt to join the cluster failed)
     init_cluster_local()
 
+    def update_expected_votes():
+        # get a list of nodes, excluding remote nodes
+        nodelist = None
+        rc, nodelist_text = utils.get_stdout("cibadmin -Ql --xpath '/cib/status/node_state'")
+        if rc == 0:
+            try:
+                nodelist_xml = etree.fromstring(nodelist_text)
+                nodelist = [n.get('uname') for n in nodelist_xml.xpath('//node_state') if n.get('remote_node') != 'true']
+            except Exception:
+                pass
+
+        # Increase expected_votes
+        # TODO: wait to adjust expected_votes until after cluster join,
+        # so that we can ask the cluster for the current membership list
+        if nodelist is None:
+            nodecount = 0
+            for v in corosync.get_values("quorum.expected_votes"):
+                nodecount = int(v) + 1
+                corosync.set_value("quorum.expected_votes", str(nodecount))
+                corosync.set_value("quorum.two_node", 1 if nodecount == 2 else 0)
+        else:
+            nodecount = len(nodelist)
+            corosync.set_value("quorum.expected_votes", str(nodecount))
+            corosync.set_value("quorum.two_node", 1 if nodecount == 2 else 0)
+        csync2_update(corosync.conf())
+    update_expected_votes()
+
     # Trigger corosync config reload to ensure expected_votes is propagated
     invoke("corosync-cfgtool -R")
 
@@ -1312,7 +1394,7 @@ def remove_get_hostname(seed_host):
             _context.host_status = 2
     else:
         if seed_host not in xmlutil.listnodes():
-            error("Specified node {} is not configured in cluster, can not remove".format(nodename))
+            error("Specified node {} is not configured in cluster, can not remove".format(seed_host))
 
         warn("Could not resolve hostname {}".format(seed_host))
         nodename = prompt_for_string('Please enter the IP address of the node to be removed (e.g: 192.168.0.1)', r'([0-9]+\.){3}[0-9]+', "")
@@ -1397,7 +1479,7 @@ def remove_localhost_check():
 
 
 def bootstrap_init(cluster_name="hacluster", nic=None, ocfs2_device=None,
-                   shared_device=None, sbd_device=None, quiet=False,
+                   shared_device=None, sbd_device=None, diskless_sbd=False, quiet=False,
                    template=None, admin_ip=None, yes_to_all=False,
                    unicast=False, watchdog=None, stage=None, args=None):
     """
@@ -1405,6 +1487,7 @@ def bootstrap_init(cluster_name="hacluster", nic=None, ocfs2_device=None,
     -o <ocfs2-device>
     -p <shared-device>
     -s <sbd-device>
+    -S - configure SBD without disk
     -t <template>
     -A [<admin-ip>]
     -q - quiet
@@ -1430,6 +1513,7 @@ def bootstrap_init(cluster_name="hacluster", nic=None, ocfs2_device=None,
     _context.ocfs2_device = ocfs2_device
     _context.shared_device = shared_device
     _context.sbd_device = sbd_device
+    _context.diskless_sbd = diskless_sbd
     _context.unicast = unicast
     _context.admin_ip = admin_ip
     _context.watchdog = watchdog
@@ -1534,11 +1618,12 @@ def bootstrap_join(cluster_node=None, nic=None, quiet=False, yes_to_all=False, w
     status("Done (log saved to %s)" % (LOG_FILE))
 
 
-def bootstrap_remove(cluster_node=None, quiet=False, yes_to_all=False):
+def bootstrap_remove(cluster_node=None, quiet=False, yes_to_all=False, force=False):
     """
     -c <cluster-node> - node to remove from cluster
     -q - quiet
     -y - yes to all
+    -f - force removal of self
     """
     global _context
     _context = Context(quiet=quiet, yes_to_all=yes_to_all)
@@ -1556,7 +1641,7 @@ def bootstrap_remove(cluster_node=None, quiet=False, yes_to_all=False):
     init()
     remove_ssh()
     if remove_localhost_check():
-        if not config.core.force:
+        if not config.core.force and not force:
             error("Removing self requires --force")
         # get list of cluster nodes
         me = utils.this_node()
@@ -1614,7 +1699,7 @@ def create_booth_authkey():
 def create_booth_config(arbitrator, clusters, tickets):
     status("Configure booth")
 
-    config_template = Template("""# The booth configuration file is "/etc/booth/booth.conf". You need to
+    config_template = """# The booth configuration file is "/etc/booth/booth.conf". You need to
 # prepare the same booth configuration file on each arbitrator and
 # each node in the cluster sites where the booth daemon can be launched.
 
@@ -1622,21 +1707,16 @@ def create_booth_config(arbitrator, clusters, tickets):
 # Currently only "UDP" is supported.
 transport="UDP"
 port="9929"
-arbitrator="$arbitrator"
-$sites
-authfile="$authkey"
-$tickets
-""")
-    ticket_template = Template("""ticket="$name"
-     expire="600"
-""")
-    site_template = Template('site="$site"\n')
-
-    cfg = config_template.substitute(
-        arbitrator=arbitrator,
-        sites="".join(site_template.substitute(site=s) for s in clusters.itervalues()),
-        authkey=BOOTH_AUTH,
-        tickets="".join(ticket_template.substitute(name=t) for t in tickets))
+"""
+    cfg = [config_template]
+    if arbitrator is not None:
+        cfg.append("arbitrator=\"{}\"".format(arbitrator))
+    for s in clusters.itervalues():
+        cfg.append("site=\"{}\"".format(s))
+    cfg.append("authfile=\"{}\"".format(BOOTH_AUTH))
+    for t in tickets:
+        cfg.append("ticket=\"{}\"\nexpire=\"600\"".format(t))
+    cfg = "\n".join(cfg) + "\n"
 
     if os.path.exists(BOOTH_CFG):
         invoke("rm -f {}".format(BOOTH_CFG))
@@ -1677,12 +1757,15 @@ def geo_fetch_config(node):
     status("Retrieving configuration - This may prompt for root@%s:" % (node))
     tmpdir = tmpfiles.create_dir()
     invoke("scp root@%s:'/etc/booth/*' %s/" % (node, tmpdir))
-    if os.path.isfile("%s/authkey" % (tmpdir)):
-        invoke("mv %s/authkey %s" % (tmpdir, BOOTH_AUTH))
-    if os.path.isfile("%s/booth.conf" % (tmpdir)):
-        invoke("mv %s/booth.conf %s" % (tmpdir, BOOTH_CFG))
-    os.chmod(BOOTH_AUTH, 0o600)
-    os.chmod(BOOTH_CFG, 0o644)
+    try:
+        if os.path.isfile("%s/authkey" % (tmpdir)):
+            invoke("mv %s/authkey %s" % (tmpdir, BOOTH_AUTH))
+            os.chmod(BOOTH_AUTH, 0o600)
+        if os.path.isfile("%s/booth.conf" % (tmpdir)):
+            invoke("mv %s/booth.conf %s" % (tmpdir, BOOTH_CFG))
+            os.chmod(BOOTH_CFG, 0o644)
+    except OSError as err:
+        raise ValueError("Problem encountered with booth configuration from {}: {}".format(node, err))
 
 
 def geo_cib_config(clusters):
diff --git a/crmsh/cibconfig.py b/crmsh/cibconfig.py
index 8476aab..0d0151d 100644
--- a/crmsh/cibconfig.py
+++ b/crmsh/cibconfig.py
@@ -380,7 +380,7 @@ class CibObjectSet(object):
         rc, d = utils.load_graphviz_file(userdir.GRAPHVIZ_USER_FILE)
         if rc and d:
             constants.graph = d
-        if outf is not None:
+        if outf is None:
             return self.show_graph(gtype)
         elif gtype == ftype:
             rc = self.save_graph(gtype, outf)
diff --git a/crmsh/corosync.py b/crmsh/corosync.py
index b72d8fc..5908b18 100644
--- a/crmsh/corosync.py
+++ b/crmsh/corosync.py
@@ -483,7 +483,7 @@ totem {
 
     interface {
         ringnumber:     0
-        bindnetaddr:    %(bindnetaddr)s
+        %(bindnetaddr)s
 %(mcast)s
         ttl:        1
     }
@@ -541,10 +541,15 @@ def create_configuration(clustername="hacluster",
     if transport is not None:
         transport_tmpl = "    transport: {}\n".format(transport)
 
+    if bindnetaddr is None:
+        bindnetaddr_tmpl = ""
+    else:
+        bindnetaddr_tmpl = "bindnetaddr: %s" % bindnetaddr
+
     config = {
         "clustername": clustername,
         "nodelist": nodelist_tmpl,
-        "bindnetaddr": bindnetaddr,
+        "bindnetaddr": bindnetaddr_tmpl,
         "mcast": mcast_tmpl,
         "transport": transport_tmpl,
     }
diff --git a/crmsh/history.py b/crmsh/history.py
index 5a2a467..0129754 100644
--- a/crmsh/history.py
+++ b/crmsh/history.py
@@ -18,7 +18,7 @@ from . import utils
 from .msg import common_debug, common_warn, common_err, common_error, common_info, warn_once
 
 
-_LOG_FILES = ("messages", "ha-log.txt", "ha-log", "cluster-log.txt", "journal.log", "pacemaker.log")
+_LOG_FILES = ("ha-log.txt", "messages", "ha-log", "cluster-log.txt", "journal.log", "pacemaker.log")
 
 
 #
diff --git a/crmsh/parse.py b/crmsh/parse.py
index b06f78b..93dad0a 100644
--- a/crmsh/parse.py
+++ b/crmsh/parse.py
@@ -1070,7 +1070,7 @@ class FencingOrderParser(BaseParser):
 
     """
     def parse(self, cmd):
-        self.begin(cmd, min_args=1)
+        self.begin(cmd)
         if not self.try_match("fencing-topology"):
             self.match("fencing_topology")
         target = "@@"
@@ -1085,15 +1085,17 @@ class FencingOrderParser(BaseParser):
                 target = self.matched(1)
             else:
                 raw_levels.append((target, self.match_any()))
-        if len(raw_levels) == 0:
-            self.err("Missing list of devices")
         return self._postprocess_levels(raw_levels)
 
     def _postprocess_levels(self, raw_levels):
         from collections import defaultdict
         from itertools import repeat
         from .cibconfig import cib_factory
-        if raw_levels[0][0] == "@@":
+        if len(raw_levels) == 0:
+            def no_levels():
+                return []
+            lvl_generator = no_levels
+        elif raw_levels[0][0] == "@@":
             def node_levels():
                 for node in cib_factory.node_id_list():
                     for target, devices in raw_levels:
diff --git a/crmsh/ui_cluster.py b/crmsh/ui_cluster.py
index 762e6bf..784143e 100644
--- a/crmsh/ui_cluster.py
+++ b/crmsh/ui_cluster.py
@@ -104,8 +104,9 @@ class Cluster(command.UI):
         def looks_like_hostnames(lst):
             sectionlist = bootstrap.INIT_STAGES
             return all(not (l.startswith('-') or l in sectionlist) for l in lst)
-        if '--dry-run' in args or looks_like_hostnames(args):
-            args = ['--yes', '--nodes'] + [arg for arg in args if arg != '--dry-run']
+        if len(args) > 0:
+            if '--dry-run' in args or looks_like_hostnames(args):
+                args = ['--yes', '--nodes'] + [arg for arg in args if arg != '--dry-run']
         parser = OptParser(usage="usage: init [options] [STAGE]", epilog="""
 
 Stage can be one of:
@@ -128,7 +129,9 @@ Note:
     To use storage you have already configured, pass -s and -o to specify
     the block devices for SBD and OCFS2, and the automatic partitioning
     will be skipped.
-""")
+""", add_help_option=False)
+
+        parser.add_option("-h", "--help",action="store_true", dest="help", help="Show this help message")
         parser.add_option("-q", "--quiet", action="store_true", dest="quiet",
                           help="Be quiet (don't describe what's happening, just do it)")
         parser.add_option("-y", "--yes", action="store_true", dest="yes_to_all",
@@ -141,6 +144,8 @@ Note:
                           help='Additional nodes to add to the created cluster. ' +
                           'May include the current node, which will always be the initial cluster node.')
         # parser.add_option("--quick-start", dest="quickstart", action="store_true", help="Perform basic system configuration (NTP, watchdog, /etc/hosts)")
+        parser.add_option("-S", "--enable-sbd", dest="diskless_sbd", action="store_true",
+                          help="Enable SBD even if no SBD device is configured (diskless mode)")
         parser.add_option("-w", "--watchdog", dest="watchdog", metavar="WATCHDOG",
                           help="Use the given watchdog device")
 
@@ -162,8 +167,13 @@ Note:
         storage_group.add_option("-o", "--ocfs2-device", dest="ocfs2_device", metavar="DEVICE",
                                  help='Block device to use for OCFS2 (only used in "vgfs" stage)')
         parser.add_option_group(storage_group)
-
-        options, args = parser.parse_args(list(args))
+        try:
+            options, args = parser.parse_args(list(args))
+        except:
+            return
+        if options.help:
+            parser.print_help()
+            return
 
         stage = ""
         if len(args):
@@ -186,6 +196,7 @@ Note:
             ocfs2_device=options.ocfs2_device,
             shared_device=options.shared_device,
             sbd_device=options.sbd_device,
+            diskless_sbd=options.diskless_sbd,
             quiet=options.quiet,
             template=options.template,
             admin_ip=options.admin_ip,
@@ -224,7 +235,8 @@ Stage can be one of:
     cluster     Start the cluster on this node
 
 If stage is not specified, each stage will be invoked in sequence.
-""")
+""", add_help_option=False)
+        parser.add_option("-h", "--help",action="store_true", dest="help", help="Show this help message")
         parser.add_option("-q", "--quiet", help="Be quiet (don't describe what's happening, just do it)", action="store_true", dest="quiet")
         parser.add_option("-y", "--yes", help='Answer "yes" to all prompts (use with caution)', action="store_true", dest="yes_to_all")
         parser.add_option("-w", "--watchdog", dest="watchdog", metavar="WATCHDOG", help="Use the given watchdog device")
@@ -233,8 +245,13 @@ If stage is not specified, each stage will be invoked in sequence.
         network_group.add_option("-c", "--cluster-node", dest="cluster_node", help="IP address or hostname of existing cluster node", metavar="HOST")
         network_group.add_option("-i", "--interface", dest="nic", help="Bind to IP address on interface IF", metavar="IF")
         parser.add_option_group(network_group)
-
-        options, args = parser.parse_args(list(args))
+        try:
+            options, args = parser.parse_args(list(args))
+        except:
+            return
+        if options.help:
+            parser.print_help()
+            return
 
         stage = ""
         if len(args) == 1:
@@ -270,9 +287,16 @@ If stage is not specified, each stage will be invoked in sequence.
         Installs packages, sets up corosync and pacemaker, etc.
         Must be executed from a node in the existing cluster.
         '''
-        parser = OptParser(usage="usage: add [options] [node ...]")
+        parser = OptParser(usage="usage: add [options] [node ...]", add_help_option=False)
+        parser.add_option("-h", "--help",action="store_true", dest="help", help="Show this help message")
         parser.add_option("-y", "--yes", help='Answer "yes" to all prompts (use with caution)', action="store_true", dest="yes_to_all")
-        options, args = parser.parse_args(list(args))
+        try:
+            options, args = parser.parse_args(list(args))
+        except:
+            return
+        if options.help:
+            parser.print_help()
+            return
         for node in args:
             if not self._add_node(node, yes_to_all=options.yes_to_all):
                 return False
@@ -283,12 +307,19 @@ If stage is not specified, each stage will be invoked in sequence.
         '''
         Remove the given node(s) from the cluster.
         '''
-        parser = OptParser(usage="usage: remove [options] [<node> ...]")
+        parser = OptParser(usage="usage: remove [options] [<node> ...]", add_help_option=False)
+        parser.add_option("-h", "--help",action="store_true", dest="help", help="Show this help message")
         parser.add_option("-q", "--quiet", help="Be quiet (don't describe what's happening, just do it)", action="store_true", dest="quiet")
         parser.add_option("-y", "--yes", help='Answer "yes" to all prompts (use with caution)', action="store_true", dest="yes_to_all")
         parser.add_option("-c", "--cluster-node", dest="cluster_node", help="IP address or hostname of cluster node which will be deleted", metavar="HOST")
-
-        options, args = parser.parse_args(list(args))
+        parser.add_option("-F", "--force", dest="force", help="Remove current node", action="store_true")
+        try:
+            options, args = parser.parse_args(list(args))
+        except:
+            return
+        if options.help:
+            parser.print_help()
+            return
         if options.cluster_node is not None and options.cluster_node not in args:
             args = list(args) + [options.cluster_node]
         if len(args) == 0:
@@ -301,25 +332,36 @@ If stage is not specified, each stage will be invoked in sequence.
                 bootstrap.bootstrap_remove(
                     cluster_node=node,
                     quiet=options.quiet,
-                    yes_to_all=options.yes_to_all)
+                    yes_to_all=options.yes_to_all,
+                    force=options.force)
         return True
 
     def _parse_clustermap(self, clusters):
+        '''
+        Helper function to parse the cluster map into a dictionary:
+
+        name=ip; name2=ip2 -> { name: ip, name2: ip2 }
+        '''
+        if clusters is None:
+            return None
         try:
             return dict([re.split('[=:]+', o) for o in re.split('[ ,;]+', clusters)])
+        except TypeError:
+            return None
         except ValueError:
             return None
 
-    @command.name("geo-init")
+    @command.name("geo_init")
+    @command.alias("geo-init")
     @command.skill_level('administrator')
     def do_geo_init(self, context, *args):
         '''
         Make this cluster a geo cluster.
         Needs some information to set up.
 
-        * arbitrator IP / hostname
         * cluster map: "cluster-name=ip cluster-name=ip"
-        * list of tickets
+        * arbitrator IP / hostname (optional)
+        * list of tickets (can be empty)
         '''
         parser = OptParser(usage="usage: geo-init [options]", epilog="""
 
@@ -336,20 +378,25 @@ Cluster Description
 
   Name clusters using the --name parameter to
   crm bootstrap init.
-""")
+""", add_help_option=False)
+        parser.add_option("-h", "--help",action="store_true", dest="help", help="Show this help message")
         parser.add_option("-q", "--quiet", help="Be quiet (don't describe what's happening, just do it)", action="store_true", dest="quiet")
         parser.add_option("-y", "--yes", help='Answer "yes" to all prompts (use with caution)', action="store_true", dest="yes_to_all")
-        parser.add_option("--arbitrator", help="IP address of geo cluster arbitrator", dest="arbitrator", metavar="IP")
-        parser.add_option("--clusters", help="Cluster description (see details below)", dest="clusters", metavar="DESC")
-        parser.add_option("--tickets", help="Tickets to create (space-separated)", dest="tickets", metavar="LIST")
-        options, args = parser.parse_args(list(args))
-
-        if options.clusters is None or options.arbitrator is None:
+        parser.add_option("-a", "--arbitrator", help="IP address of geo cluster arbitrator", dest="arbitrator", metavar="IP")
+        parser.add_option("-s", "--clusters", help="Geo cluster description (see details below)", dest="clusters", metavar="DESC")
+        parser.add_option("-t", "--tickets", help="Tickets to create (space-separated)", dest="tickets", metavar="LIST")
+        try:
+            options, args = parser.parse_args(list(args))
+        except:
+            return
+        if options.help:
+            parser.print_help()
+            return
+
+        if options.clusters is None:
             errs = []
             if options.clusters is None:
                 errs.append("The --clusters argument is required.")
-            if options.arbitrator is None:
-                errs.append("The --arbitrator argument is required.")
             parser.error(" ".join(errs))
 
         clustermap = self._parse_clustermap(options.clusters)
@@ -364,35 +411,58 @@ Cluster Description
         bootstrap.bootstrap_init_geo(options.quiet, options.yes_to_all, options.arbitrator, clustermap, ticketlist)
         return True
 
-    @command.name("geo-join")
+    @command.name("geo_join")
+    @command.alias("geo-join")
     @command.skill_level('administrator')
     def do_geo_join(self, context, *args):
         '''
         Join this cluster to a geo configuration.
         '''
-        parser = OptParser(usage="usage: geo-join [options]")
+        parser = OptParser(usage="usage: geo-join [options]", add_help_option=False)
+        parser.add_option("-h", "--help",action="store_true", dest="help", help="Show this help message")
         parser.add_option("-q", "--quiet", help="Be quiet (don't describe what's happening, just do it)", action="store_true", dest="quiet")
         parser.add_option("-y", "--yes", help='Answer "yes" to all prompts (use with caution)', action="store_true", dest="yes_to_all")
         parser.add_option("-c", "--cluster-node", help="IP address of an already-configured geo cluster or arbitrator", dest="node", metavar="IP")
-        parser.add_option("--clusters", help="Cluster description (see geo-init for details)", dest="clusters", metavar="DESC")
-        options, args = parser.parse_args(list(args))
+        parser.add_option("-s", "--clusters", help="Geo cluster description (see geo-init for details)", dest="clusters", metavar="DESC")
+        try:
+            options, args = parser.parse_args(list(args))
+        except:
+            return
+        if options.help:
+            parser.print_help()
+            return
+        errs = []
+        if options.node is None:
+            errs.append("The --cluster-node argument is required.")
+        if options.clusters is None:
+            errs.append("The --clusters argument is required.")
+        if len(errs) > 0:
+            parser.error(" ".join(errs))
         clustermap = self._parse_clustermap(options.clusters)
         if clustermap is None:
             parser.error("Invalid cluster description format")
         bootstrap.bootstrap_join_geo(options.quiet, options.yes_to_all, options.node, clustermap)
         return True
 
-    @command.name("geo-init-arbitrator")
+    @command.name("geo_init_arbitrator")
+    @command.alias("geo-init-arbitrator")
     @command.skill_level('administrator')
     def do_geo_init_arbitrator(self, context, *args):
         '''
         Make this node a geo arbitrator.
         '''
-        parser = OptParser(usage="usage: geo-init-arbitrator [options]")
+        parser = OptParser(usage="usage: geo-init-arbitrator [options]", add_help_option=False)
+        parser.add_option("-h", "--help",action="store_true", dest="help", help="Show this help message")
         parser.add_option("-q", "--quiet", help="Be quiet (don't describe what's happening, just do it)", action="store_true", dest="quiet")
         parser.add_option("-y", "--yes", help='Answer "yes" to all prompts (use with caution)', action="store_true", dest="yes_to_all")
         parser.add_option("-c", "--cluster-node", help="IP address of an already-configured geo cluster", dest="other", metavar="IP")
-        options, args = parser.parse_args(list(args))
+        try:
+            options, args = parser.parse_args(list(args))
+        except:
+            return
+        if options.help:
+            parser.print_help()
+            return
         bootstrap.bootstrap_arbitrator(options.quiet, options.yes_to_all, options.other)
         return True
 
diff --git a/crmsh/utils.py b/crmsh/utils.py
index e48cef5..8b2d48d 100644
--- a/crmsh/utils.py
+++ b/crmsh/utils.py
@@ -952,14 +952,25 @@ def is_int(s):
 
 
 def is_process(s):
-    cmd = "ps -e -o pid,command | grep -qs '%s'" % s
-    if options.regression_tests:
-        print ".EXT", cmd
-    proc = subprocess.Popen(cmd,
-                            shell=True,
-                            stdout=subprocess.PIPE)
-    proc.wait()
-    return proc.returncode == 0
+    """
+    Returns true if argument is the name of a running process.
+
+    s: process name
+    returns Boolean
+    """
+    from os.path import join, basename
+    # find pids of running processes
+    pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
+    for pid in pids:
+        try:
+            cmdline = open(join('/proc', pid, 'cmdline'), 'rb').read()
+            procname = basename(cmdline.replace('\x00', ' ').split(' ')[0])
+            if procname == s:
+                return True
+        except os.error:
+            # a process may have died since we got the list of pids
+            pass
+    return False
 
 
 def print_stacktrace():
diff --git a/doc/crm.8.adoc b/doc/crm.8.adoc
index ee771ea..7191494 100644
--- a/doc/crm.8.adoc
+++ b/doc/crm.8.adoc
@@ -996,13 +996,13 @@ Options:
 *-y, --yes*::
     Answer "yes" to all prompts (use with caution)
 
-*--arbitrator=IP*::
-    IP address of geo cluster arbitrator
+*-s DESC, --clusters=DESC*::
+    Geo cluster description (see details below)
 
-*--clusters=DESC*::
-    Cluster description (see details below)
+*-a IP, --arbitrator=IP*::
+    IP address of geo cluster arbitrator (optional)
 
-*--tickets=LIST*::
+*-t LIST, --tickets=LIST*::
     Tickets to create (space-separated)
 
 
@@ -1036,9 +1036,6 @@ to get the geo cluster configuration.
 
 Options:
 
-*--clusters=DESC*::
-    Cluster description (see +geo-init+ for details)
-
 *-c IP, --cluster-node=IP*::
     IP address of an already-configured geo cluster
 
@@ -1063,12 +1060,18 @@ an existing cluster.
 
 Options:
 
+*-s DESC, --clusters=DESC*::
+    Geo cluster description (see +geo-init+ for details)
+
 *-c IP, --cluster-node=IP*::
-    IP address of an already-configured geo cluster or arbitrator
+    IP address of an already-configured geo cluster node
+    or arbitrator. This argument can also be a virtual IP
+    as long as it resolves to a node in an existing geo
+    cluster.
 
 Usage:
 ...............
-geo-join [options]
+geo-join --cluster-node <node> --clusters <description>
 ...............
 
 
@@ -1115,6 +1118,9 @@ Options:
 *-w WATCHDOG, --watchdog=WATCHDOG*::
     Use the given watchdog device.
 
+*-S, --enable-sbd*::
+    Enable SBD even if no SBD device is configured (diskless mode).
+
 Network configuration:
 
 Options for configuring the network and messaging layer.
diff --git a/doc/website-v1/index.adoc b/doc/website-v1/index.adoc
index b9a05b4..1c10093 100644
--- a/doc/website-v1/index.adoc
+++ b/doc/website-v1/index.adoc
@@ -18,7 +18,7 @@ package management, and history exploration tools giving you a complete
 insight into the state of your cluster.
 
 * https://github.com/ClusterLabs/crmsh/[Source Code]
-* http://crmsh.github.io/man-3/[Reference Manual (v3.0.0)]
-* http://crmsh.github.io/man-2.0/[Reference Manual (v2.3.2]
+* http://crmsh.github.io/man-3/[Reference Manual (v3.0.1)]
+* http://crmsh.github.io/man-2.0/[Reference Manual (v2.3.2)]
 * https://build.opensuse.org/package/show/network:ha-clustering:Stable/crmsh[Packages]
-* http://clusterlabs.org[Cluster Labs]
\ No newline at end of file
+* http://clusterlabs.org[Cluster Labs]
diff --git a/doc/website-v1/news.adoc b/doc/website-v1/news.adoc
index 9cd0d6d..65ea71a 100644
--- a/doc/website-v1/news.adoc
+++ b/doc/website-v1/news.adoc
@@ -1,14 +1,15 @@
 = News
 
-link:/news/2017-01-31-release-3_0_0[2017-01-31 10:00]
+link:/news/2017-07-21-release-3_0_1[2017-07-21 11:00]
 
 :leveloffset: 1
 
-include::news/2017-01-31-release-3_0_0.adoc[]
+include::news/2017-07-21-release-3_0_1.adoc[]
 
 :leveloffset: 0
 
 ''''
+* link:/news/2017-01-31-release-3_0_0[2017-01-31 10:00 Releasing crmsh version 3.0.0]
 * link:/news/2016-09-05-release-2_2_2[2016-09-05 19:00 Releasing crmsh version 2.2.2]
 * link:/news/2016-09-02-release-2_3_1[2016-09-02 10:00 Releasing crmsh version 2.3.1]
 * link:/news/2016-09-01-release-2_1_7[2016-09-01 09:00 Announcing crmsh stable release 2.1.7]
diff --git a/doc/website-v1/news/2017-07-21-release-3_0_1.adoc b/doc/website-v1/news/2017-07-21-release-3_0_1.adoc
new file mode 100644
index 0000000..60a8b9f
--- /dev/null
+++ b/doc/website-v1/news/2017-07-21-release-3_0_1.adoc
@@ -0,0 +1,44 @@
+Releasing crmsh version 3.0.1
+=============================
+:Author: Kristoffer Gronlund
+:Email: kgronlund at suse.com
+:Date: 2017-07-21 11:00
+
+Hello everyone!
+
+I'm happy to announce the release of crmsh version 3.0.1 today. This
+is mainly a bug fix release, so no new exciting features and mainly
+fixes to the new bootstrap functionality added in 3.0.0.
+
+I would also like to take the opportinity to introduce a new core
+developer for crmsh, Xin Liang! For this release he has contributed
+some of the bug fixes discovered, but he has also contributed a
+rewrite of hb_report into Python, as well as worked on improving the
+tab completion support in crmsh. I also want to recognize the hard
+work of Shiwen Zhang who initially started the work of rewriting the
+hb_report script in Python.
+
+For the complete list of changes in this release, see the ChangeLog:
+
+* https://github.com/ClusterLabs/crmsh/blob/3.0.1/ChangeLog
+
+The source code can be downloaded from Github:
+
+* https://github.com/ClusterLabs/crmsh/releases/tag/3.0.1
+
+This version of crmsh (or a version very close to it) is already
+available in openSUSE Tumbleweed, and packages for several popular
+Linux distributions will be available 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/3.0.1.tar.gz
+* https://github.com/ClusterLabs/crmsh/archive/3.0.1.zip
+
+As usual, a huge thank you to all contributors and users of crmsh!
+
+Cheers,
+Kristoffer
diff --git a/scripts/drbd/main.yml b/scripts/drbd/main.yml
index afb4f8b..29ba472 100644
--- a/scripts/drbd/main.yml
+++ b/scripts/drbd/main.yml
@@ -6,6 +6,8 @@ longdesc: >-
 
   Also creates a multistate resource managing the state of DRBD.
 
+  Does not create or modify the referenced DRBD configuration.
+
 parameters:
   - name: id
     shortdesc: DRBD Cluster Resource ID
diff --git a/scripts/health/collect.py b/scripts/health/collect.py
index bb87368..15c2e5c 100755
--- a/scripts/health/collect.py
+++ b/scripts/health/collect.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python
 import os
+import pwd
 import hashlib
 import platform
 import crm_script
@@ -16,6 +17,9 @@ def rpm_info():
 def logrotate_info():
     return {}
 
+def get_user():
+    return pwd.getpwuid(os.getuid()).pw_name
+
 def sys_info():
     sysname, nodename, release, version, machine = os.uname()
     #The first three columns measure CPU and IO utilization of the
@@ -38,7 +42,7 @@ def sys_info():
             'distname': distname,
             'distver': distver,
             'distid': distid,
-            'user': os.getlogin(),
+            'user': get_user(),
             'hostname': hostname,
             'uptime': uptime[0],
             'idletime': uptime[1],
diff --git a/scripts/health/main.yml b/scripts/health/main.yml
index 327fa17..7c59bdd 100644
--- a/scripts/health/main.yml
+++ b/scripts/health/main.yml
@@ -1,11 +1,16 @@
 version: 2.2
-category: Script
-shortdesc: Check the health of the cluster
-longdesc: Runs various checks to verify the health of the cluster nodes
+category: Basic
+shortdesc: Verify health and configuration
+longdesc: |
+  Checks and detects issues with the cluster, by creating and
+  analysing a cluster report.
+
+  Requires SSH access between cluster nodes. This command is
+  also available from the command line as "crm cluster health".
 actions:
   - collect: collect.py
-    shortdesc: Collect cluster information
+    shortdesc: Collect information
   - apply_local: hahealth.py
-    shortdesc: Run HA health check
+    shortdesc: Run cluster health check
   - report: report.py
     shortdesc: Report cluster state
diff --git a/scripts/lvm/main.yml b/scripts/lvm/main.yml
index ecde524..381f56c 100644
--- a/scripts/lvm/main.yml
+++ b/scripts/lvm/main.yml
@@ -1,5 +1,10 @@
 version: 2.2
 category: Script
+longdesc: >-
+  Configure a resource for managing an LVM volume group.
+
+  Does not create the referenced volume group.
+
 include:
   - agent: ocf:heartbeat:LVM
     name: lvm
diff --git a/scripts/nfsserver-lvm-drbd/main.yml b/scripts/nfsserver-lvm-drbd/main.yml
index 7a2a005..233f26f 100644
--- a/scripts/nfsserver-lvm-drbd/main.yml
+++ b/scripts/nfsserver-lvm-drbd/main.yml
@@ -28,7 +28,7 @@ longdesc: |
   For more details on what needs to be prepared to use
   this wizard, see the Highly Available NFS Storage with
   DRBD and Pacemaker section of the SUSE Linux Enterprise
-  High Availability Extension 12 SP1 documentation.
+  High Availability Extension documentation.
 
 parameters:
   - name: nfsserver_id
diff --git a/scripts/virtual-ip/main.yml b/scripts/virtual-ip/main.yml
index 697bf74..1ccb19e 100644
--- a/scripts/virtual-ip/main.yml
+++ b/scripts/virtual-ip/main.yml
@@ -15,7 +15,7 @@ include:
         type: integer
         required: false
       - name: broadcast
-        type: ip_address
+        type: string
         required: false
     ops: |
       op start timeout="20" op stop timeout="20"
diff --git a/setup.py b/setup.py
index 5cff15c..71ccddb 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@
 from setuptools import setup
 
 setup(name='crmsh',
-      version='3.0.0',
+      version='3.0.1',
       description='Command-line interface for High-Availability cluster management',
       author='Kristoffer Gronlund',
       author_email='kgronlund at suse.com',
diff --git a/test/unittests/test_parse.py b/test/unittests/test_parse.py
index febdd38..6b32a08 100644
--- a/test/unittests/test_parse.py
+++ b/test/unittests/test_parse.py
@@ -434,6 +434,10 @@ class TestCliParser(unittest.TestCase):
     def test_fencing(self):
         # num test nodes are 3
 
+        out = self._parse('fencing_topology')
+        expect = '<fencing-topology/>'
+        self.assertEqual(expect, etree.tostring(out))
+
         out = self._parse('fencing_topology poison-pill power')
         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))
diff --git a/utils/crm_init.py b/utils/crm_init.py
index 3538453..f695b6d 100644
--- a/utils/crm_init.py
+++ b/utils/crm_init.py
@@ -1,4 +1,5 @@
 import os
+import pwd
 import re
 import platform
 import socket
@@ -43,6 +44,8 @@ def services_info():
     'check enabled/active services'
     return [service_info(service) for service in SERVICES]
 
+def get_user():
+    return pwd.getpwuid(os.getuid()).pw_name
 
 def sys_info():
     'system information'
@@ -58,7 +61,7 @@ def sys_info():
             'distname': distname,
             'distver': distver,
             'distid': distid,
-            'user': os.getlogin(),
+            'user': get_user(),
             'hostname': hostname,
             'fqdn': socket.getfqdn()}
 

-- 
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