Blame SOURCES/bz2180706-01-fix-pcs-stonith-update-scsi-devices.patch

cd5b9a
From 0b9175e04ee0527bbf603ad8dd8240c50c623bd6 Mon Sep 17 00:00:00 2001
cd5b9a
From: Miroslav Lisik <mlisik@redhat.com>
cd5b9a
Date: Mon, 20 Mar 2023 10:35:34 +0100
cd5b9a
Subject: [PATCH 2/2] fix `pcs stonith update-scsi-devices` command
cd5b9a
cd5b9a
---
cd5b9a
 pcs/lib/cib/stonith.py                        | 168 +++++-
cd5b9a
 .../test_stonith_update_scsi_devices.py       | 571 ++++++++++++++----
cd5b9a
 2 files changed, 601 insertions(+), 138 deletions(-)
cd5b9a
cd5b9a
diff --git a/pcs/lib/cib/stonith.py b/pcs/lib/cib/stonith.py
cd5b9a
index 85b46fd7..f49bbc37 100644
cd5b9a
--- a/pcs/lib/cib/stonith.py
cd5b9a
+++ b/pcs/lib/cib/stonith.py
cd5b9a
@@ -158,12 +158,64 @@ def get_node_key_map_for_mpath(
cd5b9a
     return node_key_map
cd5b9a
 
cd5b9a
 
cd5b9a
-DIGEST_ATTRS = ["op-digest", "op-secure-digest", "op-restart-digest"]
cd5b9a
-DIGEST_ATTR_TO_TYPE_MAP = {
cd5b9a
+DIGEST_ATTR_TO_DIGEST_TYPE_MAP = {
cd5b9a
     "op-digest": "all",
cd5b9a
     "op-secure-digest": "nonprivate",
cd5b9a
     "op-restart-digest": "nonreloadable",
cd5b9a
 }
cd5b9a
+TRANSIENT_DIGEST_ATTR_TO_DIGEST_TYPE_MAP = {
cd5b9a
+    "#digests-all": "all",
cd5b9a
+    "#digests-secure": "nonprivate",
cd5b9a
+}
cd5b9a
+DIGEST_ATTRS = frozenset(DIGEST_ATTR_TO_DIGEST_TYPE_MAP.keys())
cd5b9a
+TRANSIENT_DIGEST_ATTRS = frozenset(
cd5b9a
+    TRANSIENT_DIGEST_ATTR_TO_DIGEST_TYPE_MAP.keys()
cd5b9a
+)
cd5b9a
+
cd5b9a
+
cd5b9a
+def _get_digest(
cd5b9a
+    attr: str,
cd5b9a
+    attr_to_type_map: Dict[str, str],
cd5b9a
+    calculated_digests: Dict[str, Optional[str]],
cd5b9a
+) -> str:
cd5b9a
+    """
cd5b9a
+    Return digest of right type for the specified attribute. If missing, raise
cd5b9a
+    an error.
cd5b9a
+
cd5b9a
+    attr -- name of digest attribute
cd5b9a
+    atttr_to_type_map -- map for attribute name to digest type conversion
cd5b9a
+    calculated_digests -- digests calculated by pacemaker
cd5b9a
+    """
cd5b9a
+    if attr not in attr_to_type_map:
cd5b9a
+        raise AssertionError(
cd5b9a
+            f"Key '{attr}' is missing in the attribute name to digest type map"
cd5b9a
+        )
cd5b9a
+    digest = calculated_digests.get(attr_to_type_map[attr])
cd5b9a
+    if digest is None:
cd5b9a
+        # this should not happen and when it does it is pacemaker fault
cd5b9a
+        raise LibraryError(
cd5b9a
+            ReportItem.error(
cd5b9a
+                reports.messages.StonithRestartlessUpdateUnableToPerform(
cd5b9a
+                    f"necessary digest for '{attr}' attribute is missing"
cd5b9a
+                )
cd5b9a
+            )
cd5b9a
+        )
cd5b9a
+    return digest
cd5b9a
+
cd5b9a
+
cd5b9a
+def _get_transient_instance_attributes(cib: _Element) -> List[_Element]:
cd5b9a
+    """
cd5b9a
+    Return list of instance_attributes elements which could contain digest
cd5b9a
+    attributes.
cd5b9a
+
cd5b9a
+    cib -- CIB root element
cd5b9a
+    """
cd5b9a
+    return cast(
cd5b9a
+        List[_Element],
cd5b9a
+        cib.xpath(
cd5b9a
+            "./status/node_state/transient_attributes/instance_attributes"
cd5b9a
+        ),
cd5b9a
+    )
cd5b9a
 
cd5b9a
 
cd5b9a
 def _get_lrm_rsc_op_elements(
cd5b9a
@@ -267,21 +319,89 @@ def _update_digest_attrs_in_lrm_rsc_op(
cd5b9a
             )
cd5b9a
         )
cd5b9a
     for attr in common_digests_attrs:
cd5b9a
-        new_digest = calculated_digests[DIGEST_ATTR_TO_TYPE_MAP[attr]]
cd5b9a
-        if new_digest is None:
cd5b9a
-            # this should not happen and when it does it is pacemaker fault
cd5b9a
+        # update digest in cib
cd5b9a
+        lrm_rsc_op.attrib[attr] = _get_digest(
cd5b9a
+            attr, DIGEST_ATTR_TO_DIGEST_TYPE_MAP, calculated_digests
cd5b9a
+        )
cd5b9a
+
cd5b9a
+
cd5b9a
+def _get_transient_digest_value(
cd5b9a
+    old_value: str, stonith_id: str, stonith_type: str, digest: str
cd5b9a
+) -> str:
cd5b9a
+    """
cd5b9a
+    Return transient digest value with replaced digest.
cd5b9a
+
cd5b9a
+    Value has comma separated format:
cd5b9a
+    <stonith_id>:<stonith_type>:<digest>,...
cd5b9a
+
cd5b9a
+    and we need to replace only digest for our currently updated stonith device.
cd5b9a
+
cd5b9a
+    old_value -- value to be replaced
cd5b9a
+    stonith_id -- id of stonith resource
cd5b9a
+    stonith_type -- stonith resource type
cd5b9a
+    digest -- digest for new value
cd5b9a
+    """
cd5b9a
+    new_comma_values_list = []
cd5b9a
+    for comma_value in old_value.split(","):
cd5b9a
+        if comma_value:
cd5b9a
+            try:
cd5b9a
+                _id, _type, _ = comma_value.split(":")
cd5b9a
+            except ValueError as e:
cd5b9a
+                raise LibraryError(
cd5b9a
+                    ReportItem.error(
cd5b9a
+                        reports.messages.StonithRestartlessUpdateUnableToPerform(
cd5b9a
+                            f"invalid digest attribute value: '{old_value}'"
cd5b9a
+                        )
cd5b9a
+                    )
cd5b9a
+                ) from e
cd5b9a
+            if _id == stonith_id and _type == stonith_type:
cd5b9a
+                comma_value = ":".join([stonith_id, stonith_type, digest])
cd5b9a
+        new_comma_values_list.append(comma_value)
cd5b9a
+    return ",".join(new_comma_values_list)
cd5b9a
+
cd5b9a
+
cd5b9a
+def _update_digest_attrs_in_transient_instance_attributes(
cd5b9a
+    nvset_el: _Element,
cd5b9a
+    stonith_id: str,
cd5b9a
+    stonith_type: str,
cd5b9a
+    calculated_digests: Dict[str, Optional[str]],
cd5b9a
+) -> None:
cd5b9a
+    """
cd5b9a
+    Update digests attributes in transient instance attributes element.
cd5b9a
+
cd5b9a
+    nvset_el -- instance_attributes element containing nvpairs with digests
cd5b9a
+        attributes
cd5b9a
+    stonith_id -- id of stonith resource being updated
cd5b9a
+    stonith_type -- type of stonith resource being updated
cd5b9a
+    calculated_digests -- digests calculated by pacemaker
cd5b9a
+    """
cd5b9a
+    for attr in TRANSIENT_DIGEST_ATTRS:
cd5b9a
+        nvpair_list = cast(
cd5b9a
+            List[_Element],
cd5b9a
+            nvset_el.xpath("./nvpair[@name=$name]", name=attr),
cd5b9a
+        )
cd5b9a
+        if not nvpair_list:
cd5b9a
+            continue
cd5b9a
+        if len(nvpair_list) > 1:
cd5b9a
             raise LibraryError(
cd5b9a
                 ReportItem.error(
cd5b9a
                     reports.messages.StonithRestartlessUpdateUnableToPerform(
cd5b9a
-                        (
cd5b9a
-                            f"necessary digest for '{attr}' attribute is "
cd5b9a
-                            "missing"
cd5b9a
-                        )
cd5b9a
+                        f"multiple digests attributes: '{attr}'"
cd5b9a
                     )
cd5b9a
                 )
cd5b9a
             )
cd5b9a
-        # update digest in cib
cd5b9a
-        lrm_rsc_op.attrib[attr] = new_digest
cd5b9a
+        old_value = nvpair_list[0].attrib["value"]
cd5b9a
+        if old_value:
cd5b9a
+            nvpair_list[0].attrib["value"] = _get_transient_digest_value(
cd5b9a
+                str(old_value),
cd5b9a
+                stonith_id,
cd5b9a
+                stonith_type,
cd5b9a
+                _get_digest(
cd5b9a
+                    attr,
cd5b9a
+                    TRANSIENT_DIGEST_ATTR_TO_DIGEST_TYPE_MAP,
cd5b9a
+                    calculated_digests,
cd5b9a
+                ),
cd5b9a
+            )
cd5b9a
 
cd5b9a
 
cd5b9a
 def update_scsi_devices_without_restart(
cd5b9a
@@ -300,6 +420,8 @@ def update_scsi_devices_without_restart(
cd5b9a
     id_provider -- elements' ids generator
cd5b9a
     device_list -- list of updated scsi devices
cd5b9a
     """
cd5b9a
+    # pylint: disable=too-many-locals
cd5b9a
+    cib = get_root(resource_el)
cd5b9a
     resource_id = resource_el.get("id", "")
cd5b9a
     roles_with_nodes = get_resource_state(cluster_state, resource_id)
cd5b9a
     if "Started" not in roles_with_nodes:
cd5b9a
@@ -330,17 +452,14 @@ def update_scsi_devices_without_restart(
cd5b9a
     )
cd5b9a
 
cd5b9a
     lrm_rsc_op_start_list = _get_lrm_rsc_op_elements(
cd5b9a
-        get_root(resource_el), resource_id, node_name, "start"
cd5b9a
+        cib, resource_id, node_name, "start"
cd5b9a
+    )
cd5b9a
+    new_instance_attrs_digests = get_resource_digests(
cd5b9a
+        runner, resource_id, node_name, new_instance_attrs
cd5b9a
     )
cd5b9a
     if len(lrm_rsc_op_start_list) == 1:
cd5b9a
         _update_digest_attrs_in_lrm_rsc_op(
cd5b9a
-            lrm_rsc_op_start_list[0],
cd5b9a
-            get_resource_digests(
cd5b9a
-                runner,
cd5b9a
-                resource_id,
cd5b9a
-                node_name,
cd5b9a
-                new_instance_attrs,
cd5b9a
-            ),
cd5b9a
+            lrm_rsc_op_start_list[0], new_instance_attrs_digests
cd5b9a
         )
cd5b9a
     else:
cd5b9a
         raise LibraryError(
cd5b9a
@@ -353,7 +472,7 @@ def update_scsi_devices_without_restart(
cd5b9a
 
cd5b9a
     monitor_attrs_list = _get_monitor_attrs(resource_el)
cd5b9a
     lrm_rsc_op_monitor_list = _get_lrm_rsc_op_elements(
cd5b9a
-        get_root(resource_el), resource_id, node_name, "monitor"
cd5b9a
+        cib, resource_id, node_name, "monitor"
cd5b9a
     )
cd5b9a
     if len(lrm_rsc_op_monitor_list) != len(monitor_attrs_list):
cd5b9a
         raise LibraryError(
cd5b9a
@@ -369,7 +488,7 @@ def update_scsi_devices_without_restart(
cd5b9a
 
cd5b9a
     for monitor_attrs in monitor_attrs_list:
cd5b9a
         lrm_rsc_op_list = _get_lrm_rsc_op_elements(
cd5b9a
-            get_root(resource_el),
cd5b9a
+            cib,
cd5b9a
             resource_id,
cd5b9a
             node_name,
cd5b9a
             "monitor",
cd5b9a
@@ -398,3 +517,10 @@ def update_scsi_devices_without_restart(
cd5b9a
                     )
cd5b9a
                 )
cd5b9a
             )
cd5b9a
+    for nvset_el in _get_transient_instance_attributes(cib):
cd5b9a
+        _update_digest_attrs_in_transient_instance_attributes(
cd5b9a
+            nvset_el,
cd5b9a
+            resource_id,
cd5b9a
+            resource_el.get("type", ""),
cd5b9a
+            new_instance_attrs_digests,
cd5b9a
+        )
cd5b9a
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
cd5b9a
index 6cb1f80c..db8953c8 100644
cd5b9a
--- a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py
cd5b9a
+++ b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py
cd5b9a
@@ -35,6 +35,7 @@ DEFAULT_DIGEST = _DIGEST + "0"
cd5b9a
 ALL_DIGEST = _DIGEST + "1"
cd5b9a
 NONPRIVATE_DIGEST = _DIGEST + "2"
cd5b9a
 NONRELOADABLE_DIGEST = _DIGEST + "3"
cd5b9a
+DIGEST_ATTR_VALUE_GOOD_FORMAT = f"stonith_id:stonith_type:{DEFAULT_DIGEST},"
cd5b9a
 DEV_1 = "/dev/sda"
cd5b9a
 DEV_2 = "/dev/sdb"
cd5b9a
 DEV_3 = "/dev/sdc"
cd5b9a
@@ -148,33 +149,58 @@ def _fixture_lrm_rsc_start_ops(resource_id, lrm_start_ops):
cd5b9a
     return _fixture_lrm_rsc_ops("start", resource_id, lrm_start_ops)
cd5b9a
 
cd5b9a
 
cd5b9a
-def _fixture_status_lrm_ops_base(
cd5b9a
-    resource_id,
cd5b9a
-    resource_type,
cd5b9a
-    lrm_ops,
cd5b9a
-):
cd5b9a
+def _fixture_status_lrm_ops(resource_id, resource_type, lrm_ops):
cd5b9a
     return f"""
cd5b9a
-        <status>
cd5b9a
-            <node_state id="1" uname="node1">
cd5b9a
-                <lrm id="1">
cd5b9a
-                    <lrm_resources>
cd5b9a
-                        <lrm_resource id="{resource_id}" type="{resource_type}" class="stonith">
cd5b9a
-                            {lrm_ops}
cd5b9a
-                        </lrm_resource>
cd5b9a
-                    </lrm_resources>
cd5b9a
-                </lrm>
cd5b9a
-            </node_state>
cd5b9a
-        </status>
cd5b9a
+        <lrm id="1">
cd5b9a
+            <lrm_resources>
cd5b9a
+                <lrm_resource id="{resource_id}" type="{resource_type}" class="stonith">
cd5b9a
+                    {lrm_ops}
cd5b9a
+                </lrm_resource>
cd5b9a
+            </lrm_resources>
cd5b9a
+        </lrm>
cd5b9a
+    """
cd5b9a
+
cd5b9a
+
cd5b9a
+def _fixture_digest_nvpair(node_id, digest_name, digest_value):
cd5b9a
+    return (
cd5b9a
+        f'
cd5b9a
+        f'value="{digest_value}"/>'
cd5b9a
+    )
cd5b9a
+
cd5b9a
+
cd5b9a
+def _fixture_transient_attributes(node_id, digests_nvpairs):
cd5b9a
+    return f"""
cd5b9a
+        <transient_attributes id="{node_id}">
cd5b9a
+            <instance_attributes id="status-{node_id}">
cd5b9a
+                <nvpair id="status-{node_id}-.feature-set" name="#feature-set" value="3.16.2"/>
cd5b9a
+                <nvpair id="status-{node_id}-.node-unfenced" name="#node-unfenced" value="1679319764"/>
cd5b9a
+                {digests_nvpairs}
cd5b9a
+            </instance_attributes>
cd5b9a
+        </transient_attributes>
cd5b9a
+    """
cd5b9a
+
cd5b9a
+
cd5b9a
+def _fixture_node_state(node_id, lrm_ops=None, transient_attrs=None):
cd5b9a
+    if transient_attrs is None:
cd5b9a
+        transient_attrs = ""
cd5b9a
+    if lrm_ops is None:
cd5b9a
+        lrm_ops = ""
cd5b9a
+    return f"""
cd5b9a
+        <node_state id="{node_id}" uname="node{node_id}">
cd5b9a
+            {lrm_ops}
cd5b9a
+            {transient_attrs}
cd5b9a
+        </node_state>
cd5b9a
     """
cd5b9a
 
cd5b9a
 
cd5b9a
-def _fixture_status_lrm_ops(
cd5b9a
+def _fixture_status(
cd5b9a
     resource_id,
cd5b9a
     resource_type,
cd5b9a
     lrm_start_ops=DEFAULT_LRM_START_OPS,
cd5b9a
     lrm_monitor_ops=DEFAULT_LRM_MONITOR_OPS,
cd5b9a
+    digests_attrs_list=None,
cd5b9a
 ):
cd5b9a
-    return _fixture_status_lrm_ops_base(
cd5b9a
+    lrm_ops = _fixture_status_lrm_ops(
cd5b9a
         resource_id,
cd5b9a
         resource_type,
cd5b9a
         "\n".join(
cd5b9a
@@ -182,18 +208,52 @@ def _fixture_status_lrm_ops(
cd5b9a
             + _fixture_lrm_rsc_monitor_ops(resource_id, lrm_monitor_ops)
cd5b9a
         ),
cd5b9a
     )
cd5b9a
+    node_states_list = []
cd5b9a
+    if not digests_attrs_list:
cd5b9a
+        node_states_list.append(
cd5b9a
+            _fixture_node_state("1", lrm_ops, transient_attrs=None)
cd5b9a
+        )
cd5b9a
+    else:
cd5b9a
+        for node_id, digests_attrs in enumerate(digests_attrs_list, start=1):
cd5b9a
+            transient_attrs = _fixture_transient_attributes(
cd5b9a
+                node_id,
cd5b9a
+                "\n".join(
cd5b9a
+                    _fixture_digest_nvpair(node_id, name, value)
cd5b9a
+                    for name, value in digests_attrs
cd5b9a
+                ),
cd5b9a
+            )
cd5b9a
+            node_state = _fixture_node_state(
cd5b9a
+                node_id,
cd5b9a
+                lrm_ops=lrm_ops if node_id == 1 else None,
cd5b9a
+                transient_attrs=transient_attrs,
cd5b9a
+            )
cd5b9a
+            node_states_list.append(node_state)
cd5b9a
+    node_states = "\n".join(node_states_list)
cd5b9a
+    return f"""
cd5b9a
+        <status>
cd5b9a
+            {node_states}
cd5b9a
+        </status>
cd5b9a
+    """
cd5b9a
+
cd5b9a
 
cd5b9a
+def fixture_digests_xml(resource_id, node_name, devices="", nonprivate=True):
cd5b9a
+    nonprivate_xml = (
cd5b9a
+        f"""
cd5b9a
+        <digest type="nonprivate" hash="{NONPRIVATE_DIGEST}">
cd5b9a
+            <parameters devices="{devices}"/>
cd5b9a
+        </digest>
cd5b9a
+        """
cd5b9a
+        if nonprivate
cd5b9a
+        else ""
cd5b9a
+    )
cd5b9a
 
cd5b9a
-def fixture_digests_xml(resource_id, node_name, devices=""):
cd5b9a
     return f"""
cd5b9a
         <pacemaker-result api-version="2.9" request="crm_resource --digests --resource {resource_id} --node {node_name} --output-as xml devices={devices}">
cd5b9a
             <digests resource="{resource_id}" node="{node_name}" task="stop" interval="0ms">
cd5b9a
                 <digest type="all" hash="{ALL_DIGEST}">
cd5b9a
                     <parameters devices="{devices}" pcmk_host_check="static-list" pcmk_host_list="node1 node2 node3" pcmk_reboot_action="off"/>
cd5b9a
                 </digest>
cd5b9a
-                <digest type="nonprivate" hash="{NONPRIVATE_DIGEST}">
cd5b9a
-                    <parameters devices="{devices}"/>
cd5b9a
-                </digest>
cd5b9a
+                {nonprivate_xml}
cd5b9a
             </digests>
cd5b9a
             <status code="0" message="OK"/>
cd5b9a
         </pacemaker-result>
cd5b9a
@@ -331,6 +391,8 @@ class UpdateScsiDevicesMixin:
cd5b9a
         nodes_running_on=1,
cd5b9a
         start_digests=True,
cd5b9a
         monitor_digests=True,
cd5b9a
+        digests_attrs_list=None,
cd5b9a
+        crm_digests_xml=None,
cd5b9a
     ):
cd5b9a
         # pylint: disable=too-many-arguments
cd5b9a
         # pylint: disable=too-many-locals
cd5b9a
@@ -343,11 +405,12 @@ class UpdateScsiDevicesMixin:
cd5b9a
                 resource_ops=resource_ops,
cd5b9a
                 host_map=host_map,
cd5b9a
             ),
cd5b9a
-            status=_fixture_status_lrm_ops(
cd5b9a
+            status=_fixture_status(
cd5b9a
                 self.stonith_id,
cd5b9a
                 self.stonith_type,
cd5b9a
                 lrm_start_ops=lrm_start_ops,
cd5b9a
                 lrm_monitor_ops=lrm_monitor_ops,
cd5b9a
+                digests_attrs_list=digests_attrs_list,
cd5b9a
             ),
cd5b9a
         )
cd5b9a
         self.config.runner.pcmk.is_resource_digests_supported()
cd5b9a
@@ -360,14 +423,17 @@ class UpdateScsiDevicesMixin:
cd5b9a
             nodes=FIXTURE_CRM_MON_NODES,
cd5b9a
         )
cd5b9a
         devices_opt = "devices={}".format(devices_value)
cd5b9a
+
cd5b9a
+        if crm_digests_xml is None:
cd5b9a
+            crm_digests_xml = fixture_digests_xml(
cd5b9a
+                self.stonith_id, SCSI_NODE, devices=devices_value
cd5b9a
+            )
cd5b9a
         if start_digests:
cd5b9a
             self.config.runner.pcmk.resource_digests(
cd5b9a
                 self.stonith_id,
cd5b9a
                 SCSI_NODE,
cd5b9a
                 name="start.op.digests",
cd5b9a
-                stdout=fixture_digests_xml(
cd5b9a
-                    self.stonith_id, SCSI_NODE, devices=devices_value
cd5b9a
-                ),
cd5b9a
+                stdout=crm_digests_xml,
cd5b9a
                 args=[devices_opt],
cd5b9a
             )
cd5b9a
         if monitor_digests:
cd5b9a
@@ -391,11 +457,7 @@ class UpdateScsiDevicesMixin:
cd5b9a
                     self.stonith_id,
cd5b9a
                     SCSI_NODE,
cd5b9a
                     name=f"{name}-{num}.op.digests",
cd5b9a
-                    stdout=fixture_digests_xml(
cd5b9a
-                        self.stonith_id,
cd5b9a
-                        SCSI_NODE,
cd5b9a
-                        devices=devices_value,
cd5b9a
-                    ),
cd5b9a
+                    stdout=crm_digests_xml,
cd5b9a
                     args=args,
cd5b9a
                 )
