Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package crmsh for openSUSE:Factory checked in at 2021-07-02 13:27:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/crmsh (Old) and /work/SRC/openSUSE:Factory/.crmsh.new.2625 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "crmsh" Fri Jul 2 13:27:57 2021 rev:216 rq:903599 version:4.3.1+20210702.314a7eb4 Changes: -------- --- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes 2021-06-24 18:23:02.568947407 +0200 +++ /work/SRC/openSUSE:Factory/.crmsh.new.2625/crmsh.changes 2021-07-02 13:29:00.068046430 +0200 @@ -1,0 +2,19 @@ +Fri Jul 02 02:08:01 UTC 2021 - [email protected] + +- Update to version 4.3.1+20210702.314a7eb4: + * Fix: resource: make untrace consistent with trace (bsc#1187396) + +------------------------------------------------------------------- +Wed Jun 30 02:46:57 UTC 2021 - [email protected] + +- Update to version 4.3.1+20210630.bff856e3: + * Fix: bootstrap: check for missing fields in 'crm_node -l' output (bsc#1182131) + +------------------------------------------------------------------- +Mon Jun 28 08:47:48 UTC 2021 - [email protected] + +- Update to version 4.3.1+20210628.3128d590: + * Dev: unittest: add unit test for previous changes + * Dev: sbd: enable SBD_DELAY_START in virtualization environment + +------------------------------------------------------------------- Old: ---- crmsh-4.3.1+20210624.c64d3a07.tar.bz2 New: ---- crmsh-4.3.1+20210702.314a7eb4.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ crmsh.spec ++++++ --- /var/tmp/diff_new_pack.4Iy2ET/_old 2021-07-02 13:29:00.604042271 +0200 +++ /var/tmp/diff_new_pack.4Iy2ET/_new 2021-07-02 13:29:00.608042240 +0200 @@ -36,7 +36,7 @@ Summary: High Availability cluster command-line interface License: GPL-2.0-or-later Group: %{pkg_group} -Version: 4.3.1+20210624.c64d3a07 +Version: 4.3.1+20210702.314a7eb4 Release: 0 URL: http://crmsh.github.io Source0: %{name}-%{version}.tar.bz2 ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.4Iy2ET/_old 2021-07-02 13:29:00.644041961 +0200 +++ /var/tmp/diff_new_pack.4Iy2ET/_new 2021-07-02 13:29:00.644041961 +0200 @@ -9,6 +9,6 @@ </service> <service name="tar_scm"> <param name="url">https://github.com/ClusterLabs/crmsh.git</param> - <param name="changesrevision">8ec9634ece1b3e388b0c5f303e5021f7241fc90a</param> + <param name="changesrevision">314a7eb4ed5e72ae103b0edcdbbf89cb4484004e</param> </service> </servicedata> \ No newline at end of file ++++++ crmsh-4.3.1+20210624.c64d3a07.tar.bz2 -> crmsh-4.3.1+20210702.314a7eb4.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.3.1+20210624.c64d3a07/crmsh/bootstrap.py new/crmsh-4.3.1+20210702.314a7eb4/crmsh/bootstrap.py --- old/crmsh-4.3.1+20210624.c64d3a07/crmsh/bootstrap.py 2021-06-24 04:19:10.000000000 +0200 +++ new/crmsh-4.3.1+20210702.314a7eb4/crmsh/bootstrap.py 2021-07-02 03:25:42.000000000 +0200 @@ -1671,9 +1671,21 @@ error("Can't fetch cluster nodes list from {}: {}".format(init_node, err)) cluster_nodes_list = [] for line in out.splitlines(): - _, node, stat = line.split() - if stat == "member": - cluster_nodes_list.append(node) + # Parse line in format: <id> <nodename> <state>, and collect the + # nodename. + tokens = line.split() + if len(tokens) == 0: + pass # Skip any spurious empty line. + elif len(tokens) < 3: + warn("Unable to configure passwordless ssh with nodeid {}. The " + "node has no known name and/or state information".format( + tokens[0])) + elif tokens[2] != "member": + warn("Skipping configuration of passwordless ssh with node {} in " + "state '{}'. The node is not a current member".format( + tokens[1], tokens[2])) + else: + cluster_nodes_list.append(tokens[1]) # Filter out init node from cluster_nodes_list cmd = "ssh {} root@{} hostname".format(SSH_OPTION, init_node) @@ -1809,9 +1821,16 @@ nodeid_dict = {} _rc, outp, _ = utils.get_stdout_stderr("crm_node -l") if _rc == 0: - for line in outp.split('\n'): - tmp = line.split() - nodeid_dict[tmp[1]] = tmp[0] + for line in outp.splitlines(): + tokens = line.split() + if len(tokens) == 0: + pass # Skip any spurious empty line. + elif len(tokens) < 3: + warn("Unable to update configuration for nodeid {}. " + "The node has no known name and/or state " + "information".format(tokens[0])) + else: + nodeid_dict[tokens[1]] = tokens[0] # apply nodelist in cluster if is_unicast or is_qdevice_configured: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.3.1+20210624.c64d3a07/crmsh/sbd.py new/crmsh-4.3.1+20210702.314a7eb4/crmsh/sbd.py --- old/crmsh-4.3.1+20210624.c64d3a07/crmsh/sbd.py 2021-06-24 04:19:10.000000000 +0200 +++ new/crmsh-4.3.1+20210702.314a7eb4/crmsh/sbd.py 2021-07-02 03:25:42.000000000 +0200 @@ -164,7 +164,7 @@ sbd_config_dict = { "SBD_PACEMAKER": "yes", "SBD_STARTMODE": "always", - "SBD_DELAY_START": "no", + "SBD_DELAY_START": "yes" if utils.detect_virt() and self._sbd_devices else "no", "SBD_WATCHDOG_DEV": self._watchdog_inst.watchdog_device_name } if self._sbd_watchdog_timeout > 0: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.3.1+20210624.c64d3a07/crmsh/ui_resource.py new/crmsh-4.3.1+20210702.314a7eb4/crmsh/ui_resource.py --- old/crmsh-4.3.1+20210624.c64d3a07/crmsh/ui_resource.py 2021-06-24 04:19:10.000000000 +0200 +++ new/crmsh-4.3.1+20210702.314a7eb4/crmsh/ui_resource.py 2021-07-02 03:25:42.000000000 +0200 @@ -644,7 +644,8 @@ return rsc.add_operation(n) def _trace_resource(self, context, rsc_id, rsc): - op_nodes = rsc.node.xpath('.//op') + """Enable RA tracing for a specified resource.""" + op_nodes = rsc.node.xpath('operations/op') def trace(name): for o in op_nodes: @@ -661,7 +662,8 @@ rsc.set_op_attr(op_node, constants.trace_ra_attr, "1") def _trace_op(self, context, rsc_id, rsc, op): - op_nodes = rsc.node.xpath('.//op[@name="%s"]' % (op)) + """Enable RA tracing for a specified operation.""" + op_nodes = rsc.node.xpath('operations/op[@name="%s"]' % (op)) if not op_nodes: if op == 'monitor': context.fatal_error("No monitor operation configured for %s" % (rsc_id)) @@ -671,6 +673,7 @@ rsc.set_op_attr(op_node, constants.trace_ra_attr, "1") def _trace_op_interval(self, context, rsc_id, rsc, op, interval): + """Enable RA tracing for an operation with the exact interval.""" op_node = xmlutil.find_operation(rsc.node, op, interval) if op_node is None and utils.crm_msec(interval) != 0: context.fatal_error("Operation %s with interval %s not found in %s" % (op, interval, rsc_id)) @@ -711,12 +714,36 @@ return True def _remove_trace(self, rsc, op_node): - from lxml import etree common_debug("op_node: %s" % (xmlutil.xml_tostring(op_node))) op_node = rsc.del_op_attr(op_node, constants.trace_ra_attr) if rsc.is_dummy_operation(op_node): rsc.del_operation(op_node) + def _untrace_resource(self, context, rsc_id, rsc): + """Disable RA tracing for a specified resource.""" + op_nodes = rsc.node.xpath( + 'operations/op[instance_attributes/nvpair[@name="%s"]]' % + (constants.trace_ra_attr)) + for op_node in op_nodes: + self._remove_trace(rsc, op_node) + + def _untrace_op(self, context, rsc_id, rsc, op): + """Disable RA tracing for a specified operation.""" + op_nodes = rsc.node.xpath('operations/op[@name="%s"]' % (op)) + if not op_nodes: + context.fatal_error("Operation %s not found in %s" % (op, rsc_id)) + for op_node in op_nodes: + self._remove_trace(rsc, op_node) + + def _untrace_op_interval(self, context, rsc_id, rsc, op, interval): + """Disable RA tracing for an operation with the exact interval.""" + op_node = xmlutil.find_operation(rsc.node, op, interval) + if op_node is None: + context.fatal_error( + "Operation %s with interval %s not found in %s" % + (op, interval, rsc_id)) + self._remove_trace(rsc, op_node) + @command.completers(compl.primitives, _raoperations) def do_untrace(self, context, rsc_id, op=None, interval=None): 'usage: untrace <rsc> [<op>] [<interval>]' @@ -725,19 +752,12 @@ return False if op == "probe": op = "monitor" + if interval is None: + interval = "0" if op is None: - n = 0 - for tn in rsc.node.xpath('.//*[@%s]' % (constants.trace_ra_attr)): - self._remove_trace(rsc, tn) - n += 1 - for tn in rsc.node.xpath('.//*[@name="%s"]' % (constants.trace_ra_attr)): - if tn.getparent().getparent().tag == 'op': - self._remove_trace(rsc, tn.getparent().getparent()) - n += 1 + self._untrace_resource(context, rsc_id, rsc) + elif interval is None: + self._untrace_op(context, rsc_id, rsc, op) else: - op_node = xmlutil.find_operation(rsc.node, op, interval=interval) - if op_node is None: - common_err("operation %s does not exist in %s" % (op, rsc.obj_id)) - return False - self._remove_trace(rsc, op_node) + self._untrace_op_interval(context, rsc_id, rsc, op, interval) return cib_factory.commit() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.3.1+20210624.c64d3a07/crmsh/utils.py new/crmsh-4.3.1+20210702.314a7eb4/crmsh/utils.py --- old/crmsh-4.3.1+20210624.c64d3a07/crmsh/utils.py 2021-06-24 04:19:10.000000000 +0200 +++ new/crmsh-4.3.1+20210702.314a7eb4/crmsh/utils.py 2021-07-02 03:25:42.000000000 +0200 @@ -2888,4 +2888,12 @@ if not res: raise ValueError("Cannot find qdevice sync timeout") return int(int(res.group(1))/1000) + + +def detect_virt(): + """ + Detect if running in virt environment + """ + rc, _, _ = get_stdout_stderr("systemd-detect-virt") + return rc == 0 # vim:ts=4:sw=4:et: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.3.1+20210624.c64d3a07/data-manifest new/crmsh-4.3.1+20210702.314a7eb4/data-manifest --- old/crmsh-4.3.1+20210624.c64d3a07/data-manifest 2021-06-24 04:19:10.000000000 +0200 +++ new/crmsh-4.3.1+20210702.314a7eb4/data-manifest 2021-07-02 03:25:42.000000000 +0200 @@ -194,6 +194,7 @@ test/unittests/test_ocfs2.py test/unittests/test_parallax.py test/unittests/test_parse.py +test/unittests/test_ratrace.py test/unittests/test_report.py test/unittests/test_sbd.py test/unittests/test_scripts.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.3.1+20210624.c64d3a07/test/unittests/test_bugs.py new/crmsh-4.3.1+20210702.314a7eb4/test/unittests/test_bugs.py --- old/crmsh-4.3.1+20210624.c64d3a07/test/unittests/test_bugs.py 2021-06-24 04:19:10.000000000 +0200 +++ new/crmsh-4.3.1+20210702.314a7eb4/test/unittests/test_bugs.py 2021-07-02 03:25:42.000000000 +0200 @@ -343,25 +343,6 @@ assert set(x.obj_id for x in elems) == set(['r1', 'r2']) -def test_ratrace(): - xml = '''<primitive class="ocf" id="%s" provider="pacemaker" type="Dummy"/>''' - factory.create_from_node(etree.fromstring(xml % ('r1'))) - factory.create_from_node(etree.fromstring(xml % ('r2'))) - factory.create_from_node(etree.fromstring(xml % ('r3'))) - - context = object() - - from crmsh.ui_resource import RscMgmt - obj = factory.find_object('r1') - RscMgmt()._trace_resource(context, 'r1', obj) - - obj = factory.find_object('r1') - ops = obj.node.xpath('./operations/op') - for op in ops: - assert op.xpath('./instance_attributes/nvpair[@name="trace_ra"]/@value') == ["1"] - assert set(obj.node.xpath('./operations/op/@name')) == set(['start', 'stop']) - - def test_op_role(): xml = '''<primitive class="ocf" id="rsc2" provider="pacemaker" type="Dummy"> <operations> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.3.1+20210624.c64d3a07/test/unittests/test_ratrace.py new/crmsh-4.3.1+20210702.314a7eb4/test/unittests/test_ratrace.py --- old/crmsh-4.3.1+20210624.c64d3a07/test/unittests/test_ratrace.py 1970-01-01 01:00:00.000000000 +0100 +++ new/crmsh-4.3.1+20210702.314a7eb4/test/unittests/test_ratrace.py 2021-07-02 03:25:42.000000000 +0200 @@ -0,0 +1,123 @@ +import unittest +from lxml import etree + +from crmsh import cibconfig +from crmsh.ui_context import Context +from crmsh.ui_resource import RscMgmt +from crmsh.ui_root import Root + + +class TestRATrace(unittest.TestCase): + """Unit tests for enabling/disabling RA tracing.""" + + context = Context(Root()) + factory = cibconfig.cib_factory + + def setUp(self): + self.factory._push_state() + + def tearDown(self): + self.factory._pop_state() + + def test_ratrace_resource(self): + """Check setting RA tracing for a resource.""" + xml = '''<primitive class="ocf" id="r1" provider="pacemaker" type="Dummy"/>''' + obj = self.factory.create_from_node(etree.fromstring(xml)) + + # Trace the resource. + RscMgmt()._trace_resource(self.context, obj.obj_id, obj) + self.assertEqual(obj.node.xpath('operations/op/@id'), ['r1-start-0', 'r1-stop-0']) + self.assertEqual(obj.node.xpath('operations/op[@id="r1-start-0"]/instance_attributes/nvpair[@name="trace_ra"]/@value'), ['1']) + self.assertEqual(obj.node.xpath('operations/op[@id="r1-stop-0"]/instance_attributes/nvpair[@name="trace_ra"]/@value'), ['1']) + + # Untrace the resource. + RscMgmt()._untrace_resource(self.context, obj.obj_id, obj) + self.assertEqual(obj.node.xpath('operations/op/@id'), []) + self.assertEqual(obj.node.xpath('.//*[@name="trace_ra"]'), []) + + def test_ratrace_op(self): + """Check setting RA tracing for a specific operation.""" + xml = '''<primitive class="ocf" id="r1" provider="pacemaker" type="Dummy"> + <operations> + <op id="r1-monitor-10" interval="10" name="monitor"/> + </operations> + </primitive>''' + obj = self.factory.create_from_node(etree.fromstring(xml)) + + # Trace the operation. + RscMgmt()._trace_op(self.context, obj.obj_id, obj, 'monitor') + self.assertEqual(obj.node.xpath('operations/op/@id'), ['r1-monitor-10']) + self.assertEqual(obj.node.xpath('operations/op[@id="r1-monitor-10"]/instance_attributes/nvpair[@name="trace_ra"]/@value'), ['1']) + + # Untrace the operation. + RscMgmt()._untrace_op(self.context, obj.obj_id, obj, 'monitor') + self.assertEqual(obj.node.xpath('operations/op/@id'), ['r1-monitor-10']) + self.assertEqual(obj.node.xpath('.//*[@name="trace_ra"]'), []) + + # Try untracing a non-existent operation. + with self.assertRaises(ValueError) as err: + RscMgmt()._untrace_op(self.context, obj.obj_id, obj, 'invalid-op') + self.assertEqual(str(err.exception), "Operation invalid-op not found in r1") + + def test_ratrace_new(self): + """Check setting RA tracing for an operation that is not in CIB.""" + xml = '''<primitive class="ocf" id="r1" provider="pacemaker" type="Dummy"> + </primitive>''' + obj = self.factory.create_from_node(etree.fromstring(xml)) + + # Trace a regular operation that is not yet defined in CIB. The request + # should succeed and introduce an op node for the operation. + RscMgmt()._trace_op(self.context, obj.obj_id, obj, 'start') + self.assertEqual(obj.node.xpath('operations/op/@id'), ['r1-start-0']) + self.assertEqual(obj.node.xpath('operations/op[@id="r1-start-0"]/instance_attributes/nvpair[@name="trace_ra"]/@value'), ['1']) + + # Try tracing the monitor operation in the same way. The request should + # get rejected because no explicit interval is specified. + with self.assertRaises(ValueError) as err: + RscMgmt()._trace_op(self.context, obj.obj_id, obj, 'monitor') + self.assertEqual(str(err.exception), "No monitor operation configured for r1") + + def test_ratrace_op_stateful(self): + """Check setting RA tracing for an operation on a stateful resource.""" + xml = '''<primitive class="ocf" id="r1" provider="pacemaker" type="Dummy"> + <operations> + <op id="r1-monitor-10" interval="10" name="monitor" role="Master"/> + <op id="r1-monitor-11" interval="11" name="monitor" role="Slave"/> + </operations> + </primitive>''' + obj = self.factory.create_from_node(etree.fromstring(xml)) + + # Trace the operation. + RscMgmt()._trace_op(self.context, obj.obj_id, obj, 'monitor') + self.assertEqual(obj.node.xpath('operations/op/@id'), ['r1-monitor-10', 'r1-monitor-11']) + self.assertEqual(obj.node.xpath('operations/op[@id="r1-monitor-10"]/instance_attributes/nvpair[@name="trace_ra"]/@value'), ['1']) + self.assertEqual(obj.node.xpath('operations/op[@id="r1-monitor-11"]/instance_attributes/nvpair[@name="trace_ra"]/@value'), ['1']) + + # Untrace the operation. + RscMgmt()._untrace_op(self.context, obj.obj_id, obj, 'monitor') + self.assertEqual(obj.node.xpath('operations/op/@id'), ['r1-monitor-10', 'r1-monitor-11']) + self.assertEqual(obj.node.xpath('.//*[@name="trace_ra"]'), []) + + def test_ratrace_op_interval(self): + """Check setting RA tracing for an operation+interval.""" + xml = '''<primitive class="ocf" id="r1" provider="pacemaker" type="Dummy"> + <operations> + <op id="r1-monitor-10" interval="10" name="monitor"/> + </operations> + </primitive>''' + obj = self.factory.create_from_node(etree.fromstring(xml)) + + # Trace the operation. + RscMgmt()._trace_op_interval(self.context, obj.obj_id, obj, 'monitor', '10') + self.assertEqual(obj.node.xpath('operations/op/@id'), ['r1-monitor-10']) + self.assertEqual(obj.node.xpath('operations/op[@id="r1-monitor-10"]/instance_attributes/nvpair[@name="trace_ra"]/@value'), ['1']) + + # Untrace the operation. + RscMgmt()._untrace_op_interval(self.context, obj.obj_id, obj, 'monitor', '10') + self.assertEqual(obj.node.xpath('operations/op/@id'), ['r1-monitor-10']) + self.assertEqual(obj.node.xpath('.//*[@name="trace_ra"]'), []) + + # Try untracing a non-existent operation. + with self.assertRaises(ValueError) as err: + RscMgmt()._untrace_op_interval(self.context, obj.obj_id, obj, 'invalid-op', '10') + self.assertEqual(str(err.exception), "Operation invalid-op with interval 10 not found in r1") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.3.1+20210624.c64d3a07/test/unittests/test_sbd.py new/crmsh-4.3.1+20210702.314a7eb4/test/unittests/test_sbd.py --- old/crmsh-4.3.1+20210624.c64d3a07/test/unittests/test_sbd.py 2021-06-24 04:19:10.000000000 +0200 +++ new/crmsh-4.3.1+20210702.314a7eb4/test/unittests/test_sbd.py 2021-07-02 03:25:42.000000000 +0200 @@ -207,19 +207,21 @@ ]) mock_error.assert_called_once_with("Failed to initialize SBD device /dev/sdc1: error") + @mock.patch('crmsh.utils.detect_virt') @mock.patch('crmsh.bootstrap.csync2_update') @mock.patch('crmsh.utils.sysconfig_set') @mock.patch('crmsh.sbd.SBDManager._determine_sbd_watchdog_timeout') @mock.patch('shutil.copyfile') - def test_update_configuration(self, mock_copy, mock_determine, mock_sysconfig, mock_update): + def test_update_configuration(self, mock_copy, mock_determine, mock_sysconfig, mock_update, mock_detect): self.sbd_inst._sbd_devices = ["/dev/sdb1", "/dev/sdc1"] self.sbd_inst._watchdog_inst = mock.Mock(watchdog_device_name="/dev/watchdog") + mock_detect.return_value = True self.sbd_inst._sbd_watchdog_timeout = 30 self.sbd_inst._update_configuration() mock_copy.assert_called_once_with("/usr/share/fillup-templates/sysconfig.sbd", "/etc/sysconfig/sbd") - mock_sysconfig.assert_called_once_with("/etc/sysconfig/sbd", SBD_PACEMAKER='yes', SBD_STARTMODE='always', SBD_DELAY_START='no', SBD_WATCHDOG_DEV='/dev/watchdog', SBD_DEVICE='/dev/sdb1;/dev/sdc1', SBD_WATCHDOG_TIMEOUT="30") + mock_sysconfig.assert_called_once_with("/etc/sysconfig/sbd", SBD_PACEMAKER='yes', SBD_STARTMODE='always', SBD_DELAY_START='yes', SBD_WATCHDOG_DEV='/dev/watchdog', SBD_DEVICE='/dev/sdb1;/dev/sdc1', SBD_WATCHDOG_TIMEOUT="30") mock_update.assert_called_once_with("/etc/sysconfig/sbd") @mock.patch('crmsh.bootstrap.utils.parse_sysconfig') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-4.3.1+20210624.c64d3a07/test/unittests/test_utils.py new/crmsh-4.3.1+20210702.314a7eb4/test/unittests/test_utils.py --- old/crmsh-4.3.1+20210624.c64d3a07/test/unittests/test_utils.py 2021-06-24 04:19:10.000000000 +0200 +++ new/crmsh-4.3.1+20210702.314a7eb4/test/unittests/test_utils.py 2021-07-02 03:25:42.000000000 +0200 @@ -1537,3 +1537,10 @@ utils.check_all_nodes_reachable() mock_run.assert_called_once_with("crm_node -l") mock_ping.assert_called_once_with("15sp2-1") + + [email protected]('crmsh.utils.get_stdout_stderr') +def test_detect_virt(mock_run): + mock_run.return_value = (0, None, None) + assert utils.detect_virt() is True + mock_run.assert_called_once_with("systemd-detect-virt")
