From 57615d576dc54045b03c000fed6c66a6275eb770 Mon Sep 17 00:00:00 2001
From: liangxin1300 <XLiang@suse.com>
Date: Fri, 8 May 2020 15:14:10 +0800
Subject: [PATCH] High: bootstrap: using class SBDManager for sbd configuration
 and management(bsc#1170037, bsc#1170999)

---
 crmsh/bootstrap.py  | 402 +++++++++++++++++++++++++-------------------
 crmsh/ui_cluster.py |   6 +-
 2 files changed, 236 insertions(+), 172 deletions(-)

diff --git a/crmsh/bootstrap.py b/crmsh/bootstrap.py
index 39a6f4f8..c6c6274d 100644
--- a/crmsh/bootstrap.py
+++ b/crmsh/bootstrap.py
@@ -58,7 +58,6 @@ class Context(object):
         self.yes_to_all = None
         self.template = None
         self.cluster_name = None
-        self.diskless_sbd = None
         self.watchdog = None
         self.no_overwrite_sshkey = None
         self.nic = None
@@ -72,7 +71,6 @@ class Context(object):
         self.qdevice_algo = None
         self.qdevice_tie_breaker = None
         self.shared_device = None
-        self.sbd_device = None
         self.ocfs2_device = None
         self.cluster_node = None
         self.force = None
@@ -81,6 +79,9 @@ class Context(object):
         self.tickets = None
         self.ip_address = None
         self.ip_network = None
+        self.sbd_manager = None
+        self.sbd_devices = None
+        self.diskless_sbd = None
 
     @classmethod
     def set_context(cls, options):
@@ -98,6 +99,229 @@ class Context(object):
                 algo=self.qdevice_algo,
                 tie_breaker=self.qdevice_tie_breaker)
 
