[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