From 6841064bf1d06e16c9c5bf9a6ab42b3185d55afb Mon Sep 17 00:00:00 2001 From: Miroslav Lisik Date: Mon, 20 Mar 2023 10:35:34 +0100 Subject: [PATCH 2/2] fix `pcs stonith update-scsi-devices` command --- pcs/lib/cib/resource/stonith.py | 168 +++++- .../test_stonith_update_scsi_devices.py | 571 ++++++++++++++---- 2 files changed, 601 insertions(+), 138 deletions(-) diff --git a/pcs/lib/cib/resource/stonith.py b/pcs/lib/cib/resource/stonith.py index 1f5bddff..07cffba6 100644 --- a/pcs/lib/cib/resource/stonith.py +++ b/pcs/lib/cib/resource/stonith.py @@ -169,12 +169,64 @@ def get_node_key_map_for_mpath( return node_key_map -DIGEST_ATTRS = ["op-digest", "op-secure-digest", "op-restart-digest"] -DIGEST_ATTR_TO_TYPE_MAP = { +DIGEST_ATTR_TO_DIGEST_TYPE_MAP = { "op-digest": "all", "op-secure-digest": "nonprivate", "op-restart-digest": "nonreloadable", } +TRANSIENT_DIGEST_ATTR_TO_DIGEST_TYPE_MAP = { + "#digests-all": "all", + "#digests-secure": "nonprivate", +} +DIGEST_ATTRS = frozenset(DIGEST_ATTR_TO_DIGEST_TYPE_MAP.keys()) +TRANSIENT_DIGEST_ATTRS = frozenset( + TRANSIENT_DIGEST_ATTR_TO_DIGEST_TYPE_MAP.keys() +) + + +def _get_digest( + attr: str, + attr_to_type_map: Dict[str, str], + calculated_digests: Dict[str, Optional[str]], +) -> str: + """ + Return digest of right type for the specified attribute. If missing, raise + an error. + + attr -- name of digest attribute + atttr_to_type_map -- map for attribute name to digest type conversion + calculated_digests -- digests calculated by pacemaker + """ + if attr not in attr_to_type_map: + raise AssertionError( + f"Key '{attr}' is missing in the attribute name to digest type map" + ) + digest = calculated_digests.get(attr_to_type_map[attr]) + if digest is None: + # this should not happen and when it does it is pacemaker fault + raise LibraryError( + ReportItem.error( + reports.messages.StonithRestartlessUpdateUnableToPerform( + f"necessary digest for '{attr}' attribute is missing" + ) + ) + ) + return digest + + +def _get_transient_instance_attributes(cib: _Element) -> List[_Element]: + """ + Return list of instance_attributes elements which could contain digest + attributes. + + cib -- CIB root element + """ + return cast( + List[_Element], + cib.xpath( + "./status/node_state/transient_attributes/instance_attributes" + ), + ) def _get_lrm_rsc_op_elements( @@ -278,21 +330,89 @@ def _update_digest_attrs_in_lrm_rsc_op( ) ) for attr in common_digests_attrs: - new_digest = calculated_digests[DIGEST_ATTR_TO_TYPE_MAP[attr]] - if new_digest is None: - # this should not happen and when it does it is pacemaker fault + # update digest in cib + lrm_rsc_op.attrib[attr] = _get_digest( + attr, DIGEST_ATTR_TO_DIGEST_TYPE_MAP, calculated_digests + ) + + +def _get_transient_digest_value( + old_value: str, stonith_id: str, stonith_type: str, digest: str +) -> str: + """ + Return transient digest value with replaced digest. + + Value has comma separated format: + ::,... + + and we need to replace only digest for our currently updated stonith device. + + old_value -- value to be replaced + stonith_id -- id of stonith resource + stonith_type -- stonith resource type + digest -- digest for new value + """ + new_comma_values_list = [] + for comma_value in old_value.split(","): + if comma_value: + try: + _id, _type, _ = comma_value.split(":") + except ValueError as e: + raise LibraryError( + ReportItem.error( + reports.messages.StonithRestartlessUpdateUnableToPerform( + f"invalid digest attribute value: '{old_value}'" + ) + ) + ) from e + if _id == stonith_id and _type == stonith_type: + comma_value = ":".join([stonith_id, stonith_type, digest]) + new_comma_values_list.append(comma_value) + return ",".join(new_comma_values_list) + + +def _update_digest_attrs_in_transient_instance_attributes( + nvset_el: _Element, + stonith_id: str, + stonith_type: str, + calculated_digests: Dict[str, Optional[str]], +) -> None: + """ + Update digests attributes in transient instance attributes element. + + nvset_el -- instance_attributes element containing nvpairs with digests + attributes + stonith_id -- id of stonith resource being updated + stonith_type -- type of stonith resource being updated + calculated_digests -- digests calculated by pacemaker + """ + for attr in TRANSIENT_DIGEST_ATTRS: + nvpair_list = cast( + List[_Element], + nvset_el.xpath("./nvpair[@name=$name]", name=attr), + ) + if not nvpair_list: + continue + if len(nvpair_list) > 1: raise LibraryError( ReportItem.error( reports.messages.StonithRestartlessUpdateUnableToPerform( - ( - f"necessary digest for '{attr}' attribute is " - "missing" - ) + f"multiple digests attributes: '{attr}'" ) ) ) - # update digest in cib - lrm_rsc_op.attrib[attr] = new_digest + old_value = nvpair_list[0].attrib["value"] + if old_value: + nvpair_list[0].attrib["value"] = _get_transient_digest_value( + str(old_value), + stonith_id, + stonith_type, + _get_digest( + attr, + TRANSIENT_DIGEST_ATTR_TO_DIGEST_TYPE_MAP, + calculated_digests, + ), + ) def update_scsi_devices_without_restart( @@ -311,6 +431,8 @@ def update_scsi_devices_without_restart( id_provider -- elements' ids generator device_list -- list of updated scsi devices """ + # pylint: disable=too-many-locals + cib = get_root(resource_el) resource_id = resource_el.get("id", "") roles_with_nodes = get_resource_state(cluster_state, resource_id) if "Started" not in roles_with_nodes: @@ -341,17 +463,14 @@ def update_scsi_devices_without_restart( ) lrm_rsc_op_start_list = _get_lrm_rsc_op_elements( - get_root(resource_el), resource_id, node_name, "start" + cib, resource_id, node_name, "start" + ) + new_instance_attrs_digests = get_resource_digests( + runner, resource_id, node_name, new_instance_attrs ) if len(lrm_rsc_op_start_list) == 1: _update_digest_attrs_in_lrm_rsc_op( - lrm_rsc_op_start_list[0], - get_resource_digests( - runner, - resource_id, - node_name, - new_instance_attrs, - ), + lrm_rsc_op_start_list[0], new_instance_attrs_digests ) else: raise LibraryError( @@ -364,7 +483,7 @@ def update_scsi_devices_without_restart( monitor_attrs_list = _get_monitor_attrs(resource_el) lrm_rsc_op_monitor_list = _get_lrm_rsc_op_elements( - get_root(resource_el), resource_id, node_name, "monitor" + cib, resource_id, node_name, "monitor" ) if len(lrm_rsc_op_monitor_list) != len(monitor_attrs_list): raise LibraryError( @@ -380,7 +499,7 @@ def update_scsi_devices_without_restart( for monitor_attrs in monitor_attrs_list: lrm_rsc_op_list = _get_lrm_rsc_op_elements( - get_root(resource_el), + cib, resource_id, node_name, "monitor", @@ -409,3 +528,10 @@ def update_scsi_devices_without_restart( ) ) ) + for nvset_el in _get_transient_instance_attributes(cib): + _update_digest_attrs_in_transient_instance_attributes( + nvset_el, + resource_id, + resource_el.get("type", ""), + new_instance_attrs_digests, + ) diff --git a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py index 69ea097c..72c7dbcf 100644 --- a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py +++ b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py @@ -38,6 +38,7 @@ DEFAULT_DIGEST = _DIGEST + "0" ALL_DIGEST = _DIGEST + "1" NONPRIVATE_DIGEST = _DIGEST + "2" NONRELOADABLE_DIGEST = _DIGEST + "3" +DIGEST_ATTR_VALUE_GOOD_FORMAT = f"stonith_id:stonith_type:{DEFAULT_DIGEST}," DEV_1 = "/dev/sda" DEV_2 = "/dev/sdb" DEV_3 = "/dev/sdc" @@ -151,33 +152,58 @@ def _fixture_lrm_rsc_start_ops(resource_id, lrm_start_ops): return _fixture_lrm_rsc_ops("start", resource_id, lrm_start_ops) -def _fixture_status_lrm_ops_base( - resource_id, - resource_type, - lrm_ops, -): +def _fixture_status_lrm_ops(resource_id, resource_type, lrm_ops): return f""" - - - - - - {lrm_ops} - - - - - + + + + {lrm_ops} + + + + """ + + +def _fixture_digest_nvpair(node_id, digest_name, digest_value): + return ( + f'' + ) + + +def _fixture_transient_attributes(node_id, digests_nvpairs): + return f""" + + + + + {digests_nvpairs} + + + """ + + +def _fixture_node_state(node_id, lrm_ops=None, transient_attrs=None): + if transient_attrs is None: + transient_attrs = "" + if lrm_ops is None: + lrm_ops = "" + return f""" + + {lrm_ops} + {transient_attrs} + """ -def _fixture_status_lrm_ops( +def _fixture_status( resource_id, resource_type, lrm_start_ops=DEFAULT_LRM_START_OPS, lrm_monitor_ops=DEFAULT_LRM_MONITOR_OPS, + digests_attrs_list=None, ): - return _fixture_status_lrm_ops_base( + lrm_ops = _fixture_status_lrm_ops( resource_id, resource_type, "\n".join( @@ -185,18 +211,52 @@ def _fixture_status_lrm_ops( + _fixture_lrm_rsc_monitor_ops(resource_id, lrm_monitor_ops) ), ) + node_states_list = [] + if not digests_attrs_list: + node_states_list.append( + _fixture_node_state("1", lrm_ops, transient_attrs=None) + ) + else: + for node_id, digests_attrs in enumerate(digests_attrs_list, start=1): + transient_attrs = _fixture_transient_attributes( + node_id, + "\n".join( + _fixture_digest_nvpair(node_id, name, value) + for name, value in digests_attrs + ), + ) + node_state = _fixture_node_state( + node_id, + lrm_ops=lrm_ops if node_id == 1 else None, + transient_attrs=transient_attrs, + ) + node_states_list.append(node_state) + node_states = "\n".join(node_states_list) + return f""" + + {node_states} + + """ + +def fixture_digests_xml(resource_id, node_name, devices="", nonprivate=True): + nonprivate_xml = ( + f""" + + + + """ + if nonprivate + else "" + ) -def fixture_digests_xml(resource_id, node_name, devices=""): return f""" - - - + {nonprivate_xml} @@ -334,6 +394,8 @@ class UpdateScsiDevicesMixin: nodes_running_on=1, start_digests=True, monitor_digests=True, + digests_attrs_list=None, + crm_digests_xml=None, ): # pylint: disable=too-many-arguments # pylint: disable=too-many-locals @@ -346,11 +408,12 @@ class UpdateScsiDevicesMixin: resource_ops=resource_ops, host_map=host_map, ), - status=_fixture_status_lrm_ops( + status=_fixture_status( self.stonith_id, self.stonith_type, lrm_start_ops=lrm_start_ops, lrm_monitor_ops=lrm_monitor_ops, + digests_attrs_list=digests_attrs_list, ), ) self.config.runner.pcmk.is_resource_digests_supported() @@ -363,14 +426,17 @@ class UpdateScsiDevicesMixin: nodes=FIXTURE_CRM_MON_NODES, ) devices_opt = "devices={}".format(devices_value) + + if crm_digests_xml is None: + crm_digests_xml = fixture_digests_xml( + self.stonith_id, SCSI_NODE, devices=devices_value + ) if start_digests: self.config.runner.pcmk.resource_digests( self.stonith_id, SCSI_NODE, name="start.op.digests", - stdout=fixture_digests_xml( - self.stonith_id, SCSI_NODE, devices=devices_value - ), + stdout=crm_digests_xml, args=[devices_opt], ) if monitor_digests: @@ -394,11 +460,7 @@ class UpdateScsiDevicesMixin: self.stonith_id, SCSI_NODE, name=f"{name}-{num}.op.digests", - stdout=fixture_digests_xml( - self.stonith_id, - SCSI_NODE, - devices=devices_value, - ), + stdout=crm_digests_xml, args=args, ) @@ -406,14 +468,16 @@ class UpdateScsiDevicesMixin: self, devices_before=DEVICES_1, devices_updated=DEVICES_2, - devices_add=(), - devices_remove=(), + devices_add=None, + devices_remove=None, unfence=None, resource_ops=DEFAULT_OPS, lrm_monitor_ops=DEFAULT_LRM_MONITOR_OPS, lrm_start_ops=DEFAULT_LRM_START_OPS, lrm_monitor_ops_updated=DEFAULT_LRM_MONITOR_OPS_UPDATED, lrm_start_ops_updated=DEFAULT_LRM_START_OPS_UPDATED, + digests_attrs_list=None, + digests_attrs_list_updated=None, ): # pylint: disable=too-many-arguments self.config_cib( @@ -422,6 +486,7 @@ class UpdateScsiDevicesMixin: resource_ops=resource_ops, lrm_monitor_ops=lrm_monitor_ops, lrm_start_ops=lrm_start_ops, + digests_attrs_list=digests_attrs_list, ) if unfence: self.config.corosync_conf.load_content( @@ -445,20 +510,34 @@ class UpdateScsiDevicesMixin: devices=devices_updated, resource_ops=resource_ops, ), - status=_fixture_status_lrm_ops( + status=_fixture_status( self.stonith_id, self.stonith_type, lrm_start_ops=lrm_start_ops_updated, lrm_monitor_ops=lrm_monitor_ops_updated, + digests_attrs_list=digests_attrs_list_updated, ), ) - self.command( - devices_updated=devices_updated, - devices_add=devices_add, - devices_remove=devices_remove, - )() + kwargs = dict(devices_updated=devices_updated) + if devices_add is not None: + kwargs["devices_add"] = devices_add + if devices_remove is not None: + kwargs["devices_remove"] = devices_remove + self.command(**kwargs)() self.env_assist.assert_reports([]) + def digest_attr_value_single(self, digest, last_comma=True): + comma = "," if last_comma else "" + return f"{self.stonith_id}:{self.stonith_type}:{digest}{comma}" + + def digest_attr_value_multiple(self, digest, last_comma=True): + if self.stonith_type == STONITH_TYPE_SCSI: + value = f"{STONITH_ID_MPATH}:{STONITH_TYPE_MPATH}:{DEFAULT_DIGEST}," + else: + value = f"{STONITH_ID_SCSI}:{STONITH_TYPE_SCSI}:{DEFAULT_DIGEST}," + + return f"{value}{self.digest_attr_value_single(digest, last_comma=last_comma)}" + class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): def test_pcmk_doesnt_support_digests(self): @@ -567,9 +646,7 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): ) def test_no_lrm_start_op(self): - self.config_cib( - lrm_start_ops=(), start_digests=False, monitor_digests=False - ) + self.config_cib(lrm_start_ops=(), monitor_digests=False) self.env_assist.assert_raise_library_error( self.command(), [ @@ -622,6 +699,59 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): expected_in_processor=False, ) + def test_crm_resource_digests_missing_for_transient_digests_attrs(self): + self.config_cib( + digests_attrs_list=[ + [ + ( + "digests-secure", + self.digest_attr_value_single(ALL_DIGEST), + ), + ], + ], + crm_digests_xml=fixture_digests_xml( + self.stonith_id, SCSI_NODE, devices="", nonprivate=False + ), + ) + self.env_assist.assert_raise_library_error( + self.command(), + [ + fixture.error( + reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM, + reason=( + "necessary digest for '#digests-secure' attribute is " + "missing" + ), + reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER, + ) + ], + expected_in_processor=False, + ) + + def test_multiple_digests_attributes(self): + self.config_cib( + digests_attrs_list=[ + 2 + * [ + ( + "digests-all", + self.digest_attr_value_single(DEFAULT_DIGEST), + ), + ], + ], + ) + self.env_assist.assert_raise_library_error( + self.command(), + [ + fixture.error( + reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM, + reason=("multiple digests attributes: '#digests-all'"), + reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER, + ) + ], + expected_in_processor=False, + ) + def test_monitor_ops_and_lrm_monitor_ops_do_not_match(self): self.config_cib( resource_ops=( @@ -812,7 +942,7 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): stonith_type=self.stonith_type, devices=DEVICES_2, ), - status=_fixture_status_lrm_ops( + status=_fixture_status( self.stonith_id, self.stonith_type, lrm_start_ops=DEFAULT_LRM_START_OPS_UPDATED, @@ -959,6 +1089,28 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): ] ) + def test_transient_digests_attrs_bad_value_format(self): + bad_format = f"{DIGEST_ATTR_VALUE_GOOD_FORMAT}id:type," + self.config_cib( + digests_attrs_list=[ + [ + ("digests-all", DIGEST_ATTR_VALUE_GOOD_FORMAT), + ("digests-secure", bad_format), + ] + ] + ) + self.env_assist.assert_raise_library_error( + self.command(), + [ + fixture.error( + reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM, + reason=f"invalid digest attribute value: '{bad_format}'", + reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER, + ) + ], + expected_in_processor=False, + ) + class UpdateScsiDevicesSetBase(UpdateScsiDevicesMixin, CommandSetMixin): def test_update_1_to_1_devices(self): @@ -1002,80 +1154,6 @@ class UpdateScsiDevicesSetBase(UpdateScsiDevicesMixin, CommandSetMixin): unfence=[DEV_3, DEV_4], ) - def test_default_monitor(self): - self.assert_command_success(unfence=[DEV_2]) - - def test_no_monitor_ops(self): - self.assert_command_success( - unfence=[DEV_2], - resource_ops=(), - lrm_monitor_ops=(), - lrm_monitor_ops_updated=(), - ) - - def test_1_monitor_with_timeout(self): - self.assert_command_success( - unfence=[DEV_2], - resource_ops=(("monitor", "30s", "10s", None),), - lrm_monitor_ops=(("30000", DEFAULT_DIGEST, None, None),), - lrm_monitor_ops_updated=(("30000", ALL_DIGEST, None, None),), - ) - - def test_2_monitor_ops_with_timeouts(self): - self.assert_command_success( - unfence=[DEV_2], - resource_ops=( - ("monitor", "30s", "10s", None), - ("monitor", "40s", "20s", None), - ), - lrm_monitor_ops=( - ("30000", DEFAULT_DIGEST, None, None), - ("40000", DEFAULT_DIGEST, None, None), - ), - lrm_monitor_ops_updated=( - ("30000", ALL_DIGEST, None, None), - ("40000", ALL_DIGEST, None, None), - ), - ) - - def test_2_monitor_ops_with_one_timeout(self): - self.assert_command_success( - unfence=[DEV_2], - resource_ops=( - ("monitor", "30s", "10s", None), - ("monitor", "60s", None, None), - ), - lrm_monitor_ops=( - ("30000", DEFAULT_DIGEST, None, None), - ("60000", DEFAULT_DIGEST, None, None), - ), - lrm_monitor_ops_updated=( - ("30000", ALL_DIGEST, None, None), - ("60000", ALL_DIGEST, None, None), - ), - ) - - def test_various_start_ops_one_lrm_start_op(self): - self.assert_command_success( - unfence=[DEV_2], - resource_ops=( - ("monitor", "60s", None, None), - ("start", "0s", "40s", None), - ("start", "0s", "30s", "1"), - ("start", "10s", "5s", None), - ("start", "20s", None, None), - ), - ) - - def test_1_nonrecurring_start_op_with_timeout(self): - self.assert_command_success( - unfence=[DEV_2], - resource_ops=( - ("monitor", "60s", None, None), - ("start", "0s", "40s", None), - ), - ) - class UpdateScsiDevicesAddRemoveBase( UpdateScsiDevicesMixin, CommandAddRemoveMixin @@ -1245,6 +1323,221 @@ class MpathFailuresMixin: self.assert_failure("node1:1;node2=", ["node2", "node3"]) +class UpdateScsiDevicesDigestsBase(UpdateScsiDevicesMixin): + def test_default_monitor(self): + self.assert_command_success(unfence=[DEV_2]) + + def test_no_monitor_ops(self): + self.assert_command_success( + unfence=[DEV_2], + resource_ops=(), + lrm_monitor_ops=(), + lrm_monitor_ops_updated=(), + ) + + def test_1_monitor_with_timeout(self): + self.assert_command_success( + unfence=[DEV_2], + resource_ops=(("monitor", "30s", "10s", None),), + lrm_monitor_ops=(("30000", DEFAULT_DIGEST, None, None),), + lrm_monitor_ops_updated=(("30000", ALL_DIGEST, None, None),), + ) + + def test_2_monitor_ops_with_timeouts(self): + self.assert_command_success( + unfence=[DEV_2], + resource_ops=( + ("monitor", "30s", "10s", None), + ("monitor", "40s", "20s", None), + ), + lrm_monitor_ops=( + ("30000", DEFAULT_DIGEST, None, None), + ("40000", DEFAULT_DIGEST, None, None), + ), + lrm_monitor_ops_updated=( + ("30000", ALL_DIGEST, None, None), + ("40000", ALL_DIGEST, None, None), + ), + ) + + def test_2_monitor_ops_with_one_timeout(self): + self.assert_command_success( + unfence=[DEV_2], + resource_ops=( + ("monitor", "30s", "10s", None), + ("monitor", "60s", None, None), + ), + lrm_monitor_ops=( + ("30000", DEFAULT_DIGEST, None, None), + ("60000", DEFAULT_DIGEST, None, None), + ), + lrm_monitor_ops_updated=( + ("30000", ALL_DIGEST, None, None), + ("60000", ALL_DIGEST, None, None), + ), + ) + + def test_various_start_ops_one_lrm_start_op(self): + self.assert_command_success( + unfence=[DEV_2], + resource_ops=( + ("monitor", "60s", None, None), + ("start", "0s", "40s", None), + ("start", "0s", "30s", "1"), + ("start", "10s", "5s", None), + ("start", "20s", None, None), + ), + ) + + def test_1_nonrecurring_start_op_with_timeout(self): + self.assert_command_success( + unfence=[DEV_2], + resource_ops=( + ("monitor", "60s", None, None), + ("start", "0s", "40s", None), + ), + ) + + def _digests_attrs_before(self, last_comma=True): + return [ + ( + "digests-all", + self.digest_attr_value_single(DEFAULT_DIGEST, last_comma), + ), + ( + "digests-secure", + self.digest_attr_value_single(DEFAULT_DIGEST, last_comma), + ), + ] + + def _digests_attrs_after(self, last_comma=True): + return [ + ( + "digests-all", + self.digest_attr_value_single(ALL_DIGEST, last_comma), + ), + ( + "digests-secure", + self.digest_attr_value_single(NONPRIVATE_DIGEST, last_comma), + ), + ] + + def _digests_attrs_before_multi(self, last_comma=True): + return [ + ( + "digests-all", + self.digest_attr_value_multiple(DEFAULT_DIGEST, last_comma), + ), + ( + "digests-secure", + self.digest_attr_value_multiple(DEFAULT_DIGEST, last_comma), + ), + ] + + def _digests_attrs_after_multi(self, last_comma=True): + return [ + ( + "digests-all", + self.digest_attr_value_multiple(ALL_DIGEST, last_comma), + ), + ( + "digests-secure", + self.digest_attr_value_multiple(NONPRIVATE_DIGEST, last_comma), + ), + ] + + def test_transient_digests_attrs_all_nodes(self): + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=len(self.existing_nodes) + * [self._digests_attrs_before()], + digests_attrs_list_updated=len(self.existing_nodes) + * [self._digests_attrs_after()], + ) + + def test_transient_digests_attrs_not_on_all_nodes(self): + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=[self._digests_attrs_before()], + digests_attrs_list_updated=[self._digests_attrs_after()], + ) + + def test_transient_digests_attrs_all_nodes_multi_value(self): + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=len(self.existing_nodes) + * [self._digests_attrs_before_multi()], + digests_attrs_list_updated=len(self.existing_nodes) + * [self._digests_attrs_after_multi()], + ) + + def test_transient_digests_attrs_not_on_all_nodes_multi_value(self): + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=[self._digests_attrs_before()], + digests_attrs_list_updated=[self._digests_attrs_after()], + ) + + def test_transient_digests_attrs_not_all_digest_types(self): + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=len(self.existing_nodes) + * [self._digests_attrs_before()[0:1]], + digests_attrs_list_updated=len(self.existing_nodes) + * [self._digests_attrs_after()[0:1]], + ) + + def test_transient_digests_attrs_without_digests_attrs(self): + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=len(self.existing_nodes) * [[]], + digests_attrs_list_updated=len(self.existing_nodes) * [[]], + ) + + def test_transient_digests_attrs_without_last_comma(self): + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=[self._digests_attrs_before(last_comma=False)], + digests_attrs_list_updated=[ + self._digests_attrs_after(last_comma=False) + ], + ) + + def test_transient_digests_attrs_without_last_comma_multi_value(self): + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=[ + self._digests_attrs_before_multi(last_comma=False) + ], + digests_attrs_list_updated=[ + self._digests_attrs_after_multi(last_comma=False) + ], + ) + + def test_transient_digests_attrs_no_digest_for_our_stonith_id(self): + digests_attrs_list = len(self.existing_nodes) * [ + [ + ("digests-all", DIGEST_ATTR_VALUE_GOOD_FORMAT), + ("digests-secure", DIGEST_ATTR_VALUE_GOOD_FORMAT), + ] + ] + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=digests_attrs_list, + digests_attrs_list_updated=digests_attrs_list, + ) + + def test_transient_digests_attrs_digests_with_empty_value(self): + digests_attrs_list = len(self.existing_nodes) * [ + [("digests-all", ""), ("digests-secure", "")] + ] + self.assert_command_success( + unfence=[DEV_2], + digests_attrs_list=digests_attrs_list, + digests_attrs_list_updated=digests_attrs_list, + ) + + @mock.patch.object( settings, "pacemaker_api_result_schema", @@ -1337,3 +1630,47 @@ class TestUpdateScsiDevicesAddRemoveFailuresScsi( UpdateScsiDevicesAddRemoveFailuresBaseMixin, ScsiMixin, TestCase ): pass + + +@mock.patch.object( + settings, + "pacemaker_api_result_schema", + rc("pcmk_api_rng/api-result.rng"), +) +class TestUpdateScsiDevicesDigestsSetScsi( + UpdateScsiDevicesDigestsBase, ScsiMixin, CommandSetMixin, TestCase +): + pass + + +@mock.patch.object( + settings, + "pacemaker_api_result_schema", + rc("pcmk_api_rng/api-result.rng"), +) +class TestUpdateScsiDevicesDigestsAddRemoveScsi( + UpdateScsiDevicesDigestsBase, ScsiMixin, CommandAddRemoveMixin, TestCase +): + pass + + +@mock.patch.object( + settings, + "pacemaker_api_result_schema", + rc("pcmk_api_rng/api-result.rng"), +) +class TestUpdateScsiDevicesDigestsSetMpath( + UpdateScsiDevicesDigestsBase, MpathMixin, CommandSetMixin, TestCase +): + pass + + +@mock.patch.object( + settings, + "pacemaker_api_result_schema", + rc("pcmk_api_rng/api-result.rng"), +) +class TestUpdateScsiDevicesDigestsAddRemoveMpath( + UpdateScsiDevicesDigestsBase, MpathMixin, CommandAddRemoveMixin, TestCase +): + pass -- 2.39.2