cd5b9a
 
cd5b9a
@@ -403,14 +465,16 @@ class UpdateScsiDevicesMixin:
cd5b9a
         self,
cd5b9a
         devices_before=DEVICES_1,
cd5b9a
         devices_updated=DEVICES_2,
cd5b9a
-        devices_add=(),
cd5b9a
-        devices_remove=(),
cd5b9a
+        devices_add=None,
cd5b9a
+        devices_remove=None,
cd5b9a
         unfence=None,
cd5b9a
         resource_ops=DEFAULT_OPS,
cd5b9a
         lrm_monitor_ops=DEFAULT_LRM_MONITOR_OPS,
cd5b9a
         lrm_start_ops=DEFAULT_LRM_START_OPS,
cd5b9a
         lrm_monitor_ops_updated=DEFAULT_LRM_MONITOR_OPS_UPDATED,
cd5b9a
         lrm_start_ops_updated=DEFAULT_LRM_START_OPS_UPDATED,
cd5b9a
+        digests_attrs_list=None,
cd5b9a
+        digests_attrs_list_updated=None,
cd5b9a
     ):
cd5b9a
         # pylint: disable=too-many-arguments
cd5b9a
         self.config_cib(
cd5b9a
@@ -419,6 +483,7 @@ class UpdateScsiDevicesMixin:
cd5b9a
             resource_ops=resource_ops,
cd5b9a
             lrm_monitor_ops=lrm_monitor_ops,
cd5b9a
             lrm_start_ops=lrm_start_ops,
cd5b9a
+            digests_attrs_list=digests_attrs_list,
cd5b9a
         )