+    def init_sbd_manager(self):
+        self.sbd_manager = SBDManager(self.sbd_devices, self.diskless_sbd)
+
+
+class SBDManager(object):
+    """
+    Class to manage sbd configuration and services
+    """
+    if os.path.exists("/usr/share/fillup-templates"):
+        SYSCONFIG_SBD_TEMPLATE = "/usr/share/fillup-templates/sysconfig.sbd"
+    else:
+        SYSCONFIG_SBD_TEMPLATE = "/var/adm/fillup-templates/sysconfig.sbd"
+
+    SBD_STATUS_DESCRIPTION = """
+Configure SBD:
+  If you have shared storage, for example a SAN or iSCSI target,
+  you can use it avoid split-brain scenarios by configuring SBD.
+  This requires a 1 MB partition, accessible to all nodes in the
+  cluster.  The device path must be persistent and consistent
+  across all nodes in the cluster, so /dev/disk/by-id/* devices
+  are a good choice.  Note that all data on the partition you
+  specify here will be destroyed.
+"""
+
+    def __init__(self, sbd_devices=None, diskless_sbd=False):
+        """
+        Init function
+
+        sbd_devices is provided by '-s' option on init process
+        diskless_sbd is provided by '-S' option on init process
+        """
+        self.sbd_devices_input = sbd_devices
+        self.diskless_sbd = diskless_sbd
+        self._sbd_service_flag = False
+        self._sbd_devices = None
+
+    @staticmethod
+    def _check_environment():
+        """
+        Check prerequisites for SBD
+        """
+        if not check_watchdog():
+            error("Watchdog device must be configured in order to use SBD")
+        if not utils.is_program("sbd"):
+            error("sbd executable not found! Cannot configure SBD")
+
+    def _parse_sbd_device(self):
+        """
+        Parse sbd devices, possible command line is like:
+          -s "/dev/sdb1;/dev/sdb2"
+          -s /dev/sdb1 -s /dev/sbd2
+        """
+        result_list = []
+        for dev in self.sbd_devices_input:
+            if ';' in dev:
+                result_list.extend(dev.strip(';').split(';'))
+            else:
+                result_list.append(dev)
+        return result_list
+
+    @staticmethod
+    def _verify_sbd_device(dev_list):
+        """
+        Verify sbd device
+        """
+        if len(dev_list) > 3:
+            raise ValueError("Maximum number of SBD device is 3")
+        for dev in dev_list:
+            if not is_block_device(dev):
+                raise ValueError("{} doesn't look like a block device".format(dev))
+
+    def _get_sbd_device_interactive(self):
+        """
+        Get sbd device on interactive mode
+        """
+        if _context.yes_to_all:
+            warn("Not configuring SBD (%s left untouched)." % (SYSCONFIG_SBD))
+            return
+
+        status(self.SBD_STATUS_DESCRIPTION)
+
+        if not confirm("Do you wish to use SBD?"):
+            warn("Not configuring SBD - STONITH will be disabled.")
+            return
+
+        self._check_environment()
+
+        configured_dev = self._get_sbd_device_from_config()
+        if configured_dev and not confirm("SBD is already configured to use {} - overwrite?".format(';'.join(configured_dev))):
+            return configured_dev
+
+        dev_list = []
+        dev_looks_sane = False
+        while not dev_looks_sane:
+            dev = prompt_for_string('Path to storage device (e.g. /dev/disk/by-id/...), or "none" for diskless sbd, use ";" as separator for multi path', r'none|\/.*')
+            if dev == "none":
+                self.diskless_sbd = True
+                return
+            dev_list = dev.strip(';').split(';')
+            try:
+                self._verify_sbd_device(dev_list)
+            except ValueError as err_msg:
+                print(term.render(clidisplay.error(str(err_msg))))
+                continue
+            for dev_item in dev_list:
+                warn("All data on {} will be destroyed!".format(dev_item))
+                if confirm('Are you sure you wish to use this device?'):
+                    dev_looks_sane = True
+                else:
+                    dev_looks_sane = False
+                    break
+
+        return dev_list
+
+    def _get_sbd_device(self):
+        """
+        Get sbd device from options or interactive mode
+        """
+        dev_list = []
+        if self.sbd_devices_input:
+            dev_list = self._parse_sbd_device()
+            self._verify_sbd_device(dev_list)
+            self._check_environment()
+        elif self.diskless_sbd:
+            self._check_environment()
+        else:
+            dev_list = self._get_sbd_device_interactive()
+        self._sbd_devices = dev_list
+
+    def _initialize_sbd(self):
+        """
+        Initialize SBD device
+        """
+        if self.diskless_sbd:
+            return
+        for dev in self._sbd_devices:
+            if not invoke("sbd -d {} create".format(dev)):
+                error("Failed to initialize SBD device {}".format(dev))
+
+    def _update_configuration(self):
+        """
+        Update /etc/sysconfig/sbd
+        """
+        shutil.copyfile(self.SYSCONFIG_SBD_TEMPLATE, SYSCONFIG_SBD)
+        sbd_config_dict = {
+                "SBD_PACEMAKER": "yes",
+                "SBD_STARTMODE": "always",
+                "SBD_DELAY_START": "no",
+                "SBD_WATCHDOG_DEV": detect_watchdog_device()
+                }
+        if self._sbd_devices:
+            sbd_config_dict["SBD_DEVICE"] = ';'.join(self._sbd_devices)
+        utils.sysconfig_set(SYSCONFIG_SBD, **sbd_config_dict)
+        csync2_update(SYSCONFIG_SBD)
+
+    @staticmethod
+    def _get_sbd_device_from_config():
+        """
+        Gets currently configured SBD device, i.e. what's in /etc/sysconfig/sbd
+        """
+        conf = utils.parse_sysconfig(SYSCONFIG_SBD)
+        res = conf.get("SBD_DEVICE")
+        if res:
+            return res.strip(';').split(';')
+        else:
+            return None
+
+    def sbd_init(self):
+        """
+        Function sbd_init includes these steps:
+        1. Get sbd device from options or interactive mode
+        2. Initialize sbd device
+        3. Write config file /etc/sysconfig/sbd
+        """
+        self._get_sbd_device()
+        if not self._sbd_devices and not self.diskless_sbd:
+            return
+        status_long("Initializing {}SBD...".format("diskless " if self.diskless_sbd else ""))
+        self._initialize_sbd()
+        self._update_configuration()
+        status_done()
+        # If process work through here, consider it's ready for enable service
+        self._sbd_service_flag = True
+
+    def manage_sbd_service(self):
+        """
+        Manage sbd service, running on both init and join process
+        """
+        if self._sbd_service_flag:
+            invoke("systemctl enable sbd.service")
+        else:
+            invoke("systemctl disable sbd.service")
+
+    def configure_sbd_resource(self):
+        """
+        Configure stonith-sbd resource and stonith-enabled property
+        """
+        if self._sbd_devices and self._get_sbd_device_from_config():
+            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 for SBD")
+        elif self.diskless_sbd:
+            if not invoke("crm configure property stonith-enabled=true stonith-watchdog-timeout=5s"):
+                error("Can't enable STONITH for diskless SBD")
+
+    def join_sbd(self, peer_host):
+        """
+        Function join_sbd running on join process only
+        On joining process, check whether peer node has enabled sbd.service
+        If so, check prerequisites of SBD and verify sbd device on join node
+        """
+        if not os.path.exists(SYSCONFIG_SBD):
+            return
+        if not invoke("ssh -o StrictHostKeyChecking=no root@{} systemctl is-enabled sbd.service".format(peer_host)):
+            return
+        self._check_environment()
+        dev_list = self._get_sbd_device_from_config()
+        if dev_list:
+            self._verify_sbd_device(dev_list)
+        status("Got {}SBD configuration".format("" if dev_list else "diskless "))
+        self._sbd_service_flag = True
+
 
 _context = None
 
