pcs/SOURCES/bz1992668-01-add-add-remove...

3630 lines
131 KiB
Diff

From d20c356298eacec1a71a85c29f7d1f8b63fd8cb7 Mon Sep 17 00:00:00 2001
From: Miroslav Lisik <mlisik@redhat.com>
Date: Fri, 6 Aug 2021 17:35:03 +0200
Subject: [PATCH 1/2] add add/remove syntax for command `pcs stonith
update-scsi-devices`
---
CHANGELOG.md | 9 -
pcs/cli/common/lib_wrapper.py | 1 +
pcs/common/reports/codes.py | 39 +
pcs/common/reports/const.py | 4 +
pcs/common/reports/messages.py | 289 ++++
pcs/common/reports/types.py | 2 +
pcs/common/str_tools.py | 26 +-
pcs/lib/commands/stonith.py | 307 +++-
pcs/pcs.8.in | 4 +-
pcs/stonith.py | 43 +-
pcs/usage.py | 13 +-
pcs_test/Makefile.am | 1 +
pcs_test/tier0/cli/test_stonith.py | 169 +-
.../tier0/common/reports/test_messages.py | 185 +++
pcs_test/tier0/common/test_str_tools.py | 63 +-
pcs_test/tier0/lib/cib/test_stonith.py | 135 +-
.../test_stonith_update_scsi_devices.py | 1439 ++++++++++-------
pcsd/capabilities.xml | 8 +
18 files changed, 2041 insertions(+), 696 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c15546ba..f768cc36 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,5 @@
# Change Log
-## [Unreleased]
-
-### Fixed
-- Fixed an error when creating a resource which defines 'depth' attribute for
- its operations ([rhbz#1998454])
-
-[rhbz#1998454]: https://bugzilla.redhat.com/show_bug.cgi?id=1998454
-
-
## [0.10.10] - 2021-08-19
### Added
diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py
index 06410b41..2bf83485 100644
--- a/pcs/cli/common/lib_wrapper.py
+++ b/pcs/cli/common/lib_wrapper.py
@@ -436,6 +436,7 @@ def load_module(env, middleware_factory, name):
"history_cleanup": stonith.history_cleanup,
"history_update": stonith.history_update,
"update_scsi_devices": stonith.update_scsi_devices,
+ "update_scsi_devices_add_remove": stonith.update_scsi_devices_add_remove,
},
)
diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py
index 32898154..bbd61500 100644
--- a/pcs/common/reports/codes.py
+++ b/pcs/common/reports/codes.py
@@ -12,6 +12,29 @@ SKIP_OFFLINE_NODES = F("SKIP_OFFLINE_NODES")
# messages
+ADD_REMOVE_ITEMS_NOT_SPECIFIED = M("ADD_REMOVE_ITEMS_NOT_SPECIFIED")
+ADD_REMOVE_ITEMS_DUPLICATION = M("ADD_REMOVE_ITEMS_DUPLICATION")
+ADD_REMOVE_CANNOT_ADD_ITEMS_ALREADY_IN_THE_CONTAINER = M(
+ "ADD_REMOVE_CANNOT_ADD_ITEMS_ALREADY_IN_THE_CONTAINER"
+)
+ADD_REMOVE_CANNOT_REMOVE_ITEMS_NOT_IN_THE_CONTAINER = M(
+ "ADD_REMOVE_CANNOT_REMOVE_ITEMS_NOT_IN_THE_CONTAINER"
+)
+ADD_REMOVE_CANNOT_ADD_AND_REMOVE_ITEMS_AT_THE_SAME_TIME = M(
+ "ADD_REMOVE_CANNOT_ADD_AND_REMOVE_ITEMS_AT_THE_SAME_TIME"
+)
+ADD_REMOVE_CANNOT_REMOVE_ALL_ITEMS_FROM_THE_CONTAINER = M(
+ "ADD_REMOVE_CANNOT_REMOVE_ALL_ITEMS_FROM_THE_CONTAINER"
+)
+ADD_REMOVE_ADJACENT_ITEM_NOT_IN_THE_CONTAINER = M(
+ "ADD_REMOVE_ADJACENT_ITEM_NOT_IN_THE_CONTAINER"
+)
+ADD_REMOVE_CANNOT_PUT_ITEM_NEXT_TO_ITSELF = M(
+ "ADD_REMOVE_CANNOT_PUT_ITEM_NEXT_TO_ITSELF"
+)
+ADD_REMOVE_CANNOT_SPECIFY_ADJACENT_ITEM_WITHOUT_ITEMS_TO_ADD = M(
+ "ADD_REMOVE_CANNOT_SPECIFY_ADJACENT_ITEM_WITHOUT_ITEMS_TO_ADD"
+)
AGENT_NAME_GUESS_FOUND_MORE_THAN_ONE = M("AGENT_NAME_GUESS_FOUND_MORE_THAN_ONE")
AGENT_NAME_GUESS_FOUND_NONE = M("AGENT_NAME_GUESS_FOUND_NONE")
AGENT_NAME_GUESSED = M("AGENT_NAME_GUESSED")
@@ -44,17 +67,23 @@ CANNOT_BAN_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE = M(
CANNOT_BAN_RESOURCE_STOPPED_NO_NODE_SPECIFIED = M(
"CANNOT_BAN_RESOURCE_STOPPED_NO_NODE_SPECIFIED"
)
+# TODO: remove, use ADD_REMOVE reports
CANNOT_GROUP_RESOURCE_ADJACENT_RESOURCE_FOR_NEW_GROUP = M(
"CANNOT_GROUP_RESOURCE_ADJACENT_RESOURCE_FOR_NEW_GROUP"
)
+# TODO: remove, use ADD_REMOVE reports
CANNOT_GROUP_RESOURCE_ADJACENT_RESOURCE_NOT_IN_GROUP = M(
"CANNOT_GROUP_RESOURCE_ADJACENT_RESOURCE_NOT_IN_GROUP"
)
+# TODO: remove, use ADD_REMOVE reports
CANNOT_GROUP_RESOURCE_ALREADY_IN_THE_GROUP = M(
"CANNOT_GROUP_RESOURCE_ALREADY_IN_THE_GROUP"
)
+# TODO: remove, use ADD_REMOVE reports
CANNOT_GROUP_RESOURCE_MORE_THAN_ONCE = M("CANNOT_GROUP_RESOURCE_MORE_THAN_ONCE")
+# TODO: remove, use ADD_REMOVE reports
CANNOT_GROUP_RESOURCE_NEXT_TO_ITSELF = M("CANNOT_GROUP_RESOURCE_NEXT_TO_ITSELF")
+# TODO: remove, use ADD_REMOVE reports
CANNOT_GROUP_RESOURCE_NO_RESOURCES = M("CANNOT_GROUP_RESOURCE_NO_RESOURCES")
CANNOT_GROUP_RESOURCE_WRONG_TYPE = M("CANNOT_GROUP_RESOURCE_WRONG_TYPE")
CANNOT_LEAVE_GROUP_EMPTY_AFTER_MOVE = M("CANNOT_LEAVE_GROUP_EMPTY_AFTER_MOVE")
@@ -448,13 +477,17 @@ SERVICE_COMMAND_ON_NODE_ERROR = M("SERVICE_COMMAND_ON_NODE_ERROR")
SERVICE_COMMAND_ON_NODE_SUCCESS = M("SERVICE_COMMAND_ON_NODE_SUCCESS")
SERVICE_UNABLE_TO_DETECT_INIT_SYSTEM = M("SERVICE_UNABLE_TO_DETECT_INIT_SYSTEM")
SYSTEM_WILL_RESET = M("SYSTEM_WILL_RESET")
+# TODO: remove, use ADD_REMOVE reports
TAG_ADD_REMOVE_IDS_DUPLICATION = M("TAG_ADD_REMOVE_IDS_DUPLICATION")
+# TODO: remove, use ADD_REMOVE reports
TAG_ADJACENT_REFERENCE_ID_NOT_IN_THE_TAG = M(
"TAG_ADJACENT_REFERENCE_ID_NOT_IN_THE_TAG"
)
+# TODO: remove, use ADD_REMOVE reports
TAG_CANNOT_ADD_AND_REMOVE_IDS_AT_THE_SAME_TIME = M(
"TAG_CANNOT_ADD_AND_REMOVE_IDS_AT_THE_SAME_TIME"
)
+# TODO: remove, use ADD_REMOVE reports
TAG_CANNOT_ADD_REFERENCE_IDS_ALREADY_IN_THE_TAG = M(
"TAG_CANNOT_ADD_REFERENCE_IDS_ALREADY_IN_THE_TAG"
)
@@ -462,8 +495,11 @@ TAG_CANNOT_CONTAIN_ITSELF = M("TAG_CANNOT_CONTAIN_ITSELF")
TAG_CANNOT_CREATE_EMPTY_TAG_NO_IDS_SPECIFIED = M(
"TAG_CANNOT_CREATE_EMPTY_TAG_NO_IDS_SPECIFIED"
)
+# TODO: remove, use ADD_REMOVE reports
TAG_CANNOT_PUT_ID_NEXT_TO_ITSELF = M("TAG_CANNOT_PUT_ID_NEXT_TO_ITSELF")
+# TODO: remove, use ADD_REMOVE reports
TAG_CANNOT_REMOVE_ADJACENT_ID = M("TAG_CANNOT_REMOVE_ADJACENT_ID")
+# TODO: remove, use ADD_REMOVE reports
TAG_CANNOT_REMOVE_REFERENCES_WITHOUT_REMOVING_TAG = M(
"TAG_CANNOT_REMOVE_REFERENCES_WITHOUT_REMOVING_TAG"
)
@@ -473,12 +509,15 @@ TAG_CANNOT_REMOVE_TAG_REFERENCED_IN_CONSTRAINTS = M(
TAG_CANNOT_REMOVE_TAGS_NO_TAGS_SPECIFIED = M(
"TAG_CANNOT_REMOVE_TAGS_NO_TAGS_SPECIFIED"
)
+# TODO: remove, use ADD_REMOVE reports
TAG_CANNOT_SPECIFY_ADJACENT_ID_WITHOUT_IDS_TO_ADD = M(
"TAG_CANNOT_SPECIFY_ADJACENT_ID_WITHOUT_IDS_TO_ADD"
)
+# TODO: remove, use ADD_REMOVE reports
TAG_CANNOT_UPDATE_TAG_NO_IDS_SPECIFIED = M(
"TAG_CANNOT_UPDATE_TAG_NO_IDS_SPECIFIED"
)
+# TODO: remove, use ADD_REMOVE reports
TAG_IDS_NOT_IN_THE_TAG = M("TAG_IDS_NOT_IN_THE_TAG")
TMP_FILE_WRITE = M("TMP_FILE_WRITE")
UNABLE_TO_CONNECT_TO_ANY_REMAINING_NODE = M(
diff --git a/pcs/common/reports/const.py b/pcs/common/reports/const.py
index c551338e..88725eb3 100644
--- a/pcs/common/reports/const.py
+++ b/pcs/common/reports/const.py
@@ -1,4 +1,6 @@
from .types import (
+ AddRemoveContainerType,
+ AddRemoveItemType,
BoothConfigUsedWhere,
DefaultAddressSource,
FenceHistoryCommandType,
@@ -9,6 +11,8 @@ from .types import (
)
+ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE = AddRemoveContainerType("stonith")
+ADD_REMOVE_ITEM_TYPE_DEVICE = AddRemoveItemType("device")
BOOTH_CONFIG_USED_IN_CLUSTER_RESOURCE = BoothConfigUsedWhere(
"in a cluster resource"
)
diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py
index a1c5db11..f9688437 100644
--- a/pcs/common/reports/messages.py
+++ b/pcs/common/reports/messages.py
@@ -24,6 +24,7 @@ from pcs.common.str_tools import (
format_list_custom_last_separator,
format_optional,
format_plural,
+ get_plural,
indent,
is_iterable_not_str,
)
@@ -95,6 +96,14 @@ def _key_numeric(item: str) -> Tuple[int, str]:
return (int(item), item) if item.isdigit() else (-1, item)
+_add_remove_container_translation = {
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE: "stonith resource",
+}
+
+_add_remove_item_translation = {
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE: "device",
+}
+
_file_role_translation = {
file_type_codes.BOOTH_CONFIG: "Booth configuration",
file_type_codes.BOOTH_KEY: "Booth key",
@@ -129,6 +138,16 @@ _type_articles = {
}
+def _add_remove_container_str(
+ container: types.AddRemoveContainerType,
+) -> str:
+ return _add_remove_container_translation.get(container, container)
+
+
+def _add_remove_item_str(item: types.AddRemoveItemType) -> str:
+ return _add_remove_item_translation.get(item, item)
+
+
def _format_file_role(role: file_type_codes.FileTypeCode) -> str:
return _file_role_translation.get(role, role)
@@ -2528,6 +2547,7 @@ class ResourceBundleAlreadyContainsAResource(ReportItemMessage):
)
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class CannotGroupResourceAdjacentResourceForNewGroup(ReportItemMessage):
"""
@@ -2551,6 +2571,7 @@ class CannotGroupResourceAdjacentResourceForNewGroup(ReportItemMessage):
)
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class CannotGroupResourceAdjacentResourceNotInGroup(ReportItemMessage):
"""
@@ -2573,6 +2594,7 @@ class CannotGroupResourceAdjacentResourceNotInGroup(ReportItemMessage):
)
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class CannotGroupResourceAlreadyInTheGroup(ReportItemMessage):
"""
@@ -2593,6 +2615,7 @@ class CannotGroupResourceAlreadyInTheGroup(ReportItemMessage):
return f"{resources} already {exist} in '{self.group_id}'"
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class CannotGroupResourceMoreThanOnce(ReportItemMessage):
"""
@@ -2610,6 +2633,7 @@ class CannotGroupResourceMoreThanOnce(ReportItemMessage):
return f"Resources specified more than once: {resources}"
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class CannotGroupResourceNoResources(ReportItemMessage):
"""
@@ -2623,6 +2647,7 @@ class CannotGroupResourceNoResources(ReportItemMessage):
return "No resources to add"
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class CannotGroupResourceNextToItself(ReportItemMessage):
"""
@@ -6482,6 +6507,7 @@ class BoothTicketOperationFailed(ReportItemMessage):
)
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagAddRemoveIdsDuplication(ReportItemMessage):
"""
@@ -6500,6 +6526,7 @@ class TagAddRemoveIdsDuplication(ReportItemMessage):
return f"Ids to {action} must be unique, duplicate ids: {duplicate_ids}"
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagAdjacentReferenceIdNotInTheTag(ReportItemMessage):
"""
@@ -6522,6 +6549,7 @@ class TagAdjacentReferenceIdNotInTheTag(ReportItemMessage):
)
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagCannotAddAndRemoveIdsAtTheSameTime(ReportItemMessage):
"""
@@ -6540,6 +6568,7 @@ class TagCannotAddAndRemoveIdsAtTheSameTime(ReportItemMessage):
return f"Ids cannot be added and removed at the same time: {idref_list}"
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagCannotAddReferenceIdsAlreadyInTheTag(ReportItemMessage):
"""
@@ -6591,6 +6620,7 @@ class TagCannotCreateEmptyTagNoIdsSpecified(ReportItemMessage):
return "Cannot create empty tag, no resource ids specified"
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagCannotPutIdNextToItself(ReportItemMessage):
"""
@@ -6607,6 +6637,7 @@ class TagCannotPutIdNextToItself(ReportItemMessage):
return f"Cannot put id '{self.adjacent_id}' next to itself."
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagCannotRemoveAdjacentId(ReportItemMessage):
"""
@@ -6626,6 +6657,7 @@ class TagCannotRemoveAdjacentId(ReportItemMessage):
)
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagCannotRemoveReferencesWithoutRemovingTag(ReportItemMessage):
"""
@@ -6678,6 +6710,7 @@ class TagCannotRemoveTagsNoTagsSpecified(ReportItemMessage):
return "Cannot remove tags, no tags to remove specified"
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagCannotSpecifyAdjacentIdWithoutIdsToAdd(ReportItemMessage):
"""
@@ -6697,6 +6730,7 @@ class TagCannotSpecifyAdjacentIdWithoutIdsToAdd(ReportItemMessage):
)
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagCannotUpdateTagNoIdsSpecified(ReportItemMessage):
"""
@@ -6710,6 +6744,7 @@ class TagCannotUpdateTagNoIdsSpecified(ReportItemMessage):
return "Cannot update tag, no ids to be added or removed specified"
+# TODO: remove, use ADD_REMOVE reports
@dataclass(frozen=True)
class TagIdsNotInTheTag(ReportItemMessage):
"""
@@ -6850,3 +6885,257 @@ class CibNvsetAmbiguousProvideNvsetId(ReportItemMessage):
@property
def message(self) -> str:
return "Several options sets exist, please specify an option set ID"
+
+
+@dataclass(frozen=True)
+class AddRemoveItemsNotSpecified(ReportItemMessage):
+ """
+ Cannot modify container, no add or remove items specified.
+
+ container_type -- type of item container
+ item_type -- type of item in a container
+ container_id -- id of a container
+ """
+
+ container_type: types.AddRemoveContainerType
+ item_type: types.AddRemoveItemType
+ container_id: str
+ _code = codes.ADD_REMOVE_ITEMS_NOT_SPECIFIED
+
+ @property
+ def message(self) -> str:
+ container = _add_remove_container_str(self.container_type)
+ items = get_plural(_add_remove_item_str(self.item_type))
+ return (
+ f"Cannot modify {container} '{self.container_id}', no {items} to "
+ "add or remove specified"
+ )
+
+
+@dataclass(frozen=True)
+class AddRemoveItemsDuplication(ReportItemMessage):
+ """
+ Duplicate items were found in add/remove item lists.
+
+ container_type -- type of item container
+ item_type -- type of item in a container
+ container_id -- id of a container
+ duplicate_items_list -- list of duplicate items
+ """
+
+ container_type: types.AddRemoveContainerType
+ item_type: types.AddRemoveItemType
+ container_id: str
+ duplicate_items_list: List[str]
+ _code = codes.ADD_REMOVE_ITEMS_DUPLICATION
+
+ @property
+ def message(self) -> str:
+ items = get_plural(_add_remove_item_str(self.item_type))
+ duplicate_items = format_list(self.duplicate_items_list)
+ return (
+ f"{items.capitalize()} to add or remove must be unique, duplicate "
+ f"{items}: {duplicate_items}"
+ )
+
+
+@dataclass(frozen=True)
+class AddRemoveCannotAddItemsAlreadyInTheContainer(ReportItemMessage):
+ """
+ Cannot add items already existing in the container.
+
+ container_type -- type of item container
+ item_type -- type of item in a container
+ container_id -- id of a container
+ item_list -- list of items already in the container
+ """
+
+ container_type: types.AddRemoveContainerType
+ item_type: types.AddRemoveItemType
+ container_id: str
+ item_list: List[str]
+ _code = codes.ADD_REMOVE_CANNOT_ADD_ITEMS_ALREADY_IN_THE_CONTAINER
+
+ @property
+ def message(self) -> str:
+ items = format_plural(
+ self.item_list, _add_remove_item_str(self.item_type)
+ )
+ item_list = format_list(self.item_list)
+ they = format_plural(self.item_list, "it")
+ are = format_plural(self.item_list, "is")
+ container = _add_remove_container_str(self.container_type)
+ return (
+ f"Cannot add {items} {item_list}, {they} {are} already present in "
+ f"{container} '{self.container_id}'"
+ )
+
+
+@dataclass(frozen=True)
+class AddRemoveCannotRemoveItemsNotInTheContainer(ReportItemMessage):
+ """
+ Cannot remove items not existing in the container.
+
+ container_type -- type of item container
+ item_type -- type of item in a container
+ container_id -- id of a container
+ item_list -- list of items not in the container
+ """
+
+ container_type: types.AddRemoveContainerType
+ item_type: types.AddRemoveItemType
+ container_id: str
+ item_list: List[str]
+ _code = codes.ADD_REMOVE_CANNOT_REMOVE_ITEMS_NOT_IN_THE_CONTAINER
+
+ @property
+ def message(self) -> str:
+ items = format_plural(
+ self.item_list, _add_remove_item_str(self.item_type)
+ )
+ item_list = format_list(self.item_list)
+ they = format_plural(self.item_list, "it")
+ are = format_plural(self.item_list, "is")
+ container = _add_remove_container_str(self.container_type)
+ items = format_plural(
+ self.item_list, _add_remove_item_str(self.item_type)
+ )
+ return (
+ f"Cannot remove {items} {item_list}, {they} {are} not present in "
+ f"{container} '{self.container_id}'"
+ )
+
+
+@dataclass(frozen=True)
+class AddRemoveCannotAddAndRemoveItemsAtTheSameTime(ReportItemMessage):
+ """
+ Cannot add and remove items at the same time. Avoid operation without an
+ effect.
+
+ container_type -- type of item container
+ item_type -- type of item in a container
+ container_id -- id of a container
+ item_list -- common items from add and remove item lists
+ """
+
+ container_type: types.AddRemoveContainerType
+ item_type: types.AddRemoveItemType
+ container_id: str
+ item_list: List[str]
+ _code = codes.ADD_REMOVE_CANNOT_ADD_AND_REMOVE_ITEMS_AT_THE_SAME_TIME
+
+ @property
+ def message(self) -> str:
+ items = format_plural(
+ self.item_list, _add_remove_item_str(self.item_type)
+ )
+ item_list = format_list(self.item_list)
+ return (
+ f"{items.capitalize()} cannot be added and removed at the same "
+ f"time: {item_list}"
+ )
+
+
+@dataclass(frozen=True)
+class AddRemoveCannotRemoveAllItemsFromTheContainer(ReportItemMessage):
+ """
+ Cannot remove all items from a container.
+
+ container_type -- type of item container
+ item_type -- type of item in a container
+ container_id -- id of a container
+ item_list -- common items from add and remove item lists
+ """
+
+ container_type: types.AddRemoveContainerType
+ item_type: types.AddRemoveItemType
+ container_id: str
+ item_list: List[str]
+ _code = codes.ADD_REMOVE_CANNOT_REMOVE_ALL_ITEMS_FROM_THE_CONTAINER
+
+ @property
+ def message(self) -> str:
+ container = _add_remove_container_str(self.container_type)
+ items = get_plural(_add_remove_item_str(self.item_type))
+ return (
+ f"Cannot remove all {items} from {container} '{self.container_id}'"
+ )
+
+
+@dataclass(frozen=True)
+class AddRemoveAdjacentItemNotInTheContainer(ReportItemMessage):
+ """
+ Cannot put items next to an adjacent item in the container, because the
+ adjacent item does not exist in the container.
+
+ container_type -- type of item container
+ item_type -- type of item in a container
+ container_id -- id of a container
+ adjacent_item_id -- id of an adjacent item
+ """
+
+ container_type: types.AddRemoveContainerType
+ item_type: types.AddRemoveItemType
+ container_id: str
+ adjacent_item_id: str
+ _code = codes.ADD_REMOVE_ADJACENT_ITEM_NOT_IN_THE_CONTAINER
+
+ @property
+ def message(self) -> str:
+ container = _add_remove_container_str(self.container_type)
+ item = _add_remove_item_str(self.item_type)
+ items = get_plural(item)
+ return (
+ f"There is no {item} '{self.adjacent_item_id}' in the "
+ f"{container} '{self.container_id}', cannot add {items} next to it"
+ )
+
+
+@dataclass(frozen=True)
+class AddRemoveCannotPutItemNextToItself(ReportItemMessage):
+ """
+ Cannot put an item into a container next to itself.
+
+ container_type -- type of item container
+ item_type -- type of item in a container
+ container_id -- id of a container
+ adjacent_item_id -- id of an adjacent item
+ """
+
+ container_type: types.AddRemoveContainerType
+ item_type: types.AddRemoveItemType
+ container_id: str
+ adjacent_item_id: str
+ _code = codes.ADD_REMOVE_CANNOT_PUT_ITEM_NEXT_TO_ITSELF
+
+ @property
+ def message(self) -> str:
+ item = _add_remove_item_str(self.item_type)
+ return f"Cannot put {item} '{self.adjacent_item_id}' next to itself"
+
+
+@dataclass(frozen=True)
+class AddRemoveCannotSpecifyAdjacentItemWithoutItemsToAdd(ReportItemMessage):
+ """
+ Cannot specify adjacent item without items to add.
+
+ container_type -- type of item container
+ item_type -- type of item in a container
+ container_id -- id of a container
+ adjacent_item_id -- id of an adjacent item
+ """
+
+ container_type: types.AddRemoveContainerType
+ item_type: types.AddRemoveItemType
+ container_id: str
+ adjacent_item_id: str
+ _code = codes.ADD_REMOVE_CANNOT_SPECIFY_ADJACENT_ITEM_WITHOUT_ITEMS_TO_ADD
+
+ @property
+ def message(self) -> str:
+ item = _add_remove_item_str(self.item_type)
+ items = get_plural(item)
+ return (
+ f"Cannot specify adjacent {item} '{self.adjacent_item_id}' without "
+ f"{items} to add"
+ )
diff --git a/pcs/common/reports/types.py b/pcs/common/reports/types.py
index fa7fdf4d..610c16f1 100644
--- a/pcs/common/reports/types.py
+++ b/pcs/common/reports/types.py
@@ -1,5 +1,7 @@
from typing import NewType
+AddRemoveContainerType = NewType("AddRemoveContainerType", str)
+AddRemoveItemType = NewType("AddRemoveItemType", str)
BoothConfigUsedWhere = NewType("BoothConfigUsedWhere", str)
DefaultAddressSource = NewType("DefaultAddressSource", str)
FenceHistoryCommandType = NewType("FenceHistoryCommandType", str)
diff --git a/pcs/common/str_tools.py b/pcs/common/str_tools.py
index 98fe5f50..b8dccc0c 100644
--- a/pcs/common/str_tools.py
+++ b/pcs/common/str_tools.py
@@ -131,6 +131,23 @@ def _add_s(word):
return word + "s"
+def get_plural(singular: str) -> str:
+ """
+ Take singular word form and return plural.
+
+ singular -- singular word (like: is, do, node)
+ """
+ common_plurals = {
+ "is": "are",
+ "has": "have",
+ "does": "do",
+ "it": "they",
+ }
+ if singular in common_plurals:
+ return common_plurals[singular]
+ return _add_s(singular)
+
+
def format_plural(
depends_on: Union[int, Iterable[Any]],
singular: str,
@@ -145,18 +162,11 @@ def format_plural(
singular -- singular word (like: is, do, node)
plural -- optional irregular plural form
"""
- common_plurals = {
- "is": "are",
- "has": "have",
- "does": "do",
- }
if not _is_multiple(depends_on):
return singular
if plural:
return plural
- if singular in common_plurals:
- return common_plurals[singular]
- return _add_s(singular)
+ return get_plural(singular)
T = TypeVar("T")
diff --git a/pcs/lib/commands/stonith.py b/pcs/lib/commands/stonith.py
index 036e3fa5..6f26e7d3 100644
--- a/pcs/lib/commands/stonith.py
+++ b/pcs/lib/commands/stonith.py
@@ -1,9 +1,15 @@
-from typing import Container, Iterable, Optional
+from collections import Counter
+from typing import Container, Iterable, List, Optional, Set, Tuple
+
+from lxml.etree import _Element
from pcs.common import reports
+from pcs.common.reports import ReportItemList
+from pcs.common.reports import ReportProcessor
from pcs.common.reports.item import ReportItem
from pcs.lib.cib import resource
from pcs.lib.cib import stonith
+from pcs.lib.cib.nvpair import INSTANCE_ATTRIBUTES_TAG, get_value
from pcs.lib.cib.resource.common import are_meta_disabled
from pcs.lib.cib.tools import IdProvider
from pcs.lib.commands.resource import (
@@ -20,6 +26,7 @@ from pcs.lib.communication.tools import (
)
from pcs.lib.env import LibraryEnvironment
from pcs.lib.errors import LibraryError
+from pcs.lib.external import CommandRunner
from pcs.lib.node import get_existing_nodes_names
from pcs.lib.pacemaker.live import (
FenceHistoryCommandErrorException,
@@ -268,55 +275,195 @@ def history_update(env: LibraryEnvironment):
) from e
-def update_scsi_devices(
- env: LibraryEnvironment,
- stonith_id: str,
- set_device_list: Iterable[str],
- force_flags: Container[reports.types.ForceCode] = (),
-) -> None:
+def _validate_add_remove_items(
+ add_item_list: Iterable[str],
+ remove_item_list: Iterable[str],
+ current_item_list: Iterable[str],
+ container_type: reports.types.AddRemoveContainerType,
+ item_type: reports.types.AddRemoveItemType,
+ container_id: str,
+ adjacent_item_id: Optional[str] = None,
+ container_can_be_empty: bool = False,
+) -> ReportItemList:
"""
- Update scsi fencing devices without restart and affecting other resources.
+ Validate if items can be added or removed to or from a container.
- env -- provides all for communication with externals
- stonith_id -- id of stonith resource
- set_device_list -- paths to the scsi devices that would be set for stonith
- resource
- force_flags -- list of flags codes
+ add_item_list -- items to be added
+ remove_item_list -- items to be removed
+ current_item_list -- items currently in the container
+ container_type -- container type
+ item_type -- item type
+ container_id -- id of the container
+ adjacent_item_id -- an adjacent item in the container
+ container_can_be_empty -- flag to decide if container can be left empty
"""
- if not is_getting_resource_digest_supported(env.cmd_runner()):
- raise LibraryError(
+ # pylint: disable=too-many-locals
+ report_list: ReportItemList = []
+ if not add_item_list and not remove_item_list:
+ report_list.append(
ReportItem.error(
- reports.messages.StonithRestartlessUpdateOfScsiDevicesNotSupported()
+ reports.messages.AddRemoveItemsNotSpecified(
+ container_type, item_type, container_id
+ )
)
)
- cib = env.get_cib()
- if not set_device_list:
- env.report_processor.report(
+
+ def _get_duplicate_items(item_list: Iterable[str]) -> Set[str]:
+ return {item for item, count in Counter(item_list).items() if count > 1}
+
+ duplicate_items_list = _get_duplicate_items(
+ add_item_list
+ ) | _get_duplicate_items(remove_item_list)
+ if duplicate_items_list:
+ report_list.append(
ReportItem.error(
- reports.messages.InvalidOptionValue(
- "devices", "", None, cannot_be_empty=True
+ reports.messages.AddRemoveItemsDuplication(
+ container_type,
+ item_type,
+ container_id,
+ sorted(duplicate_items_list),
+ )
+ )
+ )
+ already_present = set(add_item_list).intersection(current_item_list)
+ # report only if an adjacent id is not defined, because we want to allow
+ # to move items when adjacent_item_id is specified
+ if adjacent_item_id is None and already_present:
+ report_list.append(
+ ReportItem.error(
+ reports.messages.AddRemoveCannotAddItemsAlreadyInTheContainer(
+ container_type,
+ item_type,
+ container_id,
+ sorted(already_present),
+ )
+ )
+ )
+ missing_items = set(remove_item_list).difference(current_item_list)
+ if missing_items:
+ report_list.append(
+ ReportItem.error(
+ reports.messages.AddRemoveCannotRemoveItemsNotInTheContainer(
+ container_type,
+ item_type,
+ container_id,
+ sorted(missing_items),
)
)
)
+ common_items = set(add_item_list) & set(remove_item_list)
+ if common_items:
+ report_list.append(
+ ReportItem.error(
+ reports.messages.AddRemoveCannotAddAndRemoveItemsAtTheSameTime(
+ container_type,
+ item_type,
+ container_id,
+ sorted(common_items),
+ )
+ )
+ )
+ if not container_can_be_empty and not add_item_list:
+ remaining_items = set(current_item_list).difference(remove_item_list)
+ if not remaining_items:
+ report_list.append(
+ ReportItem.error(
+ reports.messages.AddRemoveCannotRemoveAllItemsFromTheContainer(
+ container_type,
+ item_type,
+ container_id,
+ list(current_item_list),
+ )
+ )
+ )
+ if adjacent_item_id:
+ if adjacent_item_id not in current_item_list:
+ report_list.append(
+ ReportItem.error(
+ reports.messages.AddRemoveAdjacentItemNotInTheContainer(
+ container_type,
+ item_type,
+ container_id,
+ adjacent_item_id,
+ )
+ )
+ )
+ if adjacent_item_id in add_item_list:
+ report_list.append(
+ ReportItem.error(
+ reports.messages.AddRemoveCannotPutItemNextToItself(
+ container_type,
+ item_type,
+ container_id,
+ adjacent_item_id,
+ )
+ )
+ )
+ if not add_item_list:
+ report_list.append(
+ ReportItem.error(
+ reports.messages.AddRemoveCannotSpecifyAdjacentItemWithoutItemsToAdd(
+ container_type,
+ item_type,
+ container_id,
+ adjacent_item_id,
+ )
+ )
+ )
+ return report_list
+
+
+def _update_scsi_devices_get_element_and_devices(
+ runner: CommandRunner,
+ report_processor: ReportProcessor,
+ cib: _Element,
+ stonith_id: str,
+) -> Tuple[_Element, List[str]]:
+ """
+ Do checks and return stonith element and list of current scsi devices.
+ Raise LibraryError if checks fail.
+
+ runner -- command runner instance
+ report_processor -- tool for warning/info/error reporting
+ cib -- cib element
+ stonith_id -- id of stonith resource
+ """
+ if not is_getting_resource_digest_supported(runner):
+ raise LibraryError(
+ ReportItem.error(
+ reports.messages.StonithRestartlessUpdateOfScsiDevicesNotSupported()
+ )
+ )
(
stonith_el,
report_list,
) = stonith.validate_stonith_restartless_update(cib, stonith_id)
- if env.report_processor.report_list(report_list).has_errors:
+ if report_processor.report_list(report_list).has_errors:
raise LibraryError()
- # for mypy, this should not happen because exeption would be raised
+ # for mypy, this should not happen because exception would be raised
if stonith_el is None:
raise AssertionError("stonith element is None")
-
- stonith.update_scsi_devices_without_restart(
- env.cmd_runner(),
- env.get_cluster_state(),
- stonith_el,
- IdProvider(cib),
- set_device_list,
+ current_device_list = get_value(
+ INSTANCE_ATTRIBUTES_TAG, stonith_el, "devices"
)
+ if current_device_list is None:
+ raise AssertionError("current_device_list is None")
+ return stonith_el, current_device_list.split(",")
+
+
+def _unfencing_scsi_devices(
+ env: LibraryEnvironment,
+ device_list: Iterable[str],
+ force_flags: Container[reports.types.ForceCode] = (),
+) -> None:
+ """
+ Unfence scsi devices provided in device_list if it is possible to connect
+ to pcsd and corosync is running.
- # Unfencing
+ env -- provides all for communication with externals
+ device_list -- devices to be unfenced
+ force_flags -- list of flags codes
+ """
cluster_nodes_names, nodes_report_list = get_existing_nodes_names(
env.get_corosync_conf(),
error_on_missing_name=True,
@@ -340,8 +487,104 @@ def update_scsi_devices(
online_corosync_target_list = run_and_raise(
env.get_node_communicator(), com_cmd
)
- com_cmd = Unfence(env.report_processor, sorted(set_device_list))
+ com_cmd = Unfence(env.report_processor, sorted(device_list))
com_cmd.set_targets(online_corosync_target_list)
run_and_raise(env.get_node_communicator(), com_cmd)
+
+def update_scsi_devices(
+ env: LibraryEnvironment,
+ stonith_id: str,
+ set_device_list: Iterable[str],
+ force_flags: Container[reports.types.ForceCode] = (),
+) -> None:
+ """
+ Update scsi fencing devices without restart and affecting other resources.
+
+ env -- provides all for communication with externals
+ stonith_id -- id of stonith resource
+ set_device_list -- paths to the scsi devices that would be set for stonith
+ resource
+ force_flags -- list of flags codes
+ """
+ if not set_device_list:
+ env.report_processor.report(
+ ReportItem.error(
+ reports.messages.InvalidOptionValue(
+ "devices", "", None, cannot_be_empty=True
+ )
+ )
+ )
+ runner = env.cmd_runner()
+ (
+ stonith_el,
+ current_device_list,
+ ) = _update_scsi_devices_get_element_and_devices(
+ runner, env.report_processor, env.get_cib(), stonith_id
+ )
+ if env.report_processor.has_errors:
+ raise LibraryError()
+ stonith.update_scsi_devices_without_restart(
+ runner,
+ env.get_cluster_state(),
+ stonith_el,
+ IdProvider(stonith_el),
+ set_device_list,
+ )
+ devices_for_unfencing = set(set_device_list).difference(current_device_list)
+ if devices_for_unfencing:
+ _unfencing_scsi_devices(env, devices_for_unfencing, force_flags)
+ env.push_cib()
+
+
+def update_scsi_devices_add_remove(
+ env: LibraryEnvironment,
+ stonith_id: str,
+ add_device_list: Iterable[str],
+ remove_device_list: Iterable[str],
+ force_flags: Container[reports.types.ForceCode] = (),
+) -> None:
+ """
+ Update scsi fencing devices without restart and affecting other resources.
+
+ env -- provides all for communication with externals
+ stonith_id -- id of stonith resource
+ add_device_list -- paths to the scsi devices that would be added to the
+ stonith resource
+ remove_device_list -- paths to the scsi devices that would be removed from
+ the stonith resource
+ force_flags -- list of flags codes
+ """
+ runner = env.cmd_runner()
+ (
+ stonith_el,
+ current_device_list,
+ ) = _update_scsi_devices_get_element_and_devices(
+ runner, env.report_processor, env.get_cib(), stonith_id
+ )
+ if env.report_processor.report_list(
+ _validate_add_remove_items(
+ add_device_list,
+ remove_device_list,
+ current_device_list,
+ reports.const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ reports.const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ stonith_el.get("id", ""),
+ )
+ ).has_errors:
+ raise LibraryError()
+ updated_device_set = (
+ set(current_device_list)
+ .union(add_device_list)
+ .difference(remove_device_list)
+ )
+ stonith.update_scsi_devices_without_restart(
+ env.cmd_runner(),
+ env.get_cluster_state(),
+ stonith_el,
+ IdProvider(stonith_el),
+ updated_device_set,
+ )
+ if add_device_list:
+ _unfencing_scsi_devices(env, add_device_list, force_flags)
env.push_cib()
diff --git a/pcs/pcs.8.in b/pcs/pcs.8.in
index ac093d69..1695d75c 100644
--- a/pcs/pcs.8.in
+++ b/pcs/pcs.8.in
@@ -664,8 +664,8 @@ pcs stonith create MyFence fence_virt 'pcmk_host_map=n1:p1;n2:p2,p3'
update <stonith id> [stonith device options]
Add/Change options to specified stonith id.
.TP
-update\-scsi\-devices <stonith id> set <device\-path> [<device\-path>...]
-Update scsi fencing devices without affecting other resources. Stonith resource must be running on one cluster node. Each device will be unfenced on each cluster node running cluster. Supported fence agents: fence_scsi.
+update\-scsi\-devices <stonith id> (set <device\-path> [<device\-path>...]) | (add <device\-path> [<device\-path>...] delete|remove <device\-path> [<device\-path>...] )
+Update scsi fencing devices without affecting other resources. You must specify either list of set devices or at least one device for add or delete/remove devices. Stonith resource must be running on one cluster node. Each device will be unfenced on each cluster node running cluster. Supported fence agents: fence_scsi.
.TP
delete <stonith id>
Remove stonith id from configuration.
diff --git a/pcs/stonith.py b/pcs/stonith.py
index c7eb14de..6ed8b751 100644
--- a/pcs/stonith.py
+++ b/pcs/stonith.py
@@ -894,24 +894,43 @@ def stonith_update_scsi_devices(lib, argv, modifiers):
* --skip-offline - skip unreachable nodes
"""
modifiers.ensure_only_supported("--request-timeout", "--skip-offline")
+ force_flags = []
+ if modifiers.get("--skip-offline"):
+ force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
+
if len(argv) < 2:
raise CmdLineInputError()
stonith_id = argv[0]
parsed_args = parse_args.group_by_keywords(
argv[1:],
- ["set"],
+ ["set", "add", "remove", "delete"],
keyword_repeat_allowed=False,
only_found_keywords=True,
)
- set_args = parsed_args["set"] if "set" in parsed_args else []
- if not set_args:
- raise CmdLineInputError(
- show_both_usage_and_message=True,
- hint="You must specify set devices to be updated",
- )
- force_flags = []
- if modifiers.get("--skip-offline"):
- force_flags.append(reports.codes.SKIP_OFFLINE_NODES)
- lib.stonith.update_scsi_devices(
- stonith_id, set_args, force_flags=force_flags
+ cmd_exception = CmdLineInputError(
+ show_both_usage_and_message=True,
+ hint=(
+ "You must specify either list of set devices or at least one device"
+ " for add or delete/remove devices"
+ ),
)
+ if "set" in parsed_args and {"add", "remove", "delete"} & set(
+ parsed_args.keys()
+ ):
+ raise cmd_exception
+ if "set" in parsed_args:
+ if not parsed_args["set"]:
+ raise cmd_exception
+ lib.stonith.update_scsi_devices(
+ stonith_id, parsed_args["set"], force_flags=force_flags
+ )
+ else:
+ for key in ("add", "remove", "delete"):
+ if key in parsed_args and not parsed_args[key]:
+ raise cmd_exception
+ lib.stonith.update_scsi_devices_add_remove(
+ stonith_id,
+ parsed_args.get("add", []),
+ parsed_args.get("delete", []) + parsed_args.get("remove", []),
+ force_flags=force_flags,
+ )
diff --git a/pcs/usage.py b/pcs/usage.py
index 38e21ed9..66e097f1 100644
--- a/pcs/usage.py
+++ b/pcs/usage.py
@@ -1289,11 +1289,14 @@ Commands:
update <stonith id> [stonith device options]
Add/Change options to specified stonith id.
- update-scsi-devices <stonith id> set <device-path> [<device-path>...]
- Update scsi fencing devices without affecting other resources. Stonith
- resource must be running on one cluster node. Each device will be
- unfenced on each cluster node running cluster. Supported fence agents:
- fence_scsi.
+ update-scsi-devices <stonith id> (set <device-path> [<device-path>...])
+ | (add <device-path> [<device-path>...] delete|remove <device-path>
+ [device-path>...])
+ Update scsi fencing devices without affecting other resources. You must
+ specify either list of set devices or at least one device for add or
+ delete/remove devices. Stonith resource must be running on one cluster
+ node. Each device will be unfenced on each cluster node running
+ cluster. Supported fence agents: fence_scsi.
delete <stonith id>
Remove stonith id from configuration.
diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am
index b4df00e2..c7346f96 100644
--- a/pcs_test/Makefile.am
+++ b/pcs_test/Makefile.am
@@ -236,6 +236,7 @@ EXTRA_DIST = \
tier0/lib/commands/test_stonith_agent.py \
tier0/lib/commands/test_stonith_history.py \
tier0/lib/commands/test_stonith.py \
+ tier0/lib/commands/test_stonith_update_scsi_devices.py \
tier0/lib/commands/test_ticket.py \
tier0/lib/communication/__init__.py \
tier0/lib/communication/test_booth.py \
diff --git a/pcs_test/tier0/cli/test_stonith.py b/pcs_test/tier0/cli/test_stonith.py
index 5bc18f3c..a54b442e 100644
--- a/pcs_test/tier0/cli/test_stonith.py
+++ b/pcs_test/tier0/cli/test_stonith.py
@@ -149,15 +149,41 @@ class SbdDeviceSetup(TestCase):
class StonithUpdateScsiDevices(TestCase):
+ # pylint: disable=too-many-public-methods
def setUp(self):
self.lib = mock.Mock(spec_set=["stonith"])
- self.stonith = mock.Mock(spec_set=["update_scsi_devices"])
+ self.stonith = mock.Mock(
+ spec_set=["update_scsi_devices", "update_scsi_devices_add_remove"]
+ )
self.lib.stonith = self.stonith
def assert_called_with(self, stonith_id, set_devices, force_flags):
self.stonith.update_scsi_devices.assert_called_once_with(
stonith_id, set_devices, force_flags=force_flags
)
+ self.stonith.update_scsi_devices_add_remove.assert_not_called()
+
+ def assert_add_remove_called_with(
+ self, stonith_id, add_devices, remove_devices, force_flags
+ ):
+ self.stonith.update_scsi_devices_add_remove.assert_called_once_with(
+ stonith_id, add_devices, remove_devices, force_flags=force_flags
+ )
+ self.stonith.update_scsi_devices.assert_not_called()
+
+ def assert_bad_syntax_cli_exception(self, args):
+ with self.assertRaises(CmdLineInputError) as cm:
+ self.call_cmd(args)
+ self.assertEqual(cm.exception.message, None)
+ self.assertEqual(
+ cm.exception.hint,
+ (
+ "You must specify either list of set devices or at least one "
+ "device for add or delete/remove devices"
+ ),
+ )
+ self.stonith.update_scsi_devices.assert_not_called()
+ self.stonith.update_scsi_devices_add_remove.assert_not_called()
def call_cmd(self, argv, modifiers=None):
stonith.stonith_update_scsi_devices(
@@ -174,44 +200,141 @@ class StonithUpdateScsiDevices(TestCase):
self.call_cmd(["stonith-id"])
self.assertEqual(cm.exception.message, None)
- def test_not_set_keyword(self):
+ def test_unknown_keyword(self):
with self.assertRaises(CmdLineInputError) as cm:
self.call_cmd(["stonith-id", "unset"])
self.assertEqual(cm.exception.message, None)
- def test_only_set_keyword(self):
- with self.assertRaises(CmdLineInputError) as cm:
- self.call_cmd(["stonith-id", "set"])
- self.assertEqual(cm.exception.message, None)
- self.assertEqual(
- cm.exception.hint, "You must specify set devices to be updated"
- )
-
- def test_one_device(self):
- self.call_cmd(["stonith-id", "set", "device1"])
- self.assert_called_with("stonith-id", ["device1"], [])
-
- def test_more_devices(self):
- self.call_cmd(["stonith-id", "set", "device1", "device2"])
- self.assert_called_with("stonith-id", ["device1", "device2"], [])
-
def test_supported_options(self):
self.call_cmd(
- ["stonith-id", "set", "device1", "device2"],
+ ["stonith-id", "set", "d1", "d2"],
{"skip-offline": True, "request-timeout": 60},
)
self.assert_called_with(
"stonith-id",
- ["device1", "device2"],
+ ["d1", "d2"],
[reports.codes.SKIP_OFFLINE_NODES],
)
def test_unsupported_options(self):
with self.assertRaises(CmdLineInputError) as cm:
- self.call_cmd(
- ["stonith-id", "set", "device1", "device2"], {"force": True}
- )
+ self.call_cmd(["stonith-id", "set", "d1", "d2"], {"force": True})
self.assertEqual(
cm.exception.message,
"Specified option '--force' is not supported in this command",
)
+
+ def test_only_set_keyword(self):
+ self.assert_bad_syntax_cli_exception(["stonith-id", "set"])
+
+ def test_only_add_keyword(self):
+ self.assert_bad_syntax_cli_exception(["stonith-id", "add"])
+
+ def test_only_remove_keyword(self):
+ self.assert_bad_syntax_cli_exception(["stonith-id", "remove"])
+
+ def test_only_delete_keyword(self):
+ self.assert_bad_syntax_cli_exception(["stonith-id", "delete"])
+
+ def test_add_and_empty_remove(self):
+ self.assert_bad_syntax_cli_exception(
+ ["stonith-id", "add", "d1", "remove"]
+ )
+
+ def test_add_and_empty_delete(self):
+ self.assert_bad_syntax_cli_exception(
+ ["stonith-id", "add", "d1", "delete"]
+ )
+
+ def test_empty_add_and_remove(self):
+ self.assert_bad_syntax_cli_exception(
+ ["stonith-id", "add", "remove", "d1"]
+ )
+
+ def test_empty_add_and_delete(self):
+ self.assert_bad_syntax_cli_exception(
+ ["stonith-id", "add", "delete", "d1"]
+ )
+
+ def test_empty_remove_and_delete(self):
+ self.assert_bad_syntax_cli_exception(
+ ["stonith-id", "remove", "delete", "d1"]
+ )
+
+ def test_empty_delete_and_remove(self):
+ self.assert_bad_syntax_cli_exception(
+ ["stonith-id", "delete", "remove", "d1"]
+ )
+
+ def test_empty_add_empty_remove_empty_delete(self):
+ self.assert_bad_syntax_cli_exception(
+ ["stonith-id", "add", "delete", "remove"]
+ )
+
+ def test_set_add_remove_delete_devices(self):
+ self.assert_bad_syntax_cli_exception(
+ [
+ "stonith-id",
+ "set",
+ "add",
+ "d2",
+ "remove",
+ "d3",
+ "delete",
+ "d4",
+ ]
+ )
+
+ def test_set_devices(self):
+ self.call_cmd(["stonith-id", "set", "d1", "d2"])
+ self.assert_called_with("stonith-id", ["d1", "d2"], [])
+
+ def test_add_devices(self):
+ self.call_cmd(["stonith-id", "add", "d1", "d2"])
+ self.assert_add_remove_called_with("stonith-id", ["d1", "d2"], [], [])
+
+ def test_remove_devices(self):
+ self.call_cmd(["stonith-id", "remove", "d1", "d2"])
+ self.assert_add_remove_called_with("stonith-id", [], ["d1", "d2"], [])
+
+ def test_delete_devices(self):
+ self.call_cmd(["stonith-id", "delete", "d1", "d2"])
+ self.assert_add_remove_called_with("stonith-id", [], ["d1", "d2"], [])
+
+ def test_add_remove_devices(self):
+ self.call_cmd(["stonith-id", "add", "d1", "d2", "remove", "d3", "d4"])
+ self.assert_add_remove_called_with(
+ "stonith-id", ["d1", "d2"], ["d3", "d4"], []
+ )
+
+ def test_add_delete_devices(self):
+ self.call_cmd(["stonith-id", "add", "d1", "d2", "delete", "d3", "d4"])
+ self.assert_add_remove_called_with(
+ "stonith-id", ["d1", "d2"], ["d3", "d4"], []
+ )
+
+ def test_add_delete_remove_devices(self):
+ self.call_cmd(
+ [
+ "stonith-id",
+ "add",
+ "d1",
+ "d2",
+ "delete",
+ "d3",
+ "d4",
+ "remove",
+ "d5",
+ ]
+ )
+ self.assert_add_remove_called_with(
+ "stonith-id", ["d1", "d2"], ["d3", "d4", "d5"], []
+ )
+
+ def test_remove_delete_devices(self):
+ self.call_cmd(
+ ["stonith-id", "remove", "d2", "d1", "delete", "d4", "d3"]
+ )
+ self.assert_add_remove_called_with(
+ "stonith-id", [], ["d4", "d3", "d2", "d1"], []
+ )
diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py
index 0cb97138..b0826cfd 100644
--- a/pcs_test/tier0/common/reports/test_messages.py
+++ b/pcs_test/tier0/common/reports/test_messages.py
@@ -1761,6 +1761,7 @@ class ResourceBundleAlreadyContainsAResource(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class CannotGroupResourceAdjacentResourceForNewGroup(NameBuildTest):
def test_success(self):
self.assert_message_from_report(
@@ -1772,6 +1773,7 @@ class CannotGroupResourceAdjacentResourceForNewGroup(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class CannotGroupResourceAdjacentResourceNotInGroup(NameBuildTest):
def test_success(self):
self.assert_message_from_report(
@@ -1783,6 +1785,7 @@ class CannotGroupResourceAdjacentResourceNotInGroup(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class CannotGroupResourceAlreadyInTheGroup(NameBuildTest):
def test_single_resource(self):
self.assert_message_from_report(
@@ -1797,6 +1800,7 @@ class CannotGroupResourceAlreadyInTheGroup(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class CannotGroupResourceMoreThanOnce(NameBuildTest):
def test_single_resource(self):
self.assert_message_from_report(
@@ -1811,6 +1815,7 @@ class CannotGroupResourceMoreThanOnce(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class CannotGroupResourceNoResources(NameBuildTest):
def test_success(self):
self.assert_message_from_report(
@@ -1818,6 +1823,7 @@ class CannotGroupResourceNoResources(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class CannotGroupResourceNextToItself(NameBuildTest):
def test_success(self):
self.assert_message_from_report(
@@ -4836,6 +4842,7 @@ class BoothTicketOperationFailed(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagAddRemoveIdsDuplication(NameBuildTest):
def test_message_add(self):
self.assert_message_from_report(
@@ -4855,6 +4862,7 @@ class TagAddRemoveIdsDuplication(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagAdjacentReferenceIdNotInTheTag(NameBuildTest):
def test_messag(self):
self.assert_message_from_report(
@@ -4866,6 +4874,7 @@ class TagAdjacentReferenceIdNotInTheTag(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagCannotAddAndRemoveIdsAtTheSameTime(NameBuildTest):
def test_message_one_item(self):
self.assert_message_from_report(
@@ -4885,6 +4894,7 @@ class TagCannotAddAndRemoveIdsAtTheSameTime(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagCannotAddReferenceIdsAlreadyInTheTag(NameBuildTest):
def test_message_singular(self):
self.assert_message_from_report(
@@ -4920,6 +4930,7 @@ class TagCannotCreateEmptyTagNoIdsSpecified(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagCannotPutIdNextToItself(NameBuildTest):
def test_message(self):
self.assert_message_from_report(
@@ -4928,6 +4939,7 @@ class TagCannotPutIdNextToItself(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagCannotRemoveAdjacentId(NameBuildTest):
def test_message(self):
self.assert_message_from_report(
@@ -4936,6 +4948,7 @@ class TagCannotRemoveAdjacentId(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagCannotRemoveReferencesWithoutRemovingTag(NameBuildTest):
def test_message(self):
self.assert_message_from_report(
@@ -4974,6 +4987,7 @@ class TagCannotRemoveTagsNoTagsSpecified(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagCannotSpecifyAdjacentIdWithoutIdsToAdd(NameBuildTest):
def test_message(self):
self.assert_message_from_report(
@@ -4982,6 +4996,7 @@ class TagCannotSpecifyAdjacentIdWithoutIdsToAdd(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagCannotUpdateTagNoIdsSpecified(NameBuildTest):
def test_message(self):
self.assert_message_from_report(
@@ -4990,6 +5005,7 @@ class TagCannotUpdateTagNoIdsSpecified(NameBuildTest):
)
+# TODO: remove, use ADD_REMOVE reports
class TagIdsNotInTheTag(NameBuildTest):
def test_message_singular(self):
self.assert_message_from_report(
@@ -5080,3 +5096,172 @@ class CibNvsetAmbiguousProvideNvsetId(NameBuildTest):
const.PCS_COMMAND_RESOURCE_DEFAULTS_UPDATE
),
)
+
+
+class AddRemoveItemsNotSpecified(NameBuildTest):
+ def test_message(self):
+ self.assert_message_from_report(
+ (
+ "Cannot modify stonith resource 'container-id', no devices to "
+ "add or remove specified"
+ ),
+ reports.AddRemoveItemsNotSpecified(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ ),
+ )
+
+
+class AddRemoveItemsDuplication(NameBuildTest):
+ def test_message(self):
+ self.assert_message_from_report(
+ (
+ "Devices to add or remove must be unique, duplicate devices: "
+ "'dup1', 'dup2'"
+ ),
+ reports.AddRemoveItemsDuplication(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ ["dup2", "dup1"],
+ ),
+ )
+
+
+class AddRemoveCannotAddItemsAlreadyInTheContainer(NameBuildTest):
+ def test_message_plural(self):
+ self.assert_message_from_report(
+ "Cannot add devices 'i1', 'i2', they are already present in stonith"
+ " resource 'container-id'",
+ reports.AddRemoveCannotAddItemsAlreadyInTheContainer(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ ["i2", "i1"],
+ ),
+ )
+
+ def test_message_singular(self):
+ self.assert_message_from_report(
+ "Cannot add device 'i1', it is already present in stonith resource "
+ "'container-id'",
+ reports.AddRemoveCannotAddItemsAlreadyInTheContainer(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ ["i1"],
+ ),
+ )
+
+
+class AddRemoveCannotRemoveItemsNotInTheContainer(NameBuildTest):
+ def test_message_plural(self):
+ self.assert_message_from_report(
+ (
+ "Cannot remove devices 'i1', 'i2', they are not present in "
+ "stonith resource 'container-id'"
+ ),
+ reports.AddRemoveCannotRemoveItemsNotInTheContainer(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ ["i2", "i1"],
+ ),
+ )
+
+ def test_message_singular(self):
+ self.assert_message_from_report(
+ (
+ "Cannot remove device 'i1', it is not present in "
+ "stonith resource 'container-id'"
+ ),
+ reports.AddRemoveCannotRemoveItemsNotInTheContainer(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ ["i1"],
+ ),
+ )
+
+
+class AddRemoveCannotAddAndRemoveItemsAtTheSameTime(NameBuildTest):
+ def test_message_plural(self):
+ self.assert_message_from_report(
+ "Devices cannot be added and removed at the same time: 'i1', 'i2'",
+ reports.AddRemoveCannotAddAndRemoveItemsAtTheSameTime(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ ["i2", "i1"],
+ ),
+ )
+
+ def test_message_singular(self):
+ self.assert_message_from_report(
+ "Device cannot be added and removed at the same time: 'i1'",
+ reports.AddRemoveCannotAddAndRemoveItemsAtTheSameTime(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ ["i1"],
+ ),
+ )
+
+
+class AddRemoveCannotRemoveAllItemsFromTheContainer(NameBuildTest):
+ def test_message(self):
+ self.assert_message_from_report(
+ "Cannot remove all devices from stonith resource 'container-id'",
+ reports.AddRemoveCannotRemoveAllItemsFromTheContainer(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ ["i1", "i2"],
+ ),
+ )
+
+
+class AddRemoveAdjacentItemNotInTheContainer(NameBuildTest):
+ def test_message(self):
+ self.assert_message_from_report(
+ (
+ "There is no device 'adjacent-item-id' in the stonith resource "
+ "'container-id', cannot add devices next to it"
+ ),
+ reports.AddRemoveAdjacentItemNotInTheContainer(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ "adjacent-item-id",
+ ),
+ )
+
+
+class AddRemoveCannotPutItemNextToItself(NameBuildTest):
+ def test_message(self):
+ self.assert_message_from_report(
+ "Cannot put device 'adjacent-item-id' next to itself",
+ reports.AddRemoveCannotPutItemNextToItself(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ "adjacent-item-id",
+ ),
+ )
+
+
+class AddRemoveCannotSpecifyAdjacentItemWithoutItemsToAdd(NameBuildTest):
+ def test_message(self):
+ self.assert_message_from_report(
+ (
+ "Cannot specify adjacent device 'adjacent-item-id' without "
+ "devices to add"
+ ),
+ reports.AddRemoveCannotSpecifyAdjacentItemWithoutItemsToAdd(
+ const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ const.ADD_REMOVE_ITEM_TYPE_DEVICE,
+ "container-id",
+ "adjacent-item-id",
+ ),
+ )
diff --git a/pcs_test/tier0/common/test_str_tools.py b/pcs_test/tier0/common/test_str_tools.py
index 97c1d223..b0028a88 100644
--- a/pcs_test/tier0/common/test_str_tools.py
+++ b/pcs_test/tier0/common/test_str_tools.py
@@ -1,5 +1,5 @@
# pylint: disable=protected-access
-from unittest import TestCase, mock
+from unittest import TestCase
from pcs.common import str_tools as tools
@@ -124,73 +124,48 @@ class AddSTest(TestCase):
self.assertEqual(tools._add_s("church"), "churches")
-@mock.patch("pcs.common.str_tools._add_s")
-@mock.patch("pcs.common.str_tools._is_multiple")
+class GetPluralTest(TestCase):
+ def test_common_plural(self):
+ self.assertEqual("are", tools.get_plural("is"))
+
+ def test_add_s(self):
+ self.assertEqual("pieces", tools.get_plural("piece"))
+
+
class FormatPluralTest(TestCase):
- def test_is_sg(self, mock_is_multiple, mock_add_s):
- mock_is_multiple.return_value = False
+ def test_is_sg(self):
self.assertEqual("is", tools.format_plural(1, "is"))
- mock_add_s.assert_not_called()
- mock_is_multiple.assert_called_once_with(1)
- def test_is_pl(self, mock_is_multiple, mock_add_s):
- mock_is_multiple.return_value = True
+ def test_is_pl(self):
self.assertEqual("are", tools.format_plural(2, "is"))
- mock_add_s.assert_not_called()
- mock_is_multiple.assert_called_once_with(2)
- def test_do_sg(self, mock_is_multiple, mock_add_s):
- mock_is_multiple.return_value = False
+ def test_do_sg(self):
self.assertEqual("does", tools.format_plural("he", "does"))
- mock_add_s.assert_not_called()
- mock_is_multiple.assert_called_once_with("he")
- def test_do_pl(self, mock_is_multiple, mock_add_s):
- mock_is_multiple.return_value = True
+ def test_do_pl(self):
self.assertEqual("do", tools.format_plural(["he", "she"], "does"))
- mock_add_s.assert_not_called()
- mock_is_multiple.assert_called_once_with(["he", "she"])
- def test_have_sg(self, mock_is_multiple, mock_add_s):
- mock_is_multiple.return_value = False
+ def test_have_sg(self):
self.assertEqual("has", tools.format_plural("he", "has"))
- mock_add_s.assert_not_called()
- mock_is_multiple.assert_called_once_with("he")
- def test_have_pl(self, mock_is_multiple, mock_add_s):
- mock_is_multiple.return_value = True
+ def test_have_pl(self):
self.assertEqual("have", tools.format_plural(["he", "she"], "has"))
- mock_add_s.assert_not_called()
- mock_is_multiple.assert_called_once_with(["he", "she"])
- def test_plural_sg(self, mock_is_multiple, mock_add_s):
- mock_is_multiple.return_value = False
+ def test_plural_sg(self):
self.assertEqual(
"singular", tools.format_plural(1, "singular", "plural")
)
- mock_add_s.assert_not_called()
- mock_is_multiple.assert_called_once_with(1)
- def test_plural_pl(self, mock_is_multiple, mock_add_s):
- mock_is_multiple.return_value = True
+ def test_plural_pl(self):
self.assertEqual(
"plural", tools.format_plural(10, "singular", "plural")
)
- mock_add_s.assert_not_called()
- mock_is_multiple.assert_called_once_with(10)
- def test_regular_sg(self, mock_is_multiple, mock_add_s):
- mock_is_multiple.return_value = False
+ def test_regular_sg(self):
self.assertEqual("greeting", tools.format_plural(1, "greeting"))
- mock_add_s.assert_not_called()
- mock_is_multiple.assert_called_once_with(1)
- def test_regular_pl(self, mock_is_multiple, mock_add_s):
- mock_add_s.return_value = "greetings"
- mock_is_multiple.return_value = True
+ def test_regular_pl(self):
self.assertEqual("greetings", tools.format_plural(10, "greeting"))
- mock_add_s.assert_called_once_with("greeting")
- mock_is_multiple.assert_called_once_with(10)
class FormatList(TestCase):
diff --git a/pcs_test/tier0/lib/cib/test_stonith.py b/pcs_test/tier0/lib/cib/test_stonith.py
index ef7571ce..df059121 100644
--- a/pcs_test/tier0/lib/cib/test_stonith.py
+++ b/pcs_test/tier0/lib/cib/test_stonith.py
@@ -2,8 +2,12 @@ from unittest import TestCase
from lxml import etree
+from pcs.common import reports
from pcs.lib.cib import stonith
+from pcs_test.tools import fixture
+from pcs_test.tools.assertions import assert_report_item_list_equal
+
class IsStonithEnabled(TestCase):
def test_not_set(self):
@@ -149,8 +153,129 @@ class GetMisconfiguredResources(TestCase):
)
-class ValidateStonithDeviceExistsAndSupported(TestCase):
- """
- tested in:
- pcs_test.tier0.lib.commands.test_stonith_update_scsi_devices.TestUpdateScsiDevicesFailures
- """
+class ValidateStonithRestartlessUpdate(TestCase):
+ RESOURCES = etree.fromstring(
+ """
+ <resources>
+ <primitive id="supported" class="stonith" type="fence_scsi">
+ <instance_attributes>
+ <nvpair name="devices" value="/dev/sda" />
+ </instance_attributes>
+ </primitive>
+ <primitive id="empty" class="stonith" type="fence_scsi">
+ <instance_attributes>
+ <nvpair id="empty-instance_attributes-devices"
+ name="devices" value="" />
+ </instance_attributes>
+ </primitive>
+ <primitive id="no-devices" class="stonith" type="fence_scsi"/>
+ <primitive id="unsupported_provider"
+ class="stonith" provider="provider" type="fence_scsi"/>
+ <primitive id="unsupported_type" class="stonith" type="fence_xvm"/>
+ <primitive class="ocf" id="cp-01" provider="pacemaker" type="Dummy"/>
+ </resources>
+ """
+ )
+
+ def assert_unsupported_stonith_agent(self, resource_id, resource_type):
+ stonith_el, report_list = stonith.validate_stonith_restartless_update(
+ self.RESOURCES, resource_id
+ )
+ self.assertEqual(
+ stonith_el,
+ self.RESOURCES.find(f".//primitive[@id='{resource_id}']"),
+ )
+ assert_report_item_list_equal(
+ report_list,
+ [
+ fixture.error(
+ reports.codes.STONITH_RESTARTLESS_UPDATE_UNSUPPORTED_AGENT,
+ resource_id=resource_id,
+ resource_type=resource_type,
+ supported_stonith_types=["fence_scsi"],
+ )
+ ],
+ )
+
+ def assert_no_devices(self, resource_id):
+ stonith_el, report_list = stonith.validate_stonith_restartless_update(
+ self.RESOURCES, resource_id
+ )
+ self.assertEqual(
+ stonith_el,
+ self.RESOURCES.find(f".//primitive[@id='{resource_id}']"),
+ )
+ assert_report_item_list_equal(
+ report_list,
+ [
+ fixture.error(
+ reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM,
+ reason=(
+ "no devices option configured for stonith device "
+ f"'{resource_id}'"
+ ),
+ reason_type="other",
+ )
+ ],
+ )
+
+ def test_supported(self):
+ stonith_el, report_list = stonith.validate_stonith_restartless_update(
+ self.RESOURCES, "supported"
+ )
+ self.assertEqual(
+ stonith_el, self.RESOURCES.find(".//primitive[@id='supported']")
+ )
+ assert_report_item_list_equal(report_list, [])
+
+ def test_nonexistent_id(self):
+ stonith_el, report_list = stonith.validate_stonith_restartless_update(
+ self.RESOURCES, "non-existent"
+ )
+ self.assertEqual(stonith_el, None)
+ assert_report_item_list_equal(
+ report_list,
+ [
+ fixture.error(
+ reports.codes.ID_NOT_FOUND,
+ id="non-existent",
+ expected_types=["primitive"],
+ context_type="resources",
+ context_id="",
+ )
+ ],
+ )
+
+ def test_not_a_resource_id(self):
+ stonith_el, report_list = stonith.validate_stonith_restartless_update(
+ self.RESOURCES, "empty-instance_attributes-devices"
+ )
+ self.assertEqual(stonith_el, None)
+ assert_report_item_list_equal(
+ report_list,
+ [
+ fixture.error(
+ reports.codes.ID_BELONGS_TO_UNEXPECTED_TYPE,
+ id="empty-instance_attributes-devices",
+ expected_types=["primitive"],
+ current_type="nvpair",
+ )
+ ],
+ )
+
+ def test_devices_empty(self):
+ self.assert_no_devices("empty")
+
+ def test_missing_devices_attr(self):
+ self.assert_no_devices("no-devices")
+
+ def test_unsupported_class(self):
+ self.assert_unsupported_stonith_agent("cp-01", "Dummy")
+
+ def test_unsupported_provider(self):
+ self.assert_unsupported_stonith_agent(
+ "unsupported_provider", "fence_scsi"
+ )
+
+ def test_unsupported_type(self):
+ self.assert_unsupported_stonith_agent("unsupported_type", "fence_xvm")
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 3bc51325..6ff6b99a 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
@@ -3,6 +3,7 @@ from unittest import mock, TestCase
from pcs_test.tools import fixture
+from pcs_test.tools.assertions import assert_report_item_list_equal
from pcs_test.tools.command_env import get_env_tools
from pcs_test.tools.misc import get_test_resource as rc
@@ -13,6 +14,10 @@ from pcs.common import (
reports,
)
from pcs.common.interface import dto
+from pcs.common.reports.const import (
+ ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ ADD_REMOVE_ITEM_TYPE_DEVICE,
+)
from pcs.common.tools import timeout_to_seconds
from .cluster.common import (
@@ -28,6 +33,10 @@ DEFAULT_DIGEST = _DIGEST + "0"
ALL_DIGEST = _DIGEST + "1"
NONPRIVATE_DIGEST = _DIGEST + "2"
NONRELOADABLE_DIGEST = _DIGEST + "3"
+DEV_1 = "/dev/sda"
+DEV_2 = "/dev/sdb"
+DEV_3 = "/dev/sdc"
+DEV_4 = "/dev/sdd"
DEVICES_1 = ("/dev/sda",)
DEVICES_2 = ("/dev/sda", "/dev/sdb")
DEVICES_3 = ("/dev/sda", "/dev/sdb", "/dev/sdc")
@@ -197,13 +206,9 @@ FIXTURE_CRM_MON_RES_STOPPED = f"""
"""
-@mock.patch.object(
- settings,
- "pacemaker_api_result_schema",
- rc("pcmk_api_rng/api-result.rng"),
-)
-class UpdateScsiDevices(TestCase):
+class UpdateScsiDevicesMixin:
def setUp(self):
+ # pylint: disable=invalid-name
self.env_assist, self.config = get_env_tools(self)
self.existing_nodes = ["node1", "node2", "node3"]
@@ -217,14 +222,18 @@ class UpdateScsiDevices(TestCase):
self,
devices_before=DEVICES_1,
devices_updated=DEVICES_2,
+ devices_add=(),
+ devices_remove=(),
+ 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,
):
+ # pylint: disable=too-many-arguments
# pylint: disable=too-many-locals
- self.config.runner.pcmk.is_resource_digests_supported()
+ devices_value = ",".join(sorted(devices_updated))
self.config.runner.cib.load(
resources=fixture_scsi(
devices=devices_before, resource_ops=resource_ops
@@ -235,16 +244,17 @@ class UpdateScsiDevices(TestCase):
lrm_monitor_ops=lrm_monitor_ops,
),
)
+ self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.pcmk.load_state(
resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
)
- devices_opt = "devices={}".format(",".join(devices_updated))
+ devices_opt = "devices={}".format(devices_value)
self.config.runner.pcmk.resource_digests(
SCSI_STONITH_ID,
SCSI_NODE,
name="start.op.digests",
stdout=fixture_digests_xml(
- SCSI_STONITH_ID, SCSI_NODE, devices=",".join(devices_updated)
+ SCSI_STONITH_ID, SCSI_NODE, devices=devices_value
),
args=[devices_opt],
)
@@ -272,22 +282,23 @@ class UpdateScsiDevices(TestCase):
stdout=fixture_digests_xml(
SCSI_STONITH_ID,
SCSI_NODE,
- devices=",".join(devices_updated),
+ devices=devices_value,
),
args=args,
)
- self.config.corosync_conf.load_content(
- corosync_conf_fixture(
- self.existing_corosync_nodes,
- get_two_node(len(self.existing_corosync_nodes)),
+ if unfence:
+ self.config.corosync_conf.load_content(
+ corosync_conf_fixture(
+ self.existing_corosync_nodes,
+ get_two_node(len(self.existing_corosync_nodes)),
+ )
+ )
+ self.config.http.corosync.get_corosync_online_targets(
+ node_labels=self.existing_nodes
+ )
+ self.config.http.scsi.unfence_node(
+ unfence, node_labels=self.existing_nodes
)
- )
- self.config.http.corosync.get_corosync_online_targets(
- node_labels=self.existing_nodes
- )
- self.config.http.scsi.unfence_node(
- devices_updated, node_labels=self.existing_nodes
- )
self.config.env.push_cib(
resources=fixture_scsi(
devices=devices_updated, resource_ops=resource_ops
@@ -298,113 +309,25 @@ class UpdateScsiDevices(TestCase):
lrm_monitor_ops=lrm_monitor_ops_updated,
),
)
- stonith.update_scsi_devices(
- self.env_assist.get_env(), SCSI_STONITH_ID, devices_updated
- )
+ if devices_add or devices_remove:
+ stonith.update_scsi_devices_add_remove(
+ self.env_assist.get_env(),
+ SCSI_STONITH_ID,
+ devices_add,
+ devices_remove,
+ )
+ else:
+ stonith.update_scsi_devices(
+ self.env_assist.get_env(), SCSI_STONITH_ID, devices_updated
+ )
self.env_assist.assert_reports([])
- def test_update_1_to_1_devices(self):
- self.assert_command_success(
- devices_before=DEVICES_1, devices_updated=DEVICES_1
- )
-
- def test_update_2_to_2_devices(self):
- self.assert_command_success(
- devices_before=DEVICES_1, devices_updated=DEVICES_1
- )
-
- def test_update_1_to_2_devices(self):
- self.assert_command_success()
-
- def test_update_1_to_3_devices(self):
- self.assert_command_success(
- devices_before=DEVICES_1, devices_updated=DEVICES_3
- )
-
- def test_update_3_to_1_devices(self):
- self.assert_command_success(
- devices_before=DEVICES_3, devices_updated=DEVICES_1
- )
-
- def test_update_3_to_2_devices(self):
- self.assert_command_success(
- devices_before=DEVICES_3, devices_updated=DEVICES_2
- )
-
- def test_default_monitor(self):
- self.assert_command_success()
-
- def test_no_monitor_ops(self):
- self.assert_command_success(
- resource_ops=(), lrm_monitor_ops=(), lrm_monitor_ops_updated=()
- )
-
- def test_1_monitor_with_timeout(self):
- self.assert_command_success(
- 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(
- 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(
- 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(
- 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(
- resource_ops=(
- ("monitor", "60s", None, None),
- ("start", "0s", "40s", None),
- ),
- )
+class UpdateScsiDevicesFailuresMixin:
+ def command(self, force_flags=()):
+ raise NotImplementedError
-@mock.patch.object(
- settings,
- "pacemaker_api_result_schema",
- rc("pcmk_api_rng/api-result.rng"),
-)
-class TestUpdateScsiDevicesFailures(TestCase):
- # pylint: disable=too-many-public-methods
+ # pylint: disable=invalid-name
def setUp(self):
self.env_assist, self.config = get_env_tools(self)
@@ -416,13 +339,12 @@ class TestUpdateScsiDevicesFailures(TestCase):
self.config.env.set_known_nodes(self.existing_nodes)
def test_pcmk_doesnt_support_digests(self):
+ self.config.runner.cib.load(resources=fixture_scsi())
self.config.runner.pcmk.is_resource_digests_supported(
is_supported=False
)
self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), SCSI_STONITH_ID, ()
- ),
+ self.command(),
[
fixture.error(
reports.codes.STONITH_RESTARTLESS_UPDATE_OF_SCSI_DEVICES_NOT_SUPPORTED,
@@ -431,134 +353,557 @@ class TestUpdateScsiDevicesFailures(TestCase):
expected_in_processor=False,
)
- def test_devices_cannot_be_empty(self):
- self.config.runner.pcmk.is_resource_digests_supported()
- self.config.runner.cib.load(resources=fixture_scsi())
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), SCSI_STONITH_ID, ()
- )
- )
- self.env_assist.assert_reports(
- [
- fixture.error(
- reports.codes.INVALID_OPTION_VALUE,
- option_name="devices",
- option_value="",
- allowed_values=None,
- cannot_be_empty=True,
- forbidden_characters=None,
- )
- ]
+ def test_node_missing_name_and_missing_auth_token(self):
+ self.config.runner.cib.load(
+ resources=fixture_scsi(),
+ status=_fixture_status_lrm_ops(SCSI_STONITH_ID),
)
-
- def test_nonexistant_id(self):
self.config.runner.pcmk.is_resource_digests_supported()
- self.config.runner.cib.load(resources=fixture_scsi())
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), "non-existent-id", DEVICES_2
- )
- )
- self.env_assist.assert_reports(
- [
- fixture.error(
- reports.codes.ID_NOT_FOUND,
- id="non-existent-id",
- expected_types=["primitive"],
- context_type="cib",
- context_id="",
- )
- ]
+ self.config.runner.pcmk.load_state(
+ resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
)
-
- def test_not_a_resource_id(self):
- self.config.runner.pcmk.is_resource_digests_supported()
- self.config.runner.cib.load(resources=fixture_scsi())
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(),
- f"{SCSI_STONITH_ID}-instance_attributes-devices",
- DEVICES_2,
- )
+ self.config.runner.pcmk.resource_digests(
+ SCSI_STONITH_ID,
+ SCSI_NODE,
+ name="start.op.digests",
+ stdout=fixture_digests_xml(
+ SCSI_STONITH_ID, SCSI_NODE, devices=",".join(DEVICES_2)
+ ),
+ args=["devices={}".format(",".join(DEVICES_2))],
)
- self.env_assist.assert_reports(
- [
- fixture.error(
- reports.codes.ID_BELONGS_TO_UNEXPECTED_TYPE,
- id=f"{SCSI_STONITH_ID}-instance_attributes-devices",
- expected_types=["primitive"],
- current_type="nvpair",
- )
- ]
+ self.config.runner.pcmk.resource_digests(
+ SCSI_STONITH_ID,
+ SCSI_NODE,
+ name="monitor.op.digests",
+ stdout=fixture_digests_xml(
+ SCSI_STONITH_ID, SCSI_NODE, devices=",".join(DEVICES_2)
+ ),
+ args=[
+ "devices={}".format(",".join(DEVICES_2)),
+ "CRM_meta_interval=60000",
+ ],
)
-
- def test_not_supported_resource_type(self):
- self.config.runner.pcmk.is_resource_digests_supported()
- self.config.runner.cib.load(resources=fixture_scsi())
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), "dummy", DEVICES_2
+ self.config.corosync_conf.load_content(
+ corosync_conf_fixture(
+ self.existing_corosync_nodes
+ + [[("ring0_addr", "custom_node"), ("nodeid", "5")]],
)
)
+ self.config.env.set_known_nodes(self.existing_nodes[:-1])
+ self.env_assist.assert_raise_library_error(self.command())
self.env_assist.assert_reports(
[
fixture.error(
- reports.codes.STONITH_RESTARTLESS_UPDATE_UNSUPPORTED_AGENT,
- resource_id="dummy",
- resource_type="Dummy",
- supported_stonith_types=["fence_scsi"],
- )
- ]
- )
-
- def test_devices_option_missing(self):
- self.config.runner.pcmk.is_resource_digests_supported()
- self.config.runner.cib.load(resources=fixture_scsi(devices=None))
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), SCSI_STONITH_ID, DEVICES_2
- )
- )
- self.env_assist.assert_reports(
- [
+ reports.codes.COROSYNC_CONFIG_MISSING_NAMES_OF_NODES,
+ fatal=True,
+ ),
fixture.error(
- reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM,
- reason=(
- "no devices option configured for stonith device "
- f"'{SCSI_STONITH_ID}'"
- ),
- reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER,
- )
+ reports.codes.HOST_NOT_FOUND,
+ host_list=[self.existing_nodes[-1]],
+ ),
]
)
- def test_devices_option_empty(self):
- self.config.runner.pcmk.is_resource_digests_supported()
- self.config.runner.cib.load(resources=fixture_scsi(devices=""))
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), SCSI_STONITH_ID, DEVICES_2
- )
- )
- self.env_assist.assert_reports(
- [
- fixture.error(
- reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM,
- reason=(
- "no devices option configured for stonith device "
- f"'{SCSI_STONITH_ID}'"
- ),
- reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER,
- )
- ]
+ def _unfence_failure_common_calls(self):
+ devices = ",".join(DEVICES_2)
+ self.config.runner.cib.load(
+ resources=fixture_scsi(),
+ status=_fixture_status_lrm_ops(SCSI_STONITH_ID),
)
-
- def test_stonith_resource_is_not_running(self):
self.config.runner.pcmk.is_resource_digests_supported()
- self.config.runner.cib.load(resources=fixture_scsi())
self.config.runner.pcmk.load_state(
- resources=FIXTURE_CRM_MON_RES_STOPPED, nodes=FIXTURE_CRM_MON_NODES
+ resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
+ )
+ self.config.runner.pcmk.resource_digests(
+ SCSI_STONITH_ID,
+ SCSI_NODE,
+ name="start.op.digests",
+ stdout=fixture_digests_xml(
+ SCSI_STONITH_ID,
+ SCSI_NODE,
+ devices=devices,
+ ),
+ args=[f"devices={devices}"],
+ )
+ self.config.runner.pcmk.resource_digests(
+ SCSI_STONITH_ID,
+ SCSI_NODE,
+ name="monitor.op.digests",
+ stdout=fixture_digests_xml(
+ SCSI_STONITH_ID,
+ SCSI_NODE,
+ devices=devices,
+ ),
+ args=[
+ f"devices={devices}",
+ "CRM_meta_interval=60000",
+ ],
+ )
+ self.config.corosync_conf.load_content(
+ corosync_conf_fixture(self.existing_corosync_nodes)
+ )
+
+ def test_unfence_failure_unable_to_connect(self):
+ self._unfence_failure_common_calls()
+ self.config.http.corosync.get_corosync_online_targets(
+ node_labels=self.existing_nodes
+ )
+ self.config.http.scsi.unfence_node(
+ DEVICES_2,
+ communication_list=[
+ dict(
+ label=self.existing_nodes[0],
+ raw_data=json.dumps(
+ dict(
+ devices=[DEV_2],
+ node=self.existing_nodes[0],
+ )
+ ),
+ was_connected=False,
+ error_msg="errA",
+ ),
+ dict(
+ label=self.existing_nodes[1],
+ raw_data=json.dumps(
+ dict(
+ devices=[DEV_2],
+ node=self.existing_nodes[1],
+ )
+ ),
+ output=json.dumps(
+ dto.to_dict(
+ communication.dto.InternalCommunicationResultDto(
+ status=communication.const.COM_STATUS_ERROR,
+ status_msg="error",
+ report_list=[
+ reports.ReportItem.error(
+ reports.messages.StonithUnfencingFailed(
+ "errB"
+ )
+ ).to_dto()
+ ],
+ data=None,
+ )
+ )
+ ),
+ ),
+ dict(
+ label=self.existing_nodes[2],
+ raw_data=json.dumps(
+ dict(
+ devices=[DEV_2],
+ node=self.existing_nodes[2],
+ )
+ ),
+ ),
+ ],
+ )
+ self.env_assist.assert_raise_library_error(self.command())
+ self.env_assist.assert_reports(
+ [
+ fixture.error(
+ reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
+ node=self.existing_nodes[0],
+ command="api/v1/scsi-unfence-node/v1",
+ reason="errA",
+ ),
+ fixture.error(
+ reports.codes.STONITH_UNFENCING_FAILED,
+ reason="errB",
+ context=reports.dto.ReportItemContextDto(
+ node=self.existing_nodes[1],
+ ),
+ ),
+ ]
+ )
+
+ def test_unfence_failure_agent_script_failed(self):
+ self._unfence_failure_common_calls()
+ self.config.http.corosync.get_corosync_online_targets(
+ node_labels=self.existing_nodes
+ )
+ self.config.http.scsi.unfence_node(
+ DEVICES_2,
+ communication_list=[
+ dict(
+ label=self.existing_nodes[0],
+ raw_data=json.dumps(
+ dict(
+ devices=[DEV_2],
+ node=self.existing_nodes[0],
+ )
+ ),
+ ),
+ dict(
+ label=self.existing_nodes[1],
+ raw_data=json.dumps(
+ dict(
+ devices=[DEV_2],
+ node=self.existing_nodes[1],
+ )
+ ),
+ output=json.dumps(
+ dto.to_dict(
+ communication.dto.InternalCommunicationResultDto(
+ status=communication.const.COM_STATUS_ERROR,
+ status_msg="error",
+ report_list=[
+ reports.ReportItem.error(
+ reports.messages.StonithUnfencingFailed(
+ "errB"
+ )
+ ).to_dto()
+ ],
+ data=None,
+ )
+ )
+ ),
+ ),
+ dict(
+ label=self.existing_nodes[2],
+ raw_data=json.dumps(
+ dict(
+ devices=[DEV_2],
+ node=self.existing_nodes[2],
+ )
+ ),
+ ),
+ ],
+ )
+ self.env_assist.assert_raise_library_error(self.command())
+ self.env_assist.assert_reports(
+ [
+ fixture.error(
+ reports.codes.STONITH_UNFENCING_FAILED,
+ reason="errB",
+ context=reports.dto.ReportItemContextDto(
+ node=self.existing_nodes[1],
+ ),
+ ),
+ ]
+ )
+
+ def test_corosync_targets_unable_to_connect(self):
+ self._unfence_failure_common_calls()
+ self.config.http.corosync.get_corosync_online_targets(
+ communication_list=[
+ dict(
+ label=self.existing_nodes[0],
+ output='{"corosync":true}',
+ ),
+ ]
+ + [
+ dict(
+ label=node,
+ was_connected=False,
+ errno=7,
+ error_msg="an error",
+ )
+ for node in self.existing_nodes[1:]
+ ]
+ )
+ self.env_assist.assert_raise_library_error(self.command())
+ self.env_assist.assert_reports(
+ [
+ fixture.error(
+ reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
+ force_code=reports.codes.SKIP_OFFLINE_NODES,
+ node=node,
+ command="remote/status",
+ reason="an error",
+ )
+ for node in self.existing_nodes[1:]
+ ]
+ )
+
+ def test_corosync_targets_skip_offline_unfence_node_running_corosync(
+ self,
+ ):
+ self._unfence_failure_common_calls()
+ self.config.http.corosync.get_corosync_online_targets(
+ communication_list=[
+ dict(
+ label=self.existing_nodes[0],
+ output='{"corosync":true}',
+ ),
+ dict(
+ label=self.existing_nodes[1],
+ output='{"corosync":false}',
+ ),
+ dict(
+ label=self.existing_nodes[2],
+ was_connected=False,
+ errno=7,
+ error_msg="an error",
+ ),
+ ]
+ )
+ self.config.http.scsi.unfence_node(
+ DEVICES_2,
+ communication_list=[
+ dict(
+ label=self.existing_nodes[0],
+ raw_data=json.dumps(
+ dict(
+ devices=[DEV_2],
+ node=self.existing_nodes[0],
+ )
+ ),
+ ),
+ ],
+ )
+ self.config.env.push_cib(
+ resources=fixture_scsi(devices=DEVICES_2),
+ status=_fixture_status_lrm_ops(
+ SCSI_STONITH_ID,
+ lrm_start_ops=DEFAULT_LRM_START_OPS_UPDATED,
+ lrm_monitor_ops=DEFAULT_LRM_MONITOR_OPS_UPDATED,
+ ),
+ )
+ self.command(force_flags=[reports.codes.SKIP_OFFLINE_NODES])()
+ self.env_assist.assert_reports(
+ [
+ fixture.warn(
+ reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
+ node=self.existing_nodes[2],
+ command="remote/status",
+ reason="an error",
+ ),
+ ]
+ )
+
+ def test_corosync_targets_unable_to_perform_unfencing_operation(
+ self,
+ ):
+ self._unfence_failure_common_calls()
+ self.config.http.corosync.get_corosync_online_targets(
+ communication_list=[
+ dict(
+ label=self.existing_nodes[0],
+ was_connected=False,
+ errno=7,
+ error_msg="an error",
+ ),
+ dict(
+ label=self.existing_nodes[1],
+ was_connected=False,
+ errno=7,
+ error_msg="an error",
+ ),
+ dict(
+ label=self.existing_nodes[2],
+ output='{"corosync":false}',
+ ),
+ ]
+ )
+ self.config.http.scsi.unfence_node([DEV_2], communication_list=[])
+ self.env_assist.assert_raise_library_error(
+ self.command(force_flags=[reports.codes.SKIP_OFFLINE_NODES])
+ )
+ self.env_assist.assert_reports(
+ [
+ fixture.warn(
+ reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
+ node=node,
+ command="remote/status",
+ reason="an error",
+ )
+ for node in self.existing_nodes[0:2]
+ ]
+ + [
+ fixture.error(
+ reports.codes.UNABLE_TO_PERFORM_OPERATION_ON_ANY_NODE,
+ ),
+ ]
+ )
+
+
+@mock.patch.object(
+ settings,
+ "pacemaker_api_result_schema",
+ rc("pcmk_api_rng/api-result.rng"),
+)
+class UpdateScsiDevices(UpdateScsiDevicesMixin, TestCase):
+ def test_update_1_to_1_devices(self):
+ self.assert_command_success(
+ devices_before=DEVICES_1,
+ devices_updated=DEVICES_1,
+ )
+
+ def test_update_2_to_2_devices(self):
+ self.assert_command_success(
+ devices_before=DEVICES_2,
+ devices_updated=DEVICES_2,
+ )
+
+ def test_update_1_to_2_devices(self):
+ self.assert_command_success(unfence=[DEV_2])
+
+ def test_update_1_to_3_devices(self):
+ self.assert_command_success(
+ devices_before=DEVICES_1,
+ devices_updated=DEVICES_3,
+ unfence=[DEV_2, DEV_3],
+ )
+
+ def test_update_3_to_1_devices(self):
+ self.assert_command_success(
+ devices_before=DEVICES_3,
+ devices_updated=DEVICES_1,
+ )
+
+ def test_update_3_to_2_devices(self):
+ self.assert_command_success(
+ devices_before=DEVICES_3,
+ devices_updated=DEVICES_2,
+ )
+
+ def test_update_add_2_to_2_remove_1(self):
+ self.assert_command_success(
+ devices_before=[DEV_1, DEV_2],
+ devices_updated=[DEV_2, DEV_3, DEV_4],
+ 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),
+ ),
+ )
+
+
+@mock.patch.object(
+ settings,
+ "pacemaker_api_result_schema",
+ rc("pcmk_api_rng/api-result.rng"),
+)
+class TestUpdateScsiDevicesFailures(UpdateScsiDevicesFailuresMixin, TestCase):
+ def command(self, force_flags=()):
+ return lambda: stonith.update_scsi_devices(
+ self.env_assist.get_env(),
+ SCSI_STONITH_ID,
+ DEVICES_2,
+ force_flags=force_flags,
+ )
+
+ def test_devices_cannot_be_empty(self):
+ self.config.runner.cib.load(resources=fixture_scsi())
+ self.config.runner.pcmk.is_resource_digests_supported()
+ self.env_assist.assert_raise_library_error(
+ lambda: stonith.update_scsi_devices(
+ self.env_assist.get_env(), SCSI_STONITH_ID, ()
+ )
+ )
+ self.env_assist.assert_reports(
+ [
+ fixture.error(
+ reports.codes.INVALID_OPTION_VALUE,
+ option_name="devices",
+ option_value="",
+ allowed_values=None,
+ cannot_be_empty=True,
+ forbidden_characters=None,
+ )
+ ]
+ )
+
+ def test_nonexistant_id(self):
+ """
+ lower level tested in
+ pcs_test.tier0.lib.cib.test_stonith.ValidateStonithRestartlessUpdate
+ """
+ self.config.runner.cib.load(resources=fixture_scsi())
+ self.config.runner.pcmk.is_resource_digests_supported()
+ self.env_assist.assert_raise_library_error(
+ lambda: stonith.update_scsi_devices(
+ self.env_assist.get_env(), "non-existent-id", DEVICES_2
+ )
+ )
+ self.env_assist.assert_reports(
+ [
+ fixture.error(
+ reports.codes.ID_NOT_FOUND,
+ id="non-existent-id",
+ expected_types=["primitive"],
+ context_type="cib",
+ context_id="",
+ )
+ ]
+ )
+
+ def test_stonith_resource_is_not_running(self):
+ self.config.runner.cib.load(resources=fixture_scsi())
+ self.config.runner.pcmk.is_resource_digests_supported()
+ self.config.runner.pcmk.load_state(
+ resources=FIXTURE_CRM_MON_RES_STOPPED, nodes=FIXTURE_CRM_MON_NODES
)
self.env_assist.assert_raise_library_error(
lambda: stonith.update_scsi_devices(
@@ -575,8 +920,8 @@ class TestUpdateScsiDevicesFailures(TestCase):
)
def test_stonith_resource_is_running_on_more_than_one_node(self):
- self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.cib.load(resources=fixture_scsi())
+ self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.pcmk.load_state(
resources=FIXTURE_CRM_MON_RES_RUNNING_2, nodes=FIXTURE_CRM_MON_NODES
)
@@ -599,7 +944,6 @@ class TestUpdateScsiDevicesFailures(TestCase):
def test_lrm_op_missing_digest_attributes(self):
devices = ",".join(DEVICES_2)
- self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.cib.load(
resources=fixture_scsi(),
status=_fixture_status_lrm_ops_base(
@@ -607,6 +951,7 @@ class TestUpdateScsiDevicesFailures(TestCase):
f'<lrm_rsc_op id="{SCSI_STONITH_ID}_last" operation="start"/>',
),
)
+ self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.pcmk.load_state(
resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
)
@@ -637,7 +982,6 @@ class TestUpdateScsiDevicesFailures(TestCase):
def test_crm_resource_digests_missing(self):
devices = ",".join(DEVICES_2)
- self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.cib.load(
resources=fixture_scsi(),
status=_fixture_status_lrm_ops_base(
@@ -648,6 +992,7 @@ class TestUpdateScsiDevicesFailures(TestCase):
),
),
)
+ self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.pcmk.load_state(
resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
)
@@ -680,11 +1025,11 @@ class TestUpdateScsiDevicesFailures(TestCase):
)
def test_no_lrm_start_op(self):
- self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.cib.load(
resources=fixture_scsi(),
status=_fixture_status_lrm_ops(SCSI_STONITH_ID, lrm_start_ops=()),
)
+ self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.pcmk.load_state(
resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
)
@@ -705,7 +1050,6 @@ class TestUpdateScsiDevicesFailures(TestCase):
)
def test_monitor_ops_and_lrm_monitor_ops_do_not_match(self):
- self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.cib.load(
resources=fixture_scsi(
resource_ops=(
@@ -716,6 +1060,7 @@ class TestUpdateScsiDevicesFailures(TestCase):
),
status=_fixture_status_lrm_ops(SCSI_STONITH_ID),
)
+ self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.pcmk.load_state(
resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
)
@@ -746,13 +1091,13 @@ class TestUpdateScsiDevicesFailures(TestCase):
)
def test_lrm_monitor_ops_not_found(self):
- self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.cib.load(
resources=fixture_scsi(
resource_ops=(("monitor", "30s", None, None),)
),
status=_fixture_status_lrm_ops(SCSI_STONITH_ID),
)
+ self.config.runner.pcmk.is_resource_digests_supported()
self.config.runner.pcmk.load_state(
resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
)
@@ -783,371 +1128,353 @@ class TestUpdateScsiDevicesFailures(TestCase):
expected_in_processor=False,
)
- def test_node_missing_name_and_missing_auth_token(self):
- self.config.runner.pcmk.is_resource_digests_supported()
- self.config.runner.cib.load(
- resources=fixture_scsi(),
- status=_fixture_status_lrm_ops(SCSI_STONITH_ID),
- )
- self.config.runner.pcmk.load_state(
- resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
- )
- self.config.runner.pcmk.resource_digests(
- SCSI_STONITH_ID,
- SCSI_NODE,
- name="start.op.digests",
- stdout=fixture_digests_xml(
- SCSI_STONITH_ID, SCSI_NODE, devices=",".join(DEVICES_2)
- ),
- args=["devices={}".format(",".join(DEVICES_2))],
- )
- self.config.runner.pcmk.resource_digests(
- SCSI_STONITH_ID,
- SCSI_NODE,
- name="monitor.op.digests",
- stdout=fixture_digests_xml(
- SCSI_STONITH_ID, SCSI_NODE, devices=",".join(DEVICES_2)
- ),
- args=[
- "devices={}".format(",".join(DEVICES_2)),
- "CRM_meta_interval=60000",
- ],
- )
- self.config.corosync_conf.load_content(
- corosync_conf_fixture(
- self.existing_corosync_nodes
- + [[("ring0_addr", "custom_node"), ("nodeid", "5")]],
- )
- )
- self.config.env.set_known_nodes(self.existing_nodes[:-1])
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), SCSI_STONITH_ID, DEVICES_2
- ),
+
+@mock.patch.object(
+ settings,
+ "pacemaker_api_result_schema",
+ rc("pcmk_api_rng/api-result.rng"),
+)
+class UpdateScsiDevicesAddRemove(UpdateScsiDevicesMixin, TestCase):
+ def test_add_1_to_1(self):
+ self.assert_command_success(
+ devices_before=[DEV_1],
+ devices_updated=[DEV_1, DEV_2],
+ devices_add=[DEV_2],
+ devices_remove=[],
+ unfence=[DEV_2],
)
- self.env_assist.assert_reports(
- [
- fixture.error(
- reports.codes.COROSYNC_CONFIG_MISSING_NAMES_OF_NODES,
- fatal=True,
- ),
- fixture.error(
- reports.codes.HOST_NOT_FOUND,
- host_list=[self.existing_nodes[-1]],
- ),
- ]
+
+ def test_add_2_to_1(self):
+ self.assert_command_success(
+ devices_before=[DEV_1],
+ devices_updated=[DEV_1, DEV_2, DEV_3],
+ devices_add=[DEV_2, DEV_3],
+ devices_remove=[],
+ unfence=[DEV_2, DEV_3],
)
- def _unfence_failure_common_calls(self):
- devices = ",".join(DEVICES_2)
- self.config.runner.pcmk.is_resource_digests_supported()
- self.config.runner.cib.load(
- resources=fixture_scsi(),
- status=_fixture_status_lrm_ops(SCSI_STONITH_ID),
+ def test_add_2_to_2_and_remove_1(self):
+ self.assert_command_success(
+ devices_before=[DEV_1, DEV_2],
+ devices_updated=[DEV_2, DEV_3, DEV_4],
+ devices_add=[DEV_3, DEV_4],
+ devices_remove=[DEV_1],
+ unfence=[DEV_3, DEV_4],
)
- self.config.runner.pcmk.load_state(
- resources=FIXTURE_CRM_MON_RES_RUNNING_1, nodes=FIXTURE_CRM_MON_NODES
+
+ def test_remove_1_from_2(self):
+ self.assert_command_success(
+ devices_before=[DEV_1, DEV_2],
+ devices_updated=[DEV_2],
+ devices_add=[],
+ devices_remove=[DEV_1],
)
- self.config.runner.pcmk.resource_digests(
- SCSI_STONITH_ID,
- SCSI_NODE,
- name="start.op.digests",
- stdout=fixture_digests_xml(
- SCSI_STONITH_ID,
- SCSI_NODE,
- devices=devices,
- ),
- args=[f"devices={devices}"],
+
+ def test_remove_2_from_3(self):
+ self.assert_command_success(
+ devices_before=[DEV_1, DEV_2, DEV_3],
+ devices_updated=[DEV_3],
+ devices_add=[],
+ devices_remove=[DEV_2, DEV_1],
)
- self.config.runner.pcmk.resource_digests(
- SCSI_STONITH_ID,
- SCSI_NODE,
- name="monitor.op.digests",
- stdout=fixture_digests_xml(
- SCSI_STONITH_ID,
- SCSI_NODE,
- devices=devices,
- ),
- args=[
- f"devices={devices}",
- "CRM_meta_interval=60000",
- ],
+
+ def test_remove_2_from_3_add_1(self):
+ self.assert_command_success(
+ devices_before=[DEV_1, DEV_2, DEV_3],
+ devices_updated=[DEV_3, DEV_4],
+ devices_add=[DEV_4],
+ devices_remove=[DEV_2, DEV_1],
+ unfence=[DEV_4],
)
- self.config.corosync_conf.load_content(
- corosync_conf_fixture(self.existing_corosync_nodes)
+
+ def test_add_1_remove_1(self):
+ self.assert_command_success(
+ devices_before=[DEV_1, DEV_2],
+ devices_updated=[DEV_2, DEV_3],
+ devices_add=[DEV_3],
+ devices_remove=[DEV_1],
+ unfence=[DEV_3],
)
- def test_unfence_failure_unable_to_connect(self):
- self._unfence_failure_common_calls()
- self.config.http.corosync.get_corosync_online_targets(
- node_labels=self.existing_nodes
+ def test_add_2_remove_2(self):
+ self.assert_command_success(
+ devices_before=[DEV_1, DEV_2],
+ devices_updated=[DEV_3, DEV_4],
+ devices_add=[DEV_3, DEV_4],
+ devices_remove=[DEV_1, DEV_2],
+ unfence=[DEV_3, DEV_4],
)
- self.config.http.scsi.unfence_node(
- DEVICES_2,
- communication_list=[
- dict(
- label=self.existing_nodes[0],
- raw_data=json.dumps(
- dict(devices=DEVICES_2, node=self.existing_nodes[0])
- ),
- was_connected=False,
- error_msg="errA",
- ),
- dict(
- label=self.existing_nodes[1],
- raw_data=json.dumps(
- dict(devices=DEVICES_2, node=self.existing_nodes[1])
- ),
- output=json.dumps(
- dto.to_dict(
- communication.dto.InternalCommunicationResultDto(
- status=communication.const.COM_STATUS_ERROR,
- status_msg="error",
- report_list=[
- reports.ReportItem.error(
- reports.messages.StonithUnfencingFailed(
- "errB"
- )
- ).to_dto()
- ],
- data=None,
- )
- )
- ),
- ),
- dict(
- label=self.existing_nodes[2],
- raw_data=json.dumps(
- dict(devices=DEVICES_2, node=self.existing_nodes[2])
- ),
- ),
- ],
+
+
+@mock.patch.object(
+ settings,
+ "pacemaker_api_result_schema",
+ rc("pcmk_api_rng/api-result.rng"),
+)
+class TestUpdateScsiDevicesAddRemoveFailures(
+ UpdateScsiDevicesFailuresMixin, TestCase
+):
+ def command(self, force_flags=()):
+ return lambda: stonith.update_scsi_devices_add_remove(
+ self.env_assist.get_env(),
+ SCSI_STONITH_ID,
+ [DEV_2],
+ [],
+ force_flags=force_flags,
)
+
+ def test_add_remove_are_empty(self):
+ """
+ lower level tested in
+ pcs_test/tier0/lib/test_validate.ValidateAddRemoveItems
+ """
+ self.config.runner.cib.load(resources=fixture_scsi())
+ self.config.runner.pcmk.is_resource_digests_supported()
self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), SCSI_STONITH_ID, DEVICES_2
- ),
+ lambda: stonith.update_scsi_devices_add_remove(
+ self.env_assist.get_env(), SCSI_STONITH_ID, (), ()
+ )
)
self.env_assist.assert_reports(
[
fixture.error(
- reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
- node=self.existing_nodes[0],
- command="api/v1/scsi-unfence-node/v1",
- reason="errA",
- ),
+ reports.codes.ADD_REMOVE_ITEMS_NOT_SPECIFIED,
+ container_type=reports.const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE,
+ item_type="device",
+ container_id=SCSI_STONITH_ID,
+ )
+ ]
+ )
+
+ def test_not_supported_resource_type(self):
+ """
+ lower level tested in
+ pcs_test.tier0.lib.cib.test_stonith.ValidateStonithRestartlessUpdate
+ """
+ self.config.runner.cib.load(resources=fixture_scsi())
+ self.config.runner.pcmk.is_resource_digests_supported()
+ self.env_assist.assert_raise_library_error(
+ lambda: stonith.update_scsi_devices_add_remove(
+ self.env_assist.get_env(), "dummy", [DEV_2], [DEV_1]
+ )
+ )
+ self.env_assist.assert_reports(
+ [
fixture.error(
- reports.codes.STONITH_UNFENCING_FAILED,
- reason="errB",
- context=reports.dto.ReportItemContextDto(
- node=self.existing_nodes[1],
- ),
- ),
+ reports.codes.STONITH_RESTARTLESS_UPDATE_UNSUPPORTED_AGENT,
+ resource_id="dummy",
+ resource_type="Dummy",
+ supported_stonith_types=["fence_scsi"],
+ )
]
)
- def test_unfence_failure_agent_script_failed(self):
- self._unfence_failure_common_calls()
- self.config.http.corosync.get_corosync_online_targets(
- node_labels=self.existing_nodes
+ def test_stonith_resource_is_running_on_more_than_one_node(self):
+ self.config.runner.cib.load(resources=fixture_scsi())
+ self.config.runner.pcmk.is_resource_digests_supported()
+ self.config.runner.pcmk.load_state(
+ resources=FIXTURE_CRM_MON_RES_RUNNING_2, nodes=FIXTURE_CRM_MON_NODES
)
- self.config.http.scsi.unfence_node(
- DEVICES_2,
- communication_list=[
- dict(
- label=self.existing_nodes[0],
- raw_data=json.dumps(
- dict(devices=DEVICES_2, node=self.existing_nodes[0])
- ),
- ),
- dict(
- label=self.existing_nodes[1],
- raw_data=json.dumps(
- dict(devices=DEVICES_2, node=self.existing_nodes[1])
- ),
- output=json.dumps(
- dto.to_dict(
- communication.dto.InternalCommunicationResultDto(
- status=communication.const.COM_STATUS_ERROR,
- status_msg="error",
- report_list=[
- reports.ReportItem.error(
- reports.messages.StonithUnfencingFailed(
- "errB"
- )
- ).to_dto()
- ],
- data=None,
- )
- )
- ),
- ),
- dict(
- label=self.existing_nodes[2],
- raw_data=json.dumps(
- dict(devices=DEVICES_2, node=self.existing_nodes[2])
+ self.env_assist.assert_raise_library_error(
+ lambda: stonith.update_scsi_devices_add_remove(
+ self.env_assist.get_env(), SCSI_STONITH_ID, [DEV_2], []
+ ),
+ [
+ fixture.error(
+ reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM,
+ reason=(
+ f"resource '{SCSI_STONITH_ID}' is running on more than "
+ "1 node"
),
- ),
+ reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER,
+ )
],
+ expected_in_processor=False,
)
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), SCSI_STONITH_ID, DEVICES_2
- ),
+
+
+class ValidateAddRemoveItems(TestCase):
+ CONTAINER_TYPE = ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE
+ ITEM_TYPE = ADD_REMOVE_ITEM_TYPE_DEVICE
+ CONTAINER_ID = "container_id"
+
+ def _validate(
+ self, add, remove, current=None, adjacent=None, can_be_empty=False
+ ):
+ # pylint: disable=protected-access
+ return stonith._validate_add_remove_items(
+ add,
+ remove,
+ current,
+ self.CONTAINER_TYPE,
+ self.ITEM_TYPE,
+ self.CONTAINER_ID,
+ adjacent,
+ can_be_empty,
)
- self.env_assist.assert_reports(
+
+ def test_success_add_and_remove(self):
+ assert_report_item_list_equal(
+ self._validate(["a1"], ["c3"], ["b2", "c3"]), []
+ )
+
+ def test_success_add_only(self):
+ assert_report_item_list_equal(self._validate(["b2"], [], ["a1"]), [])
+
+ def test_success_remove_only(self):
+ assert_report_item_list_equal(
+ self._validate([], ["b2"], ["a1", "b2"]), []
+ )
+
+ def test_add_remove_items_not_specified(self):
+ assert_report_item_list_equal(
+ self._validate([], [], ["a1", "b2", "c3"]),
[
fixture.error(
- reports.codes.STONITH_UNFENCING_FAILED,
- reason="errB",
- context=reports.dto.ReportItemContextDto(
- node=self.existing_nodes[1],
- ),
- ),
- ]
+ reports.codes.ADD_REMOVE_ITEMS_NOT_SPECIFIED,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ )
+ ],
)
- def test_corosync_targets_unable_to_connect(self):
- self._unfence_failure_common_calls()
- self.config.http.corosync.get_corosync_online_targets(
- communication_list=[
- dict(
- label=self.existing_nodes[0],
- output='{"corosync":true}',
- ),
- ]
- + [
- dict(
- label=node,
- was_connected=False,
- errno=7,
- error_msg="an error",
+ def test_add_remove_items_duplications(self):
+ assert_report_item_list_equal(
+ self._validate(["b2", "b2"], ["a1", "a1"], ["a1", "c3"]),
+ [
+ fixture.error(
+ reports.codes.ADD_REMOVE_ITEMS_DUPLICATION,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ duplicate_items_list=["a1", "b2"],
)
- for node in self.existing_nodes[1:]
- ]
+ ],
)
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(), SCSI_STONITH_ID, DEVICES_2
- ),
+
+ def test_add_items_already_in_container(self):
+ assert_report_item_list_equal(
+ self._validate(["a1", "b2"], [], ["a1", "b2", "c3"]),
+ [
+ fixture.error(
+ reports.codes.ADD_REMOVE_CANNOT_ADD_ITEMS_ALREADY_IN_THE_CONTAINER,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ item_list=["a1", "b2"],
+ ),
+ ],
)
- self.env_assist.assert_reports(
+
+ def test_remove_items_not_in_container(self):
+ assert_report_item_list_equal(
+ self._validate([], ["a1", "b2"], ["c3"]),
[
fixture.error(
- reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
- force_code=reports.codes.SKIP_OFFLINE_NODES,
- node=node,
- command="remote/status",
- reason="an error",
+ reports.codes.ADD_REMOVE_CANNOT_REMOVE_ITEMS_NOT_IN_THE_CONTAINER,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ item_list=["a1", "b2"],
)
- for node in self.existing_nodes[1:]
- ]
+ ],
)
- def test_corosync_targets_skip_offline_unfence_node_running_corosync(
- self,
- ):
- self._unfence_failure_common_calls()
- self.config.http.corosync.get_corosync_online_targets(
- communication_list=[
- dict(
- label=self.existing_nodes[0],
- output='{"corosync":true}',
+ def test_add_remove_items_at_the_same_time(self):
+ assert_report_item_list_equal(
+ self._validate(
+ ["a1", "a1", "b2", "b2"], ["b2", "b2", "a1", "a1"], ["c3"]
+ ),
+ [
+ fixture.error(
+ reports.codes.ADD_REMOVE_ITEMS_DUPLICATION,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ duplicate_items_list=["a1", "b2"],
),
- dict(
- label=self.existing_nodes[1],
- output='{"corosync":false}',
+ fixture.error(
+ reports.codes.ADD_REMOVE_CANNOT_REMOVE_ITEMS_NOT_IN_THE_CONTAINER,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ item_list=["a1", "b2"],
),
- dict(
- label=self.existing_nodes[2],
- was_connected=False,
- errno=7,
- error_msg="an error",
+ fixture.error(
+ reports.codes.ADD_REMOVE_CANNOT_ADD_AND_REMOVE_ITEMS_AT_THE_SAME_TIME,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ item_list=["a1", "b2"],
),
- ]
+ ],
)
- self.config.http.scsi.unfence_node(
- DEVICES_2,
- communication_list=[
- dict(
- label=self.existing_nodes[0],
- raw_data=json.dumps(
- dict(devices=DEVICES_2, node=self.existing_nodes[0])
- ),
+
+ def test_remove_all_items(self):
+ assert_report_item_list_equal(
+ self._validate([], ["a1", "b2"], ["a1", "b2"]),
+ [
+ fixture.error(
+ reports.codes.ADD_REMOVE_CANNOT_REMOVE_ALL_ITEMS_FROM_THE_CONTAINER,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ item_list=["a1", "b2"],
),
],
)
- self.config.env.push_cib(
- resources=fixture_scsi(devices=DEVICES_2),
- status=_fixture_status_lrm_ops(
- SCSI_STONITH_ID,
- lrm_start_ops=DEFAULT_LRM_START_OPS_UPDATED,
- lrm_monitor_ops=DEFAULT_LRM_MONITOR_OPS_UPDATED,
- ),
+
+ def test_remove_all_items_can_be_empty(self):
+ assert_report_item_list_equal(
+ self._validate([], ["a1", "b2"], ["a1", "b2"], can_be_empty=True),
+ [],
)
- stonith.update_scsi_devices(
- self.env_assist.get_env(),
- SCSI_STONITH_ID,
- DEVICES_2,
- force_flags=[reports.codes.SKIP_OFFLINE_NODES],
+
+ def test_remove_all_items_and_add_new_one(self):
+ assert_report_item_list_equal(
+ self._validate(["c3"], ["a1", "b2"], ["a1", "b2"]),
+ [],
)
- self.env_assist.assert_reports(
+
+ def test_missing_adjacent_item(self):
+ assert_report_item_list_equal(
+ self._validate(["a1", "b2"], [], ["c3"], adjacent="d4"),
[
- fixture.warn(
- reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
- node=self.existing_nodes[2],
- command="remote/status",
- reason="an error",
+ fixture.error(
+ reports.codes.ADD_REMOVE_ADJACENT_ITEM_NOT_IN_THE_CONTAINER,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ adjacent_item_id="d4",
),
- ]
+ ],
)
- def test_corosync_targets_unable_to_perform_unfencing_operation(
- self,
- ):
- self._unfence_failure_common_calls()
- self.config.http.corosync.get_corosync_online_targets(
- communication_list=[
- dict(
- label=self.existing_nodes[0],
- was_connected=False,
- errno=7,
- error_msg="an error",
- ),
- dict(
- label=self.existing_nodes[1],
- was_connected=False,
- errno=7,
- error_msg="an error",
- ),
- dict(
- label=self.existing_nodes[2],
- output='{"corosync":false}',
+ def test_adjacent_item_in_add_list(self):
+ assert_report_item_list_equal(
+ self._validate(["a1", "b2"], [], ["a1"], adjacent="a1"),
+ [
+ fixture.error(
+ reports.codes.ADD_REMOVE_CANNOT_PUT_ITEM_NEXT_TO_ITSELF,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ adjacent_item_id="a1",
),
- ]
- )
- self.config.http.scsi.unfence_node(DEVICES_2, communication_list=[])
- self.env_assist.assert_raise_library_error(
- lambda: stonith.update_scsi_devices(
- self.env_assist.get_env(),
- SCSI_STONITH_ID,
- DEVICES_2,
- force_flags=[reports.codes.SKIP_OFFLINE_NODES],
- ),
+ ],
)
- self.env_assist.assert_reports(
+
+ def test_adjacent_item_without_add_list(self):
+ assert_report_item_list_equal(
+ self._validate([], ["b2"], ["a1", "b2"], adjacent="a1"),
[
- fixture.warn(
- reports.codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT,
- node=node,
- command="remote/status",
- reason="an error",
- )
- for node in self.existing_nodes[0:2]
- ]
- + [
fixture.error(
- reports.codes.UNABLE_TO_PERFORM_OPERATION_ON_ANY_NODE,
+ reports.codes.ADD_REMOVE_CANNOT_SPECIFY_ADJACENT_ITEM_WITHOUT_ITEMS_TO_ADD,
+ container_type=self.CONTAINER_TYPE,
+ item_type=self.ITEM_TYPE,
+ container_id=self.CONTAINER_ID,
+ adjacent_item_id="a1",
),
- ]
+ ],
)
diff --git a/pcsd/capabilities.xml b/pcsd/capabilities.xml
index 745b05ad..58ebcf0f 100644
--- a/pcsd/capabilities.xml
+++ b/pcsd/capabilities.xml
@@ -1884,6 +1884,14 @@
pcs commands: stonith update-scsi-devices
</description>
</capability>
+ <capability id="pcmk.stonith.update.scsi-devices.add-remove" in-pcs="1" in-pcsd="0">
+ <description>
+ Update scsi fencing devices without affecting other resources using
+ add/remove cli syntax.
+
+ pcs commands: stonith update-scsi-devices
+ </description>
+ </capability>
<capability id="pcmk.stonith.scsi-unfence-node" in-pcs="0" in-pcsd="1">
<description>
Unfence scsi devices on a cluster node.
--
2.31.1