cd5b9a
         if unfence:
cd5b9a
             self.config.corosync_conf.load_content(
cd5b9a
@@ -442,20 +507,34 @@ class UpdateScsiDevicesMixin:
cd5b9a
                 devices=devices_updated,
cd5b9a
                 resource_ops=resource_ops,
cd5b9a
             ),
cd5b9a
-            status=_fixture_status_lrm_ops(
cd5b9a
+            status=_fixture_status(
cd5b9a
                 self.stonith_id,
cd5b9a
                 self.stonith_type,
cd5b9a
                 lrm_start_ops=lrm_start_ops_updated,
cd5b9a
                 lrm_monitor_ops=lrm_monitor_ops_updated,
cd5b9a
+                digests_attrs_list=digests_attrs_list_updated,
cd5b9a
             ),
cd5b9a
         )
cd5b9a
-        self.command(
cd5b9a
-            devices_updated=devices_updated,
cd5b9a
-            devices_add=devices_add,
cd5b9a
-            devices_remove=devices_remove,
cd5b9a
-        )()
cd5b9a
+        kwargs = dict(devices_updated=devices_updated)
cd5b9a
+        if devices_add is not None:
cd5b9a
+            kwargs["devices_add"] = devices_add
cd5b9a
+        if devices_remove is not None:
cd5b9a
+            kwargs["devices_remove"] = devices_remove
cd5b9a
+        self.command(**kwargs)()
cd5b9a
         self.env_assist.assert_reports([])