@@ -413,14 +637,6 @@ def probe_partitions():
     status_done()
 
 
-def configured_sbd_device():
-    """
-    Gets currently configured SBD device, i.e. what's in /etc/sysconfig/sbd
-    """
-    conf = utils.parse_sysconfig(SYSCONFIG_SBD)
-    return conf.get("SBD_DEVICE")
-
-
 def check_tty():
     """
     Check for pseudo-tty: Cannot display read prompts without a TTY (bnc#892702)
@@ -680,12 +896,7 @@ def init_cluster_local():
     if pass_msg:
         warn("You should change the hacluster password to something more secure!")
 
-    # for cluster join, diskless_sbd flag is set in join_cluster() if
-    # sbd is running on seed host
-    if (configured_sbd_device() and _context.sbd_device) or _context.diskless_sbd:
-        invoke("systemctl enable sbd.service")
-    else:
-        invoke("systemctl disable sbd.service")
+    _context.sbd_manager.manage_sbd_service()
 
     start_service("pacemaker.service")
     wait_for_cluster()
@@ -1190,8 +1401,7 @@ def is_block_device(dev):
     from stat import S_ISBLK
     try:
         rc = S_ISBLK(os.stat(dev).st_mode)
-    except OSError as msg:
-        warn(msg)
+    except OSError:
         return False
     return rc
 
@@ -1344,42 +1554,6 @@ def check_watchdog():
     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).
@@ -1387,108 +1561,7 @@ def init_sbd():
     SBD can also run in diskless mode if no device
     is configured.
     """
-    def get_dev_list(dev_list):
-        result_list = []
-        for dev in dev_list:
-            if ';' in dev:
-                result_list.extend(dev.strip(';').split(';'))
-            else:
-                result_list.append(dev)
-        return result_list
-
-    # non-interactive case
-    if _context.sbd_device:
-        _context.sbd_device = get_dev_list(_context.sbd_device)
-        if len(_context.sbd_device) > 3:
-            error("Maximum number of SBD device is 3")
-        for dev in _context.sbd_device:
-            if not is_block_device(dev):
-                error("{} doesn't look like a block device".format(dev))
-    # diskless sbd
-    elif _context.diskless_sbd:
-        init_sbd_diskless()
-        return
-    # interactive case
-    else:
-        # 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:
-            warn("Not configuring SBD (%s left untouched)." % (SYSCONFIG_SBD))
-            return
-        status("""
-Configure SBD:
-  If you have shared storage, for example a SAN or iSCSI target,
-  you can use it avoid split-brain scenarios by configuring SBD.
-  This requires a 1 MB partition, accessible to all nodes in the
-  cluster.  The device path must be persistent and consistent
-  across all nodes in the cluster, so /dev/disk/by-id/* devices
-  are a good choice.  Note that all data on the partition you
-  specify here will be destroyed.
-""")
-
-        if not confirm("Do you wish to use SBD?"):
-            warn("Not configuring SBD - STONITH will be disabled.")
-            # Comment out SBD devices if present
-            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
-
-        if not check_watchdog():
-            error("Watchdog device must be configured if want to use SBD!")
-
-        if utils.is_program("sbd") is None:
-            error("sbd executable not found! Cannot configure SBD.")
-
-        configured_dev = configured_sbd_device()
-        if configured_dev:
-            if not confirm("SBD is already configured to use %s - overwrite?" % (configured_dev)):
-                return
-
-        dev_looks_sane = False
-        while not dev_looks_sane:
-            dev = prompt_for_string('Path to storage device (e.g. /dev/disk/by-id/...), or "none", use ";" as separator for multi path', r'none|\/.*')
-            if dev == "none":
-                _context.diskless_sbd = True
-                init_sbd_diskless()
-                return
-            dev_list = dev.strip(';').split(';')
-            if len(dev_list) > 3:
-                error("Maximum number of SBD device is 3")
-                continue
-            for dev_item in dev_list:
-                if not is_block_device(dev_item):
-                    error("{} doesn't look like a block device".format(dev_item))
-                    dev_looks_sane = False
-                    break
-                else:
-                    warn("All data on {} will be destroyed!".format(dev_item))
-                    if confirm('Are you sure you wish to use this device?'):
-                        dev_looks_sane = True
-                    else:
-                        dev_looks_sane = False
-                        break
-
-        _context.sbd_device = dev_list
-
-    # TODO: need to ensure watchdog is available
-    # (actually, should work if watchdog unavailable, it'll just whine in the logs...)
-    # TODO: what about timeouts for multipath devices?
-    status_long('Initializing SBD...')
-    for dev in _context.sbd_device:
-        if not invoke("sbd -d %s create" % (dev)):
-            error("Failed to initialize SBD device %s" % (dev))
-    status_done()
-
-    utils.sysconfig_set(SYSCONFIG_SBD,
-                        SBD_DEVICE=';'.join(_context.sbd_device),
-                        SBD_PACEMAKER="yes",
-                        SBD_STARTMODE="always",
-                        SBD_DELAY_START="no",
-                        SBD_WATCHDOG_DEV=detect_watchdog_device())
-    csync2_update(SYSCONFIG_SBD)
+    _context.sbd_manager.sbd_init()
 
 
 def init_cluster():
