Blame SOURCES/bz1991654-01-fix-unfencing-in-pcs-stonith-update-scsi-devices.patch

ce1222
From cf68ded959ad03244c94de308b79fc1af806a474 Mon Sep 17 00:00:00 2001
ce1222
From: Ondrej Mular <omular@redhat.com>
ce1222
Date: Wed, 15 Sep 2021 07:55:50 +0200
ce1222
Subject: [PATCH 1/2] fix unfencing in `pcs stonith update-scsi-devices`
ce1222
ce1222
* do not unfence newly added devices on fenced cluster nodes
ce1222
---
ce1222
 pcs/common/reports/codes.py                   |   6 ++
ce1222
 pcs/common/reports/messages.py                |  41 +++++++
ce1222
 pcs/lib/commands/scsi.py                      |  55 +++++++++-
ce1222
 pcs/lib/commands/stonith.py                   |  26 +++--
ce1222
 pcs/lib/communication/scsi.py                 |  40 ++++---
ce1222
 .../tier0/common/reports/test_messages.py     |  24 +++++
ce1222
 pcs_test/tier0/lib/commands/test_scsi.py      | 101 ++++++++++++++++--
ce1222
 .../test_stonith_update_scsi_devices.py       |  87 ++++++++++++---
ce1222
 .../tools/command_env/config_http_scsi.py     |  16 ++-
ce1222
 .../tools/command_env/config_runner_scsi.py   |  36 ++++++-
ce1222
 pcsd/api_v1.rb                                |   2 +-
ce1222
 pcsd/capabilities.xml                         |   8 +-
ce1222
 12 files changed, 387 insertions(+), 55 deletions(-)
ce1222
ce1222
diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py
ce1222
index bbd61500..4bee0bac 100644
ce1222
--- a/pcs/common/reports/codes.py
ce1222
+++ b/pcs/common/reports/codes.py
ce1222
@@ -468,6 +468,12 @@ STONITH_RESTARTLESS_UPDATE_UNSUPPORTED_AGENT = M(
ce1222
     "STONITH_RESTARTLESS_UPDATE_UNSUPPORTED_AGENT"
ce1222
 )
ce1222
 STONITH_UNFENCING_FAILED = M("STONITH_UNFENCING_FAILED")
ce1222
+STONITH_UNFENCING_DEVICE_STATUS_FAILED = M(
ce1222
+    "STONITH_UNFENCING_DEVICE_STATUS_FAILED"
ce1222
+)
ce1222
+STONITH_UNFENCING_SKIPPED_DEVICES_FENCED = M(
ce1222
+    "STONITH_UNFENCING_SKIPPED_DEVICES_FENCED"
ce1222
+)
ce1222
 STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM = M(
ce1222
     "STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM"
ce1222
 )
ce1222
diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py
ce1222
index f9688437..be8dd154 100644
ce1222
--- a/pcs/common/reports/messages.py
ce1222
+++ b/pcs/common/reports/messages.py
ce1222
@@ -2782,6 +2782,47 @@ class StonithUnfencingFailed(ReportItemMessage):
ce1222
         return f"Unfencing failed:\n{self.reason}"
ce1222
 
ce1222
 
ce1222
+@dataclass(frozen=True)
ce1222
+class StonithUnfencingDeviceStatusFailed(ReportItemMessage):
ce1222
+    """
ce1222
+    Unfencing failed on a cluster node.
ce1222
+    """
ce1222
+
ce1222
+    device: str
ce1222
+    reason: str
ce1222
+
ce1222
+    _code = codes.STONITH_UNFENCING_DEVICE_STATUS_FAILED
ce1222
+
ce1222
+    @property
ce1222
+    def message(self) -> str:
ce1222
+        return (
ce1222
+            "Unfencing failed, unable to check status of device "
ce1222
+            f"'{self.device}': {self.reason}"
ce1222
+        )
ce1222
+
ce1222
+
ce1222
+@dataclass(frozen=True)
ce1222
+class StonithUnfencingSkippedDevicesFenced(ReportItemMessage):
ce1222
+    """
ce1222
+    Unfencing skipped on a cluster node, because fenced devices were found on
ce1222
+    the node.
ce1222
+    """
ce1222
+
ce1222
+    devices: List[str]
ce1222
+
ce1222
+    _code = codes.STONITH_UNFENCING_SKIPPED_DEVICES_FENCED
ce1222
+
ce1222
+    @property
ce1222
+    def message(self) -> str:
ce1222
+        return (
ce1222
+            "Unfencing skipped, {device_pl} {devices} {is_pl} fenced"
ce1222
+        ).format(
ce1222
+            device_pl=format_plural(self.devices, "device"),
ce1222
+            devices=format_list(self.devices),
ce1222
+            is_pl=format_plural(self.devices, "is", "are"),
ce1222
+        )
ce1222
+
ce1222
+
ce1222
 @dataclass(frozen=True)