cd5b9a
 
cd5b9a
+    def digest_attr_value_single(self, digest, last_comma=True):
cd5b9a
+        comma = "," if last_comma else ""
cd5b9a
+        return f"{self.stonith_id}:{self.stonith_type}:{digest}{comma}"
cd5b9a
+
cd5b9a
+    def digest_attr_value_multiple(self, digest, last_comma=True):
cd5b9a
+        if self.stonith_type == STONITH_TYPE_SCSI:
cd5b9a
+            value = f"{STONITH_ID_MPATH}:{STONITH_TYPE_MPATH}:{DEFAULT_DIGEST},"
cd5b9a
+        else:
cd5b9a
+            value = f"{STONITH_ID_SCSI}:{STONITH_TYPE_SCSI}:{DEFAULT_DIGEST},"
cd5b9a
+
cd5b9a
+        return f"{value}{self.digest_attr_value_single(digest, last_comma=last_comma)}"
cd5b9a
+
cd5b9a
 
cd5b9a
 class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin):
cd5b9a
     def test_pcmk_doesnt_support_digests(self):
cd5b9a
@@ -564,9 +643,7 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin):
cd5b9a
         )
cd5b9a
 
cd5b9a
     def test_no_lrm_start_op(self):
cd5b9a
-        self.config_cib(
cd5b9a
-            lrm_start_ops=(), start_digests=False, monitor_digests=False
cd5b9a
-        )
cd5b9a
+        self.config_cib(lrm_start_ops=(), monitor_digests=False)
cd5b9a
         self.env_assist.assert_raise_library_error(
cd5b9a
             self.command(),
cd5b9a
             [
cd5b9a
@@ -619,6 +696,59 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin):
cd5b9a
             expected_in_processor=False,
cd5b9a
         )