@@ -1512,15 +1585,7 @@ op_defaults op-options: timeout=600 record-pending=true
 rsc_defaults rsc-options: resource-stickiness=1 migration-threshold=3
 """)
 
-    if configured_sbd_device() and _context.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 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")
+    _context.sbd_manager.configure_sbd_resource()
 
 
 def init_vgfs():
@@ -1847,10 +1912,7 @@ def join_cluster(seed_host):
         csync2_update(corosync.conf())
         invoke("ssh -o StrictHostKeyChecking=no root@{} corosync-cfgtool -R".format(seed_host))
 
-    # if no SBD devices are configured,
-    # check the existing cluster if the sbd service is enabled
-    if not configured_sbd_device() and invoke("ssh -o StrictHostKeyChecking=no root@{} systemctl is-enabled sbd.service".format(seed_host)):
-        _context.diskless_sbd = True
+    _context.sbd_manager.join_sbd(seed_host)
 
     if ipv6_flag and not is_unicast:
         # for ipv6 mcast
@@ -2157,6 +2219,7 @@ def bootstrap_init(context):
     global _context
     _context = context
     _context.init_qdevice()
+    _context.init_sbd_manager()
 
     stage = _context.stage
     if stage is None:
@@ -2219,6 +2282,7 @@ def bootstrap_join(context):
     """
     global _context
     _context = context
+    _context.init_sbd_manager()
 
     check_tty()
 
diff --git a/crmsh/ui_cluster.py b/crmsh/ui_cluster.py
index 8a48b4ee..71929559 100644
--- a/crmsh/ui_cluster.py
+++ b/crmsh/ui_cluster.py
@@ -242,7 +242,7 @@ Note:
         storage_group = parser.add_argument_group("Storage configuration", "Options for configuring shared storage.")
         storage_group.add_argument("-p", "--partition-device", dest="shared_device", metavar="DEVICE",
                                    help='Partition this shared storage device (only used in "storage" stage)')
-        storage_group.add_argument("-s", "--sbd-device", dest="sbd_device", metavar="DEVICE", action="append",
+        storage_group.add_argument("-s", "--sbd-device", dest="sbd_devices", metavar="DEVICE", action="append",
                                    help="Block device to use for SBD fencing, use \";\" as separator or -s multiple times for multi path (up to 3 devices)")
         storage_group.add_argument("-o", "--ocfs2-device", dest="ocfs2_device", metavar="DEVICE",
                                    help='Block device to use for OCFS2 (only used in "vgfs" stage)')
@@ -257,8 +257,8 @@ Note:
         if stage not in bootstrap.INIT_STAGES and stage != "":
             parser.error("Invalid stage (%s)" % (stage))
 
-        if options.template and options.template != "ocfs2":
-            parser.error("Invalid template (%s)" % (options.template))
+        if options.sbd_devices and options.diskless_sbd:
+            parser.error("Can't use -s and -S options together")
 
         # if options.geo and options.name == "hacluster":
         #    parser.error("For a geo cluster, each cluster must have a unique name (use --name to set)")
-- 
2.21.1