ce1222
 class StonithRestartlessUpdateUnableToPerform(ReportItemMessage):
ce1222
     """
ce1222
diff --git a/pcs/lib/commands/scsi.py b/pcs/lib/commands/scsi.py
ce1222
index 31a3ef2d..ff20a563 100644
ce1222
--- a/pcs/lib/commands/scsi.py
ce1222
+++ b/pcs/lib/commands/scsi.py
ce1222
@@ -8,20 +8,65 @@ from pcs.lib.env import LibraryEnvironment
ce1222
 from pcs.lib.errors import LibraryError
ce1222
 
ce1222
 
ce1222
-def unfence_node(env: LibraryEnvironment, node: str, devices: Iterable[str]):
ce1222
+def unfence_node(
ce1222
+    env: LibraryEnvironment,
ce1222
+    node: str,
ce1222
+    original_devices: Iterable[str],
ce1222
+    updated_devices: Iterable[str],
ce1222
+) -> None:
ce1222
     """
ce1222
-    Unfence scsi devices on a node by calling fence_scsi agent script.
ce1222
+    Unfence scsi devices on a node by calling fence_scsi agent script. Only
ce1222
+    newly added devices will be unfenced (set(updated_devices) -
ce1222
+    set(original_devices)). Before unfencing, original devices are be checked
ce1222
+    if any of them are not fenced. If there is a fenced device, unfencing will
ce1222
+    be skipped.
ce1222
 
ce1222
     env -- provides communication with externals
ce1222
     node -- node name on wich is unfencing performed
ce1222
-    devices -- scsi devices to be unfenced
ce1222
+    original_devices -- list of devices defined before update
ce1222
+    updated_devices -- list of devices defined after update
ce1222
     """
ce1222
+    devices_to_unfence = set(updated_devices) - set(original_devices)
ce1222
+    if not devices_to_unfence:
ce1222
+        return
ce1222
+    fence_scsi_bin = os.path.join(settings.fence_agent_binaries, "fence_scsi")
ce1222
+    fenced_devices = []
ce1222
+    for device in original_devices:
ce1222
+        stdout, stderr, return_code = env.cmd_runner().run(
ce1222
+            [
ce1222
+                fence_scsi_bin,
ce1222
+                "--action=status",
ce1222
+                f"--devices={device}",
ce1222
+                f"--plug={node}",
ce1222
+            ]
ce1222
+        )
ce1222
+        if return_code == 2:
ce1222
+            fenced_devices.append(device)
ce1222
+        elif return_code != 0:
ce1222
+            raise LibraryError(
ce1222
+                reports.ReportItem.error(
ce1222
+                    reports.messages.StonithUnfencingDeviceStatusFailed(
ce1222
+                        device, join_multilines([stderr, stdout])
ce1222
+                    )
ce1222
+                )
ce1222
+            )
ce1222
+    if fenced_devices:
ce1222
+        # At least one of existing devices is off, which means the node has
ce1222
+        # been fenced and new devices should not be unfenced.
ce1222
+        env.report_processor.report(
ce1222
+            reports.ReportItem.info(
ce1222
+                reports.messages.StonithUnfencingSkippedDevicesFenced(
ce1222
+                    fenced_devices
ce1222
+                )
ce1222
+            )
ce1222
+        )
ce1222
+        return
ce1222
     stdout, stderr, return_code = env.cmd_runner().run(
ce1222
         [
ce1222
-            os.path.join(settings.fence_agent_binaries, "fence_scsi"),
ce1222
+            fence_scsi_bin,
ce1222
             "--action=on",
ce1222
             "--devices",
ce1222
-            ",".join(sorted(devices)),
ce1222
+            ",".join(sorted(devices_to_unfence)),
ce1222
             f"--plug={node}",
ce1222
         ],
ce1222
     )
ce1222
diff --git a/pcs/lib/commands/stonith.py b/pcs/lib/commands/stonith.py
ce1222
index 6f26e7d3..0dcf44f2 100644
ce1222
--- a/pcs/lib/commands/stonith.py
ce1222
+++ b/pcs/lib/commands/stonith.py
ce1222
@@ -453,7 +453,8 @@ def _update_scsi_devices_get_element_and_devices(
ce1222
 
ce1222
 def _unfencing_scsi_devices(
ce1222
     env: LibraryEnvironment,
ce1222
-    device_list: Iterable[str],
ce1222
+    original_devices: Iterable[str],
ce1222
+    updated_devices: Iterable[str],
ce1222
     force_flags: Container[reports.types.ForceCode] = (),
ce1222
 ) -> None:
ce1222
     """