cd5b9a
 
cd5b9a
+    def test_crm_resource_digests_missing_for_transient_digests_attrs(self):
cd5b9a
+        self.config_cib(
cd5b9a
+            digests_attrs_list=[
cd5b9a
+                [
cd5b9a
+                    (
cd5b9a
+                        "digests-secure",
cd5b9a
+                        self.digest_attr_value_single(ALL_DIGEST),
cd5b9a
+                    ),
cd5b9a
+                ],
cd5b9a
+            ],
cd5b9a
+            crm_digests_xml=fixture_digests_xml(
cd5b9a
+                self.stonith_id, SCSI_NODE, devices="", nonprivate=False
cd5b9a
+            ),
cd5b9a
+        )
cd5b9a
+        self.env_assist.assert_raise_library_error(
cd5b9a
+            self.command(),
cd5b9a
+            [
cd5b9a
+                fixture.error(
cd5b9a
+                    reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM,
cd5b9a
+                    reason=(
cd5b9a
+                        "necessary digest for '#digests-secure' attribute is "
cd5b9a
+                        "missing"
cd5b9a
+                    ),
cd5b9a
+                    reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER,
cd5b9a
+                )
cd5b9a
+            ],
cd5b9a
+            expected_in_processor=False,
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_multiple_digests_attributes(self):
cd5b9a
+        self.config_cib(
cd5b9a
+            digests_attrs_list=[
cd5b9a
+                2
cd5b9a
+                * [
cd5b9a
+                    (
cd5b9a
+                        "digests-all",
cd5b9a
+                        self.digest_attr_value_single(DEFAULT_DIGEST),
cd5b9a
+                    ),
cd5b9a
+                ],
cd5b9a
+            ],
cd5b9a
+        )
cd5b9a
+        self.env_assist.assert_raise_library_error(
cd5b9a
+            self.command(),
cd5b9a
+            [
cd5b9a
+                fixture.error(
cd5b9a
+                    reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM,
cd5b9a
+                    reason=("multiple digests attributes: '#digests-all'"),
cd5b9a
+                    reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER,
cd5b9a
+                )
cd5b9a
+            ],
cd5b9a
+            expected_in_processor=False,
cd5b9a
+        )
cd5b9a
+
cd5b9a
     def test_monitor_ops_and_lrm_monitor_ops_do_not_match(self):
cd5b9a
         self.config_cib(
cd5b9a
             resource_ops=(
cd5b9a
@@ -809,7 +939,7 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin):
cd5b9a
                 stonith_type=self.stonith_type,
cd5b9a
                 devices=DEVICES_2,
cd5b9a
             ),
cd5b9a
-            status=_fixture_status_lrm_ops(
cd5b9a
+            status=_fixture_status(
cd5b9a
                 self.stonith_id,
cd5b9a
                 self.stonith_type,
cd5b9a
                 lrm_start_ops=DEFAULT_LRM_START_OPS_UPDATED,
cd5b9a
@@ -956,6 +1086,28 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin):
cd5b9a
             ]
cd5b9a
         )
cd5b9a
 
cd5b9a
+    def test_transient_digests_attrs_bad_value_format(self):
cd5b9a
+        bad_format = f"{DIGEST_ATTR_VALUE_GOOD_FORMAT}id:type,"
cd5b9a
+        self.config_cib(
cd5b9a
+            digests_attrs_list=[
cd5b9a
+                [
cd5b9a
+                    ("digests-all", DIGEST_ATTR_VALUE_GOOD_FORMAT),
cd5b9a
+                    ("digests-secure", bad_format),
cd5b9a
+                ]
cd5b9a
+            ]
cd5b9a
+        )
cd5b9a
+        self.env_assist.assert_raise_library_error(
cd5b9a
+            self.command(),
cd5b9a
+            [
cd5b9a
+                fixture.error(
cd5b9a
+                    reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM,
cd5b9a
+                    reason=f"invalid digest attribute value: '{bad_format}'",
cd5b9a
+                    reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER,
cd5b9a
+                )
cd5b9a
+            ],
cd5b9a
+            expected_in_processor=False,
cd5b9a
+        )
cd5b9a
+
cd5b9a
 
cd5b9a
 class UpdateScsiDevicesSetBase(UpdateScsiDevicesMixin, CommandSetMixin):
cd5b9a
     def test_update_1_to_1_devices(self):
cd5b9a
@@ -999,80 +1151,6 @@ class UpdateScsiDevicesSetBase(UpdateScsiDevicesMixin, CommandSetMixin):
cd5b9a
             unfence=[DEV_3, DEV_4],
cd5b9a
         )
cd5b9a
 
cd5b9a
-    def test_default_monitor(self):
cd5b9a
-        self.assert_command_success(unfence=[DEV_2])
cd5b9a
-
cd5b9a
-    def test_no_monitor_ops(self):
cd5b9a
-        self.assert_command_success(
cd5b9a
-            unfence=[DEV_2],
cd5b9a
-            resource_ops=(),
cd5b9a
-            lrm_monitor_ops=(),
cd5b9a
-            lrm_monitor_ops_updated=(),
cd5b9a
-        )
cd5b9a
-
cd5b9a
-    def test_1_monitor_with_timeout(self):
cd5b9a
-        self.assert_command_success(
cd5b9a
-            unfence=[DEV_2],
cd5b9a
-            resource_ops=(("monitor", "30s", "10s", None),),
cd5b9a
-            lrm_monitor_ops=(("30000", DEFAULT_DIGEST, None, None),),
cd5b9a
-            lrm_monitor_ops_updated=(("30000", ALL_DIGEST, None, None),),
cd5b9a
-        )
cd5b9a
-
cd5b9a
-    def test_2_monitor_ops_with_timeouts(self):
cd5b9a
-        self.assert_command_success(
cd5b9a
-            unfence=[DEV_2],
cd5b9a
-            resource_ops=(
cd5b9a
-                ("monitor", "30s", "10s", None),
cd5b9a
-                ("monitor", "40s", "20s", None),
cd5b9a
-            ),
cd5b9a
-            lrm_monitor_ops=(
cd5b9a
-                ("30000", DEFAULT_DIGEST, None, None),
cd5b9a
-                ("40000", DEFAULT_DIGEST, None, None),
cd5b9a
-            ),
cd5b9a
-            lrm_monitor_ops_updated=(
cd5b9a
-                ("30000", ALL_DIGEST, None, None),
cd5b9a
-                ("40000", ALL_DIGEST, None, None),
cd5b9a
-            ),
cd5b9a
-        )
cd5b9a
-
cd5b9a
-    def test_2_monitor_ops_with_one_timeout(self):
cd5b9a
-        self.assert_command_success(
cd5b9a
-            unfence=[DEV_2],
cd5b9a
-            resource_ops=(
cd5b9a
-                ("monitor", "30s", "10s", None),
cd5b9a
-                ("monitor", "60s", None, None),
cd5b9a
-            ),
cd5b9a
-            lrm_monitor_ops=(
cd5b9a
-                ("30000", DEFAULT_DIGEST, None, None),
cd5b9a
-                ("60000", DEFAULT_DIGEST, None, None),
cd5b9a
-            ),
cd5b9a
-            lrm_monitor_ops_updated=(
cd5b9a
-                ("30000", ALL_DIGEST, None, None),
cd5b9a
-                ("60000", ALL_DIGEST, None, None),
cd5b9a
-            ),
cd5b9a
-        )
cd5b9a
-
cd5b9a
-    def test_various_start_ops_one_lrm_start_op(self):
cd5b9a
-        self.assert_command_success(
cd5b9a
-            unfence=[DEV_2],
cd5b9a
-            resource_ops=(
cd5b9a
-                ("monitor", "60s", None, None),
cd5b9a
-                ("start", "0s", "40s", None),
cd5b9a
-                ("start", "0s", "30s", "1"),
cd5b9a
-                ("start", "10s", "5s", None),
cd5b9a
-                ("start", "20s", None, None),
cd5b9a
-            ),
cd5b9a
-        )
cd5b9a
-
cd5b9a
-    def test_1_nonrecurring_start_op_with_timeout(self):
cd5b9a
-        self.assert_command_success(
cd5b9a
-            unfence=[DEV_2],
cd5b9a
-            resource_ops=(
cd5b9a
-                ("monitor", "60s", None, None),
cd5b9a
-                ("start", "0s", "40s", None),
cd5b9a
-            ),
cd5b9a
-        )
cd5b9a
-
cd5b9a
 