ce1222
@@ -461,9 +462,13 @@ def _unfencing_scsi_devices(
ce1222
     to pcsd and corosync is running.
ce1222
 
ce1222
     env -- provides all for communication with externals
ce1222
-    device_list -- devices to be unfenced
ce1222
+    original_devices -- devices before update
ce1222
+    updated_devices -- devices after update
ce1222
     force_flags -- list of flags codes
ce1222
     """
ce1222
+    devices_to_unfence = set(updated_devices) - set(original_devices)
ce1222
+    if not devices_to_unfence:
ce1222
+        return
ce1222
     cluster_nodes_names, nodes_report_list = get_existing_nodes_names(
ce1222
         env.get_corosync_conf(),
ce1222
         error_on_missing_name=True,
ce1222
@@ -487,7 +492,11 @@ def _unfencing_scsi_devices(
ce1222
     online_corosync_target_list = run_and_raise(
ce1222
         env.get_node_communicator(), com_cmd
ce1222
     )
ce1222
-    com_cmd = Unfence(env.report_processor, sorted(device_list))
ce1222
+    com_cmd = Unfence(
ce1222
+        env.report_processor,
ce1222
+        original_devices=sorted(original_devices),
ce1222
+        updated_devices=sorted(updated_devices),
ce1222
+    )
ce1222
     com_cmd.set_targets(online_corosync_target_list)
ce1222
     run_and_raise(env.get_node_communicator(), com_cmd)
ce1222
 
ce1222
@@ -531,9 +540,9 @@ def update_scsi_devices(
ce1222
         IdProvider(stonith_el),
ce1222
         set_device_list,
ce1222
     )
ce1222
-    devices_for_unfencing = set(set_device_list).difference(current_device_list)
ce1222
-    if devices_for_unfencing:
ce1222
-        _unfencing_scsi_devices(env, devices_for_unfencing, force_flags)
ce1222
+    _unfencing_scsi_devices(
ce1222
+        env, current_device_list, set_device_list, force_flags
ce1222
+    )
ce1222
     env.push_cib()
ce1222
 
ce1222
 
ce1222
@@ -585,6 +594,7 @@ def update_scsi_devices_add_remove(
ce1222
         IdProvider(stonith_el),
ce1222
         updated_device_set,
ce1222
     )
ce1222
-    if add_device_list:
ce1222
-        _unfencing_scsi_devices(env, add_device_list, force_flags)
ce1222
+    _unfencing_scsi_devices(
ce1222
+        env, current_device_list, updated_device_set, force_flags
ce1222
+    )
ce1222
     env.push_cib()
ce1222
diff --git a/pcs/lib/communication/scsi.py b/pcs/lib/communication/scsi.py
ce1222
index 7b272017..250d67aa 100644
ce1222
--- a/pcs/lib/communication/scsi.py
ce1222
+++ b/pcs/lib/communication/scsi.py
ce1222
@@ -1,4 +1,5 @@
ce1222
 import json
ce1222
+from typing import Iterable
ce1222
 
ce1222
 from dacite import DaciteError
ce1222
 
ce1222
@@ -26,9 +27,15 @@ class Unfence(
ce1222
     MarkSuccessfulMixin,
ce1222
     RunRemotelyBase,
ce1222
 ):
ce1222
-    def __init__(self, report_processor, devices):
ce1222
+    def __init__(
ce1222
+        self,
ce1222
+        report_processor: reports.ReportProcessor,
ce1222
+        original_devices: Iterable[str],
ce1222
+        updated_devices: Iterable[str],
ce1222
+    ) -> None:
ce1222
         super().__init__(report_processor)
ce1222
-        self._devices = devices
ce1222
+        self._original_devices = original_devices
ce1222
+        self._updated_devices = updated_devices
ce1222
 
ce1222
     def _get_request_data(self):
ce1222
         return None
ce1222
@@ -38,9 +45,13 @@ class Unfence(
ce1222
             Request(
ce1222
                 target,
ce1222
                 RequestData(
ce1222
-                    "api/v1/scsi-unfence-node/v1",
ce1222
+                    "api/v1/scsi-unfence-node/v2",
ce1222
                     data=json.dumps(
ce1222
-                        {"devices": self._devices, "node": target.label}
ce1222
+                        dict(
ce1222
+                            node=target.label,
ce1222
+                            original_devices=self._original_devices,
ce1222
+                            updated_devices=self._updated_devices,
ce1222
+                        )
ce1222
                     ),
ce1222
                 ),
ce1222
             )
ce1222
@@ -48,7 +59,9 @@ class Unfence(
ce1222
         ]
ce1222
 
ce1222
     def _process_response(self, response):
ce1222
-        report_item = response_to_report_item(response)
ce1222
+        report_item = response_to_report_item(
ce1222
+            response, report_pcsd_too_old_on_404=True
ce1222
+        )
ce1222
         if report_item:
ce1222
             self._report(report_item)
ce1222
             return
ce1222
@@ -57,15 +70,14 @@ class Unfence(
ce1222
             result = from_dict(
ce1222
                 InternalCommunicationResultDto, json.loads(response.data)
ce1222
             )
ce1222
-            if result.status != const.COM_STATUS_SUCCESS:
ce1222
-                context = reports.ReportItemContext(node_label)
ce1222
-                self._report_list(
ce1222
-                    [
ce1222
-                        reports.report_dto_to_item(report, context)
ce1222
-                        for report in result.report_list
ce1222
-                    ]
ce1222
-                )
ce1222
-            else:
ce1222
+            context = reports.ReportItemContext(node_label)
ce1222
+            self._report_list(
ce1222
+                [
ce1222
+                    reports.report_dto_to_item(report, context)
ce1222
+                    for report in result.report_list
ce1222
+                ]
ce1222
+            )
ce1222
+            if result.status == const.COM_STATUS_SUCCESS:
ce1222
                 self._on_success()
ce1222
 
ce1222
         except (json.JSONDecodeError, DaciteError):
ce1222
diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py
ce1222
index b0826cfd..05c3f619 100644
ce1222
--- a/pcs_test/tier0/common/reports/test_messages.py
ce1222
+++ b/pcs_test/tier0/common/reports/test_messages.py
ce1222
@@ -1904,6 +1904,30 @@ class StonithUnfencingFailed(NameBuildTest):
ce1222
         )
ce1222
 
ce1222
 
ce1222
+class StonithUnfencingDeviceStatusFailed(NameBuildTest):
ce1222
+    def test_build_message(self):
ce1222
+        self.assert_message_from_report(
ce1222
+            "Unfencing failed, unable to check status of device 'dev1': reason",
ce1222
+            reports.StonithUnfencingDeviceStatusFailed("dev1", "reason"),
ce1222
+        )
ce1222
+
ce1222
+
ce1222
+class StonithUnfencingSkippedDevicesFenced(NameBuildTest):
ce1222
+    def test_one_device(self):
ce1222
+        self.assert_message_from_report(
ce1222
+            "Unfencing skipped, device 'dev1' is fenced",
ce1222
+            reports.StonithUnfencingSkippedDevicesFenced(["dev1"]),
ce1222
+        )
ce1222
+
ce1222
+    def test_multiple_devices(self):
ce1222
+        self.assert_message_from_report(
ce1222
+            "Unfencing skipped, devices 'dev1', 'dev2', 'dev3' are fenced",
ce1222
+            reports.StonithUnfencingSkippedDevicesFenced(
ce1222
+                ["dev2", "dev1", "dev3"]
ce1222
+            ),
ce1222
+        )
ce1222
+
ce1222
+
ce1222
 class StonithRestartlessUpdateUnableToPerform(NameBuildTest):
ce1222
     def test_build_message(self):
ce1222
         self.assert_message_from_report(
ce1222
diff --git a/pcs_test/tier0/lib/commands/test_scsi.py b/pcs_test/tier0/lib/commands/test_scsi.py
ce1222
index de75743f..8ef9836a 100644
ce1222
--- a/pcs_test/tier0/lib/commands/test_scsi.py
ce1222
+++ b/pcs_test/tier0/lib/commands/test_scsi.py
ce1222
@@ -10,26 +10,113 @@ from pcs.lib.commands import scsi
ce1222
 class TestUnfenceNode(TestCase):
ce1222
     def setUp(self):
ce1222
         self.env_assist, self.config = get_env_tools(self)
ce1222
+        self.old_devices = ["device1", "device3"]
ce1222
+        self.new_devices = ["device3", "device0", "device2"]
ce1222
+        self.added_devices = set(self.new_devices) - set(self.old_devices)
ce1222
+        self.node = "node1"
ce1222
 
ce1222
-    def test_success(self):
ce1222
-        self.config.runner.scsi.unfence_node("node1", ["/dev/sda", "/dev/sdb"])
ce1222
+    def test_success_devices_to_unfence(self):
ce1222
+        for old_dev in self.old_devices:
ce1222
+            self.config.runner.scsi.get_status(
ce1222
+                self.node, old_dev, name=f"runner.scsi.is_fenced.{old_dev}"
ce1222
+            )
ce1222
+        self.config.runner.scsi.unfence_node(self.node, self.added_devices)
ce1222
         scsi.unfence_node(
ce1222
-            self.env_assist.get_env(), "node1", ["/dev/sdb", "/dev/sda"]
ce1222
+            self.env_assist.get_env(),
ce1222
+            self.node,
ce1222
+            self.old_devices,
ce1222
+            self.new_devices,
ce1222
         )
ce1222
         self.env_assist.assert_reports([])
ce1222
 
ce1222
-    def test_failure(self):
ce1222
+    def test_success_no_devices_to_unfence(self):
ce1222
+        scsi.unfence_node(
ce1222
+            self.env_assist.get_env(),
ce1222
+            self.node,
ce1222
+            {"device1", "device2", "device3"},
ce1222
+            {"device3"},
ce1222
+        )
ce1222
+        self.env_assist.assert_reports([])
ce1222
+
ce1222
+    def test_unfencing_failure(self):
ce1222
+        err_msg = "stderr"
ce1222
+        for old_dev in self.old_devices:
ce1222
+            self.config.runner.scsi.get_status(
ce1222
+                self.node, old_dev, name=f"runner.scsi.is_fenced.{old_dev}"
ce1222
+            )
ce1222
         self.config.runner.scsi.unfence_node(
ce1222
-            "node1", ["/dev/sda", "/dev/sdb"], stderr="stderr", return_code=1
ce1222
+            self.node, self.added_devices, stderr=err_msg, return_code=1
ce1222
         )
ce1222
         self.env_assist.assert_raise_library_error(
ce1222
             lambda: scsi.unfence_node(
ce1222
-                self.env_assist.get_env(), "node1", ["/dev/sdb", "/dev/sda"]
ce1222
+                self.env_assist.get_env(),
ce1222
+                self.node,
ce1222
+                self.old_devices,
ce1222
+                self.new_devices,
ce1222
             ),
ce1222
             [
ce1222
                 fixture.error(
ce1222
-                    report_codes.STONITH_UNFENCING_FAILED, reason="stderr"
ce1222
+                    report_codes.STONITH_UNFENCING_FAILED, reason=err_msg
ce1222
                 )
ce1222
             ],
ce1222
             expected_in_processor=False,
ce1222
         )
ce1222
+
ce1222
+    def test_device_status_failed(self):
ce1222
+        err_msg = "stderr"
ce1222
+        new_devices = ["device1", "device2", "device3", "device4"]
ce1222
+        old_devices = new_devices[:-1]
ce1222
+        ok_devices = new_devices[0:2]
ce1222
+        err_device = new_devices[2]
ce1222
+        for dev in ok_devices:
ce1222
+            self.config.runner.scsi.get_status(
ce1222
+                self.node, dev, name=f"runner.scsi.is_fenced.{dev}"
ce1222
+            )
ce1222
+        self.config.runner.scsi.get_status(
ce1222
+            self.node,
ce1222
+            err_device,
ce1222
+            name=f"runner.scsi.is_fenced.{err_device}",
ce1222
+            stderr=err_msg,
ce1222
+            return_code=1,
ce1222
+        )
ce1222
+        self.env_assist.assert_raise_library_error(
ce1222
+            lambda: scsi.unfence_node(
ce1222
+                self.env_assist.get_env(),
ce1222
+                self.node,
ce1222
+                old_devices,
ce1222
+                new_devices,
ce1222
+            ),
ce1222
+            [
ce1222
+                fixture.error(
ce1222
+                    report_codes.STONITH_UNFENCING_DEVICE_STATUS_FAILED,
ce1222
+                    device=err_device,
ce1222
+                    reason=err_msg,
ce1222
+                )
ce1222
+            ],
ce1222
+            expected_in_processor=False,
ce1222
+        )
ce1222
+
ce1222
+    def test_unfencing_skipped_devices_are_fenced(self):
ce1222
+        stdout_off = "Status: OFF"
ce1222
+        for old_dev in self.old_devices:
ce1222
+            self.config.runner.scsi.get_status(
ce1222
+                self.node,
ce1222
+                old_dev,
ce1222
+                name=f"runner.scsi.is_fenced.{old_dev}",
ce1222
+                stdout=stdout_off,
ce1222
+                return_code=2,
ce1222
+            )
ce1222
+        scsi.unfence_node(
ce1222
+            self.env_assist.get_env(),
ce1222
+            self.node,
ce1222
+            self.old_devices,
ce1222
+            self.new_devices,
ce1222
+        )
ce1222
+        self.env_assist.assert_reports(
ce1222
+            [
ce1222
+                fixture.info(
ce1222
+                    report_codes.STONITH_UNFENCING_SKIPPED_DEVICES_FENCED,
ce1222
+                    devices=sorted(self.old_devices),
ce1222
+                )
ce1222
+            ]
ce1222
+        )
ce1222
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
ce1222
index 6ff6b99a..ed8f5d4f 100644
ce1222
--- a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py
ce1222
+++ b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py
ce1222
@@ -1,3 +1,4 @@
ce1222
+# pylint: disable=too-many-lines
ce1222
 import json
ce1222
 from unittest import mock, TestCase
ce1222
 
ce1222
@@ -297,7 +298,9 @@ class UpdateScsiDevicesMixin:
ce1222
                 node_labels=self.existing_nodes
ce1222
             )
ce1222
             self.config.http.scsi.unfence_node(
ce1222
-                unfence, node_labels=self.existing_nodes
ce1222
+                original_devices=devices_before,
ce1222
+                updated_devices=devices_updated,
ce1222
+                node_labels=self.existing_nodes,
ce1222
             )
ce1222
         self.config.env.push_cib(
ce1222
             resources=fixture_scsi(
ce1222
@@ -449,14 +452,14 @@ class UpdateScsiDevicesFailuresMixin:
ce1222
             node_labels=self.existing_nodes
ce1222
         )
ce1222
         self.config.http.scsi.unfence_node(
ce1222
-            DEVICES_2,
ce1222
             communication_list=[
ce1222
                 dict(
ce1222
                     label=self.existing_nodes[0],
ce1222
                     raw_data=json.dumps(
ce1222
                         dict(
ce1222
-                            devices=[DEV_2],
ce1222
                             node=self.existing_nodes[0],
ce1222
+                            original_devices=DEVICES_1,
ce1222
+                            updated_devices=DEVICES_2,
ce1222
                         )
ce1222
                     ),
ce1222
                     was_connected=False,
ce1222
@@ -466,8 +469,9 @@ class UpdateScsiDevicesFailuresMixin:
ce1222
                     label=self.existing_nodes[1],
ce1222
                     raw_data=json.dumps(
ce1222
                         dict(
ce1222
-                            devices=[DEV_2],
ce1222
                             node=self.existing_nodes[1],
ce1222
+                            original_devices=DEVICES_1,
ce1222
+                            updated_devices=DEVICES_2,
ce1222
                         )
ce1222
                     ),
ce1222
                     output=json.dumps(
ce1222
@@ -491,8 +495,9 @@ class UpdateScsiDevicesFailuresMixin:
ce1222
                     label=self.existing_nodes[2],
ce1222
                     raw_data=json.dumps(
ce1222
                         dict(
ce1222
-                            devices=[DEV_2],
ce1222
                             node=self.existing_nodes[2],
ce1222
+                            original_devices=DEVICES_1,
ce1222
+                            updated_devices=DEVICES_2,
ce1222
                         )
ce1222
                     ),
ce1222
                 ),
ce1222
@@ -504,7 +509,7 @@ class UpdateScsiDevicesFailuresMixin:
ce1222
                 fixture.error(
ce1222
                     reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
ce1222
                     node=self.existing_nodes[0],
ce1222
-                    command="api/v1/scsi-unfence-node/v1",
ce1222
+                    command="api/v1/scsi-unfence-node/v2",
ce1222
                     reason="errA",
ce1222
                 ),
ce1222
                 fixture.error(
ce1222
@@ -517,20 +522,76 @@ class UpdateScsiDevicesFailuresMixin:
ce1222
             ]
ce1222
         )
ce1222
 
ce1222
+    def test_unfence_failure_unknown_command(self):
ce1222
+        self._unfence_failure_common_calls()
ce1222
+        self.config.http.corosync.get_corosync_online_targets(
ce1222
+            node_labels=self.existing_nodes
ce1222
+        )
ce1222
+        communication_list = [
ce1222
+            dict(
ce1222
+                label=node,
ce1222
+                raw_data=json.dumps(
ce1222
+                    dict(
ce1222
+                        node=node,
ce1222
+                        original_devices=DEVICES_1,
ce1222
+                        updated_devices=DEVICES_2,
ce1222
+                    )
ce1222
+                ),
ce1222
+            )
ce1222
+            for node in self.existing_nodes[0:2]
ce1222
+        ]
ce1222
+        communication_list.append(
ce1222
+            dict(
ce1222
+                label=self.existing_nodes[2],
ce1222
+                response_code=404,
ce1222
+                raw_data=json.dumps(
ce1222
+                    dict(
ce1222
+                        node=self.existing_nodes[2],
ce1222
+                        original_devices=DEVICES_1,
ce1222
+                        updated_devices=DEVICES_2,
ce1222
+                    )
ce1222
+                ),
ce1222
+                output=json.dumps(
ce1222
+                    dto.to_dict(
ce1222
+                        communication.dto.InternalCommunicationResultDto(
ce1222
+                            status=communication.const.COM_STATUS_UNKNOWN_CMD,
ce1222
+                            status_msg=(
ce1222
+                                "Unknown command '/api/v1/scsi-unfence-node/v2'"
ce1222
+                            ),
ce1222
+                            report_list=[],
ce1222
+                            data=None,
ce1222
+                        )
ce1222
+                    )
ce1222
+                ),
ce1222
+            ),
ce1222
+        )
ce1222
+        self.config.http.scsi.unfence_node(
ce1222
+            communication_list=communication_list
ce1222
+        )
ce1222
+        self.env_assist.assert_raise_library_error(self.command())
ce1222
+        self.env_assist.assert_reports(
ce1222
+            [
ce1222
+                fixture.error(
ce1222
+                    reports.codes.PCSD_VERSION_TOO_OLD,
ce1222
+                    node=self.existing_nodes[2],
ce1222
+                ),
ce1222
+            ]
ce1222
+        )
ce1222
+
ce1222
     def test_unfence_failure_agent_script_failed(self):
ce1222
         self._unfence_failure_common_calls()
ce1222
         self.config.http.corosync.get_corosync_online_targets(
ce1222
             node_labels=self.existing_nodes
ce1222
         )
ce1222
         self.config.http.scsi.unfence_node(
ce1222
-            DEVICES_2,
ce1222
             communication_list=[
ce1222
                 dict(
ce1222
                     label=self.existing_nodes[0],
ce1222
                     raw_data=json.dumps(
ce1222
                         dict(
ce1222
-                            devices=[DEV_2],
ce1222
                             node=self.existing_nodes[0],
ce1222
+                            original_devices=DEVICES_1,
ce1222
+                            updated_devices=DEVICES_2,
ce1222
                         )
ce1222
                     ),
ce1222
                 ),
ce1222
@@ -538,8 +599,9 @@ class UpdateScsiDevicesFailuresMixin:
ce1222
                     label=self.existing_nodes[1],
ce1222
                     raw_data=json.dumps(
ce1222
                         dict(
ce1222
-                            devices=[DEV_2],
ce1222
                             node=self.existing_nodes[1],
ce1222
+                            original_devices=DEVICES_1,
ce1222
+                            updated_devices=DEVICES_2,
ce1222
                         )
ce1222
                     ),
ce1222
                     output=json.dumps(
ce1222
@@ -563,8 +625,9 @@ class UpdateScsiDevicesFailuresMixin:
ce1222
                     label=self.existing_nodes[2],
ce1222
                     raw_data=json.dumps(
ce1222
                         dict(
ce1222
-                            devices=[DEV_2],
ce1222
                             node=self.existing_nodes[2],
ce1222
+                            original_devices=DEVICES_1,
ce1222
+                            updated_devices=DEVICES_2,
ce1222
                         )
ce1222
                     ),
ce1222
                 ),
ce1222
@@ -639,14 +702,14 @@ class UpdateScsiDevicesFailuresMixin:
ce1222
             ]
ce1222
         )
ce1222
         self.config.http.scsi.unfence_node(
ce1222
-            DEVICES_2,
ce1222
             communication_list=[
ce1222
                 dict(
ce1222
                     label=self.existing_nodes[0],
ce1222
                     raw_data=json.dumps(
ce1222
                         dict(
ce1222
-                            devices=[DEV_2],
ce1222
                             node=self.existing_nodes[0],
ce1222
+                            original_devices=DEVICES_1,
ce1222
+                            updated_devices=DEVICES_2,
ce1222
                         )
ce1222
                     ),
ce1222
                 ),
ce1222
diff --git a/pcs_test/tools/command_env/config_http_scsi.py b/pcs_test/tools/command_env/config_http_scsi.py
ce1222
index 0e9f63af..7150eef9 100644
ce1222
--- a/pcs_test/tools/command_env/config_http_scsi.py
ce1222
+++ b/pcs_test/tools/command_env/config_http_scsi.py
ce1222
@@ -14,7 +14,8 @@ class ScsiShortcuts:
ce1222
 
ce1222
     def unfence_node(
ce1222
         self,
ce1222
-        devices,
ce1222
+        original_devices=(),
ce1222
+        updated_devices=(),
ce1222
         node_labels=None,
ce1222
         communication_list=None,
ce1222
         name="http.scsi.unfence_node",
ce1222
@@ -22,7 +23,8 @@ class ScsiShortcuts:
ce1222
         """
ce1222
         Create a calls for node unfencing
ce1222
 
ce1222
-        list devices -- list of scsi devices
ce1222
+        list original_devices -- list of scsi devices before an update
ce1222
+        list updated_devices -- list of scsi devices after an update
ce1222
         list node_labels -- create success responses from these nodes
ce1222
         list communication_list -- use these custom responses
ce1222
         string name -- the key of this call
ce1222
@@ -39,7 +41,13 @@ class ScsiShortcuts:
ce1222
             communication_list = [
ce1222
                 dict(
ce1222
                     label=node,
ce1222
-                    raw_data=json.dumps(dict(devices=devices, node=node)),
ce1222
+                    raw_data=json.dumps(
ce1222
+                        dict(
ce1222
+                            node=node,
ce1222
+                            original_devices=original_devices,
ce1222
+                            updated_devices=updated_devices,
ce1222
+                        )
ce1222
+                    ),
ce1222
                 )
ce1222
                 for node in node_labels
ce1222
             ]
ce1222
@@ -47,7 +55,7 @@ class ScsiShortcuts:
ce1222
             self.__calls,
ce1222
             name,
ce1222
             communication_list,
ce1222
-            action="api/v1/scsi-unfence-node/v1",
ce1222
+            action="api/v1/scsi-unfence-node/v2",
ce1222
             output=json.dumps(
ce1222
                 to_dict(
ce1222
                     communication.dto.InternalCommunicationResultDto(
ce1222
diff --git a/pcs_test/tools/command_env/config_runner_scsi.py b/pcs_test/tools/command_env/config_runner_scsi.py
ce1222
index 4b671bb7..3cee13d6 100644
ce1222
--- a/pcs_test/tools/command_env/config_runner_scsi.py
ce1222
+++ b/pcs_test/tools/command_env/config_runner_scsi.py
ce1222
@@ -35,7 +35,41 @@ class ScsiShortcuts:
ce1222
                     os.path.join(settings.fence_agent_binaries, "fence_scsi"),
ce1222
                     "--action=on",
ce1222
                     "--devices",
ce1222
-                    ",".join(devices),
ce1222
+                    ",".join(sorted(devices)),
ce1222
+                    f"--plug={node}",
ce1222
+                ],
ce1222
+                stdout=stdout,
ce1222
+                stderr=stderr,
ce1222
+                returncode=return_code,
ce1222
+            ),
ce1222
+        )
ce1222
+
ce1222
+    def get_status(
ce1222
+        self,
ce1222
+        node,
ce1222
+        device,
ce1222
+        stdout="",
ce1222
+        stderr="",
ce1222
+        return_code=0,
ce1222
+        name="runner.scsi.is_fenced",
ce1222
+    ):
ce1222
+        """
ce1222
+        Create a call for getting scsi status
ce1222
+
ce1222
+        string node -- a node from which is unfencing performed
ce1222
+        str device -- a device to check
ce1222
+        string stdout -- stdout from fence_scsi agent script
ce1222
+        string stderr -- stderr from fence_scsi agent script
ce1222
+        int return_code -- return code of the fence_scsi agent script
ce1222
+        string name -- the key of this call
ce1222
+        """
ce1222
+        self.__calls.place(
ce1222
+            name,
ce1222
+            RunnerCall(
ce1222
+                [
ce1222
+                    os.path.join(settings.fence_agent_binaries, "fence_scsi"),
ce1222
+                    "--action=status",
ce1222
+                    f"--devices={device}",
ce1222
                     f"--plug={node}",
ce1222
                 ],
ce1222
                 stdout=stdout,
ce1222
diff --git a/pcsd/api_v1.rb b/pcsd/api_v1.rb
ce1222
index 7edeeabf..e55c2be7 100644
ce1222
--- a/pcsd/api_v1.rb
ce1222
+++ b/pcsd/api_v1.rb
ce1222
@@ -291,7 +291,7 @@ def route_api_v1(auth_user, params, request)
ce1222
       :only_superuser => false,
ce1222
       :permissions => Permissions::WRITE,
ce1222
     },
ce1222
-    'scsi-unfence-node/v1' => {
ce1222
+    'scsi-unfence-node/v2' => {
ce1222
       :cmd => 'scsi.unfence_node',
ce1222
       :only_superuser => false,
ce1222
       :permissions => Permissions::WRITE,
ce1222
diff --git a/pcsd/capabilities.xml b/pcsd/capabilities.xml
ce1222
index 58ebcf0f..3954aa5d 100644
ce1222
--- a/pcsd/capabilities.xml
ce1222
+++ b/pcsd/capabilities.xml
ce1222
@@ -1892,11 +1892,13 @@
ce1222
         pcs commands: stonith update-scsi-devices
ce1222
       </description>
ce1222
     </capability>
ce1222
-    <capability id="pcmk.stonith.scsi-unfence-node" in-pcs="0" in-pcsd="1">
ce1222
+    <capability id="pcmk.stonith.scsi-unfence-node-v2" in-pcs="0" in-pcsd="1">
ce1222
       <description>
ce1222
-        Unfence scsi devices on a cluster node.
ce1222
+        Unfence scsi devices on a cluster node. In comparison with v1, only
ce1222
+        newly added devices are unfenced. In case any existing device is
ce1222
+        fenced, unfencing will be skipped.
ce1222
 
ce1222
-        daemon urls: /api/v1/scsi-unfence-node/v1
ce1222
+        daemon urls: /api/v1/scsi-unfence-node/v2
ce1222
       </description>
ce1222
     </capability>
ce1222
     <capability id="pcmk.stonith.enable-disable" in-pcs="1" in-pcsd="1">
ce1222
-- 
ce1222
2.31.1
ce1222