cd5b9a
 class UpdateScsiDevicesAddRemoveBase(
cd5b9a
     UpdateScsiDevicesMixin, CommandAddRemoveMixin
cd5b9a
@@ -1242,6 +1320,221 @@ class MpathFailuresMixin:
cd5b9a
         self.assert_failure("node1:1;node2=", ["node2", "node3"])
cd5b9a
 
cd5b9a
 
cd5b9a
+class UpdateScsiDevicesDigestsBase(UpdateScsiDevicesMixin):
cd5b9a
+    def test_default_monitor(self):
cd5b9a
+        self.assert_command_success(unfence=[DEV_2])
cd5b9a
+
cd5b9a
+    def test_no_monitor_ops(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            resource_ops=(),
cd5b9a
+            lrm_monitor_ops=(),
cd5b9a
+            lrm_monitor_ops_updated=(),
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_1_monitor_with_timeout(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            resource_ops=(("monitor", "30s", "10s", None),),
cd5b9a
+            lrm_monitor_ops=(("30000", DEFAULT_DIGEST, None, None),),
cd5b9a
+            lrm_monitor_ops_updated=(("30000", ALL_DIGEST, None, None),),
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_2_monitor_ops_with_timeouts(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            resource_ops=(
cd5b9a
+                ("monitor", "30s", "10s", None),
cd5b9a
+                ("monitor", "40s", "20s", None),
cd5b9a
+            ),
cd5b9a
+            lrm_monitor_ops=(
cd5b9a
+                ("30000", DEFAULT_DIGEST, None, None),
cd5b9a
+                ("40000", DEFAULT_DIGEST, None, None),
cd5b9a
+            ),
cd5b9a
+            lrm_monitor_ops_updated=(
cd5b9a
+                ("30000", ALL_DIGEST, None, None),
cd5b9a
+                ("40000", ALL_DIGEST, None, None),
cd5b9a
+            ),
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_2_monitor_ops_with_one_timeout(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            resource_ops=(
cd5b9a
+                ("monitor", "30s", "10s", None),
cd5b9a
+                ("monitor", "60s", None, None),
cd5b9a
+            ),
cd5b9a
+            lrm_monitor_ops=(
cd5b9a
+                ("30000", DEFAULT_DIGEST, None, None),
cd5b9a
+                ("60000", DEFAULT_DIGEST, None, None),
cd5b9a
+            ),
cd5b9a
+            lrm_monitor_ops_updated=(
cd5b9a
+                ("30000", ALL_DIGEST, None, None),
cd5b9a
+                ("60000", ALL_DIGEST, None, None),
cd5b9a
+            ),
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_various_start_ops_one_lrm_start_op(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            resource_ops=(
cd5b9a
+                ("monitor", "60s", None, None),
cd5b9a
+                ("start", "0s", "40s", None),
cd5b9a
+                ("start", "0s", "30s", "1"),
cd5b9a
+                ("start", "10s", "5s", None),
cd5b9a
+                ("start", "20s", None, None),
cd5b9a
+            ),
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_1_nonrecurring_start_op_with_timeout(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            resource_ops=(
cd5b9a
+                ("monitor", "60s", None, None),
cd5b9a
+                ("start", "0s", "40s", None),
cd5b9a
+            ),
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def _digests_attrs_before(self, last_comma=True):
cd5b9a
+        return [
cd5b9a
+            (
cd5b9a
+                "digests-all",
cd5b9a
+                self.digest_attr_value_single(DEFAULT_DIGEST, last_comma),
cd5b9a
+            ),
cd5b9a
+            (
cd5b9a
+                "digests-secure",
cd5b9a
+                self.digest_attr_value_single(DEFAULT_DIGEST, last_comma),
cd5b9a
+            ),
cd5b9a
+        ]
cd5b9a
+
cd5b9a
+    def _digests_attrs_after(self, last_comma=True):
cd5b9a
+        return [
cd5b9a
+            (
cd5b9a
+                "digests-all",
cd5b9a
+                self.digest_attr_value_single(ALL_DIGEST, last_comma),
cd5b9a
+            ),
cd5b9a
+            (
cd5b9a
+                "digests-secure",
cd5b9a
+                self.digest_attr_value_single(NONPRIVATE_DIGEST, last_comma),
cd5b9a
+            ),
cd5b9a
+        ]
cd5b9a
+
cd5b9a
+    def _digests_attrs_before_multi(self, last_comma=True):
cd5b9a
+        return [
cd5b9a
+            (
cd5b9a
+                "digests-all",
cd5b9a
+                self.digest_attr_value_multiple(DEFAULT_DIGEST, last_comma),
cd5b9a
+            ),
cd5b9a
+            (
cd5b9a
+                "digests-secure",
cd5b9a
+                self.digest_attr_value_multiple(DEFAULT_DIGEST, last_comma),
cd5b9a
+            ),
cd5b9a
+        ]
cd5b9a
+
cd5b9a
+    def _digests_attrs_after_multi(self, last_comma=True):
cd5b9a
+        return [
cd5b9a
+            (
cd5b9a
+                "digests-all",
cd5b9a
+                self.digest_attr_value_multiple(ALL_DIGEST, last_comma),
cd5b9a
+            ),
cd5b9a
+            (
cd5b9a
+                "digests-secure",
cd5b9a
+                self.digest_attr_value_multiple(NONPRIVATE_DIGEST, last_comma),
cd5b9a
+            ),
cd5b9a
+        ]
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_all_nodes(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=len(self.existing_nodes)
cd5b9a
+            * [self._digests_attrs_before()],
cd5b9a
+            digests_attrs_list_updated=len(self.existing_nodes)
cd5b9a
+            * [self._digests_attrs_after()],
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_not_on_all_nodes(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=[self._digests_attrs_before()],
cd5b9a
+            digests_attrs_list_updated=[self._digests_attrs_after()],
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_all_nodes_multi_value(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=len(self.existing_nodes)
cd5b9a
+            * [self._digests_attrs_before_multi()],
cd5b9a
+            digests_attrs_list_updated=len(self.existing_nodes)
cd5b9a
+            * [self._digests_attrs_after_multi()],
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_not_on_all_nodes_multi_value(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=[self._digests_attrs_before()],
cd5b9a
+            digests_attrs_list_updated=[self._digests_attrs_after()],
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_not_all_digest_types(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=len(self.existing_nodes)
cd5b9a
+            * [self._digests_attrs_before()[0:1]],
cd5b9a
+            digests_attrs_list_updated=len(self.existing_nodes)
cd5b9a
+            * [self._digests_attrs_after()[0:1]],
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_without_digests_attrs(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=len(self.existing_nodes) * [[]],
cd5b9a
+            digests_attrs_list_updated=len(self.existing_nodes) * [[]],
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_without_last_comma(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=[self._digests_attrs_before(last_comma=False)],
cd5b9a
+            digests_attrs_list_updated=[
cd5b9a
+                self._digests_attrs_after(last_comma=False)
cd5b9a
+            ],
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_without_last_comma_multi_value(self):
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=[
cd5b9a
+                self._digests_attrs_before_multi(last_comma=False)
cd5b9a
+            ],
cd5b9a
+            digests_attrs_list_updated=[
cd5b9a
+                self._digests_attrs_after_multi(last_comma=False)
cd5b9a
+            ],
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_no_digest_for_our_stonith_id(self):
cd5b9a
+        digests_attrs_list = len(self.existing_nodes) * [
cd5b9a
+            [
cd5b9a
+                ("digests-all", DIGEST_ATTR_VALUE_GOOD_FORMAT),
cd5b9a
+                ("digests-secure", DIGEST_ATTR_VALUE_GOOD_FORMAT),
cd5b9a
+            ]
cd5b9a
+        ]
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=digests_attrs_list,
cd5b9a
+            digests_attrs_list_updated=digests_attrs_list,
cd5b9a
+        )
cd5b9a
+
cd5b9a
+    def test_transient_digests_attrs_digests_with_empty_value(self):
cd5b9a
+        digests_attrs_list = len(self.existing_nodes) * [
cd5b9a
+            [("digests-all", ""), ("digests-secure", "")]
cd5b9a
+        ]
cd5b9a
+        self.assert_command_success(
cd5b9a
+            unfence=[DEV_2],
cd5b9a
+            digests_attrs_list=digests_attrs_list,
cd5b9a
+            digests_attrs_list_updated=digests_attrs_list,
cd5b9a
+        )
cd5b9a
+
cd5b9a
+
cd5b9a
 @mock.patch.object(
cd5b9a
     settings,
cd5b9a
     "pacemaker_api_result_schema",
cd5b9a
@@ -1334,3 +1627,47 @@ class TestUpdateScsiDevicesAddRemoveFailuresScsi(
cd5b9a
     UpdateScsiDevicesAddRemoveFailuresBaseMixin, ScsiMixin, TestCase
cd5b9a
 ):
cd5b9a
     pass
cd5b9a
+
cd5b9a
+
cd5b9a
+@mock.patch.object(
cd5b9a
+    settings,
cd5b9a
+    "pacemaker_api_result_schema",
cd5b9a
+    rc("pcmk_api_rng/api-result.rng"),
cd5b9a
+)
cd5b9a
+class TestUpdateScsiDevicesDigestsSetScsi(
cd5b9a
+    UpdateScsiDevicesDigestsBase, ScsiMixin, CommandSetMixin, TestCase
cd5b9a
+):
cd5b9a
+    pass
cd5b9a
+
cd5b9a
+
cd5b9a
+@mock.patch.object(
cd5b9a
+    settings,
cd5b9a
+    "pacemaker_api_result_schema",
cd5b9a
+    rc("pcmk_api_rng/api-result.rng"),
cd5b9a
+)
cd5b9a
+class TestUpdateScsiDevicesDigestsAddRemoveScsi(
cd5b9a
+    UpdateScsiDevicesDigestsBase, ScsiMixin, CommandAddRemoveMixin, TestCase
cd5b9a
+):
cd5b9a
+    pass
cd5b9a
+
cd5b9a
+
cd5b9a
+@mock.patch.object(
cd5b9a
+    settings,
cd5b9a
+    "pacemaker_api_result_schema",
cd5b9a
+    rc("pcmk_api_rng/api-result.rng"),
cd5b9a
+)
cd5b9a
+class TestUpdateScsiDevicesDigestsSetMpath(
cd5b9a
+    UpdateScsiDevicesDigestsBase, MpathMixin, CommandSetMixin, TestCase
cd5b9a
+):
cd5b9a
+    pass
cd5b9a
+
cd5b9a
+
cd5b9a
+@mock.patch.object(
cd5b9a
+    settings,
cd5b9a
+    "pacemaker_api_result_schema",
cd5b9a
+    rc("pcmk_api_rng/api-result.rng"),
cd5b9a
+)
cd5b9a
+class TestUpdateScsiDevicesDigestsAddRemoveMpath(
cd5b9a
+    UpdateScsiDevicesDigestsBase, MpathMixin, CommandAddRemoveMixin, TestCase
cd5b9a
+):
cd5b9a
+    pass
cd5b9a
-- 
cd5b9a
2.39.2
cd5b9a