diff --git a/pcs.spec b/pcs.spec index a26ae05..2b62f2a 100644 --- a/pcs.spec +++ b/pcs.spec @@ -1,6 +1,6 @@ Name: pcs Version: 0.11.7 -Release: 8 +Release: 9 License: GPL-2.0-only AND Apache-2.0 AND MIT AND BSD-3-Clause AND (BSD-2-Clause OR Ruby) AND (BSD-2-Clause OR GPL-2.0-or-later) URL: https://github.com/ClusterLabs/pcs Group: System Environment/Base @@ -44,6 +44,7 @@ Patch4: Fix-pcsd-ruby.patch Patch5: update-crm_mon-schemas-for-tests.patch Patch6: add-dtos-and-converting-functions-for-resources-stat.patch Patch7: fixes-after-review.patch +Patch8: store-clone-instance-id-in-resource-status-dtos.patch # ui patches: >200 # Patch201: bzNUMBER-01-name.patch @@ -405,6 +406,9 @@ run_all_tests %license pyagentx_LICENSE.txt %changelog +* Tue Mar 26 2024 zouzhimin - 0.11.7-9 +- Add dtos for resources status + * Mon Mar 25 2024 zouzhimin - 0.11.7-8 - fixes after review diff --git a/store-clone-instance-id-in-resource-status-dtos.patch b/store-clone-instance-id-in-resource-status-dtos.patch new file mode 100644 index 0000000..9c1b658 --- /dev/null +++ b/store-clone-instance-id-in-resource-status-dtos.patch @@ -0,0 +1,812 @@ +From 7c56001aa76c4a5f69f29b328061c419c7ce856b Mon Sep 17 00:00:00 2001 +From: Peter Romancik +Date: Thu, 1 Feb 2024 17:17:40 +0100 +Subject: [PATCH 1/2] further fixes after review + +--- + pcs/common/reports/codes.py | 2 +- + pcs/common/reports/messages.py | 8 +- + pcs/lib/pacemaker/status.py | 85 ++++++++++--------- + .../tier0/common/reports/test_messages.py | 14 +-- + pcs_test/tier0/lib/commands/test_status.py | 4 +- + pcs_test/tier0/lib/pacemaker/test_status.py | 68 ++++++++------- + 6 files changed, 97 insertions(+), 84 deletions(-) + +diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py +index f9614331..e967d0b1 100644 +--- a/pcs/common/reports/codes.py ++++ b/pcs/common/reports/codes.py +@@ -50,7 +50,7 @@ AGENT_SELF_VALIDATION_SKIPPED_UPDATED_RESOURCE_MISCONFIGURED = M( + ) + AGENT_SELF_VALIDATION_RESULT = M("AGENT_SELF_VALIDATION_RESULT") + BAD_CLUSTER_STATE_FORMAT = M("BAD_CLUSTER_STATE_FORMAT") +-BAD_CLUSTER_STATE = M("BAD_CLUSTER_STATE") ++BAD_CLUSTER_STATE_DATA = M("BAD_CLUSTER_STATE_DATA") + BOOTH_ADDRESS_DUPLICATION = M("BOOTH_ADDRESS_DUPLICATION") + BOOTH_ALREADY_IN_CIB = M("BOOTH_ALREADY_IN_CIB") + BOOTH_AUTHFILE_NOT_USED = M("BOOTH_AUTHFILE_NOT_USED") +diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py +index 8b9bc63e..53f15170 100644 +--- a/pcs/common/reports/messages.py ++++ b/pcs/common/reports/messages.py +@@ -3277,7 +3277,7 @@ class BadClusterStateFormat(ReportItemMessage): + + + @dataclass(frozen=True) +-class BadClusterState(ReportItemMessage): ++class BadClusterStateData(ReportItemMessage): + """ + crm_mon xml output is invalid despite conforming to the schema + +@@ -3285,13 +3285,13 @@ class BadClusterState(ReportItemMessage): + """ + + reason: Optional[str] = None +- _code = codes.BAD_CLUSTER_STATE ++ _code = codes.BAD_CLUSTER_STATE_DATA + + @property + def message(self) -> str: + return ( + "Cannot load cluster status, xml does not describe valid cluster " +- f"status{format_optional(self.reason, template=': {}')}." ++ f"status{format_optional(self.reason, template=': {}')}" + ) + + +@@ -3314,7 +3314,7 @@ class ClusterStatusBundleMemberIdAsImplicit(ReportItemMessage): + return ( + "Skipping bundle '{bundle_id}': {resource_word} " + "{bad_ids} {has} the same id as some of the " +- "implicit bundle resources." ++ "implicit bundle resources" + ).format( + bundle_id=self.bundle_id, + resource_word=format_plural(self.bad_ids, "resource"), +diff --git a/pcs/lib/pacemaker/status.py b/pcs/lib/pacemaker/status.py +index a86ede55..deb8aa0d 100644 +--- a/pcs/lib/pacemaker/status.py ++++ b/pcs/lib/pacemaker/status.py +@@ -2,7 +2,6 @@ from collections import Counter + from typing import ( + Optional, + Sequence, +- Union, + cast, + ) + +@@ -60,11 +59,13 @@ class UnexpectedMemberError(ClusterStatusParsingError): + resource_id: str, + resource_type: str, + member_id: str, ++ member_type: str, + expected_types: list[str], + ): + super().__init__(resource_id) + self.resource_type = resource_type + self.member_id = member_id ++ self.member_type = member_type + self.expected_types = expected_types + + +@@ -106,46 +107,44 @@ def cluster_status_parsing_error_to_report( + ) -> reports.ReportItem: + reason = "" + if isinstance(e, EmptyResourceIdError): +- reason = "Resource with empty id." ++ reason = "Resource with an empty id" + elif isinstance(e, EmptyNodeNameError): + reason = ( +- f"Resource with id '{e.resource_id}' contains node with empty name." ++ f"Resource '{e.resource_id}' contains a node with an empty name" + ) + elif isinstance(e, UnknownPcmkRoleError): + reason = ( +- f"Resource with id '{e.resource_id}' contains unknown " +- f"pcmk role '{e.role}'." ++ f"Resource '{e.resource_id}' contains an unknown " ++ f"role '{e.role}'" + ) + elif isinstance(e, UnexpectedMemberError): + reason = ( +- f"Unexpected resource '{e.member_id}' inside of resource " +- f"'{e.resource_id}' of type '{e.resource_type}'. " +- f"Only resources of type {format_list(e.expected_types, '|')} " +- f"can be in {e.resource_type}." ++ f"Unexpected resource '{e.member_id}' of type '{e.member_type}' " ++ f"inside of resource '{e.resource_id}' of type '{e.resource_type}'." ++ f" Only resources of type {format_list(e.expected_types)} " ++ f"can be in a {e.resource_type}" + ) + + elif isinstance(e, MixedMembersError): +- reason = ( +- f"Primitive and group members mixed in clone '{e.resource_id}'." +- ) ++ reason = f"Primitive and group members mixed in clone '{e.resource_id}'" + elif isinstance(e, DifferentMemberIdsError): +- reason = f"Members with different ids in resource '{e.resource_id}'." ++ reason = f"Members with different ids in clone '{e.resource_id}'" + elif isinstance(e, BundleReplicaMissingImplicitResourceError): + reason = ( + f"Replica '{e.replica_id}' of bundle '{e.resource_id}' " +- f"is missing implicit {e.implicit_type} resource." ++ f"is missing implicit {e.implicit_type} resource" + ) + elif isinstance(e, BundleReplicaInvalidMemberCountError): + reason = ( + f"Replica '{e.replica_id}' of bundle '{e.resource_id}' has " +- "invalid number of members." ++ "invalid number of members" + ) + elif isinstance(e, BundleDifferentReplicas): +- reason = f"Replicas of bundle '{e.resource_id}' are not the same." ++ reason = f"Replicas of bundle '{e.resource_id}' are not the same" + + return reports.ReportItem( + reports.ReportItemSeverity.error(), +- reports.messages.BadClusterState(reason), ++ reports.messages.BadClusterStateData(reason), + ) + + +@@ -160,7 +159,7 @@ def _primitive_to_dto( + target_role = _get_target_role(primitive_el) + + node_names = [ +- str(node.get("name")) for node in primitive_el.iterfind("node") ++ str(node.attrib["name"]) for node in primitive_el.iterfind("node") + ] + + if node_names and any(not name for name in node_names): +@@ -168,7 +167,7 @@ def _primitive_to_dto( + + return PrimitiveStatusDto( + resource_id, +- str(primitive_el.get("resource_agent")), ++ str(primitive_el.attrib["resource_agent"]), + role, + target_role, + is_true(primitive_el.get("active", "false")), +@@ -179,7 +178,7 @@ def _primitive_to_dto( + is_true(primitive_el.get("failed", "false")), + is_true(primitive_el.get("managed", "false")), + is_true(primitive_el.get("failure_ignored", "false")), +- [str(node.get("name")) for node in primitive_el.iterfind("node")], ++ node_names, + primitive_el.get("pending"), + primitive_el.get("locked_to"), + ) +@@ -197,7 +196,11 @@ def _group_to_dto( + member_list.append(_primitive_to_dto(member, remove_clone_suffix)) + else: + raise UnexpectedMemberError( +- group_id, "group", str(member.get("id")), ["primitive"] ++ group_id, ++ "group", ++ str(member.attrib["id"]), ++ member.tag, ++ ["primitive"], + ) + + return GroupStatusDto( +@@ -228,29 +231,28 @@ def _clone_to_dto( + group_list.append(_group_to_dto(member, is_unique)) + else: + raise UnexpectedMemberError( +- clone_id, "clone", str(member.get("id")), ["primitive", "group"] ++ clone_id, ++ "clone", ++ str(member.attrib["id"]), ++ member.tag, ++ ["primitive", "group"], + ) + + if primitive_list and group_list: + raise MixedMembersError(clone_id) + +- instance_list: Union[list[PrimitiveStatusDto], list[GroupStatusDto]] + if primitive_list: + if len(set(res.resource_id for res in primitive_list)) > 1: + raise DifferentMemberIdsError(clone_id) +- instance_list = primitive_list +- else: ++ if group_list: + group_ids = set(group.resource_id for group in group_list) + children_ids = set( + tuple(child.resource_id for child in group.members) + for group in group_list + ) +- + if len(group_ids) > 1 or len(children_ids) > 1: + raise DifferentMemberIdsError(clone_id) + +- instance_list = group_list +- + return CloneStatusDto( + clone_id, + is_true(clone_el.get("multi_state", "false")), +@@ -262,7 +264,7 @@ def _clone_to_dto( + is_true(clone_el.get("failed", "false")), + is_true(clone_el.get("failure_ignored", "false")), + target_role, +- instance_list, ++ primitive_list or group_list, + ) + + +@@ -270,7 +272,7 @@ def _bundle_to_dto( + bundle_el: _Element, _remove_clone_suffix: bool = False + ) -> BundleStatusDto: + bundle_id = _get_resource_id(bundle_el) +- bundle_type = str(bundle_el.get("type")) ++ bundle_type = str(bundle_el.attrib["type"]) + + replica_list = [ + _replica_to_dto(replica, bundle_id, bundle_type) +@@ -283,7 +285,7 @@ def _bundle_to_dto( + return BundleStatusDto( + bundle_id, + bundle_type, +- str(bundle_el.get("image")), ++ str(bundle_el.attrib["image"]), + is_true(bundle_el.get("unique", "false")), + is_true(bundle_el.get("maintenance", "false")), + bundle_el.get("description"), +@@ -302,17 +304,18 @@ class ClusterStatusParser: + } + + def __init__(self, status: _Element): +- self.status = status +- self.warnings: reports.ReportItemList = [] ++ """ ++ status -- xml element from crm_mon xml, validated using the appropriate ++ rng schema ++ """ ++ self._status = status ++ self._warnings: reports.ReportItemList = [] + + def status_xml_to_dto(self) -> ResourcesStatusDto: + """ + Return dto containing status of configured resources in the cluster +- +- status -- status xml document from crm_mon, validated using +- the appropriate rng schema + """ +- resource_list = cast(list[_Element], self.status.xpath("resources/*")) ++ resource_list = cast(list[_Element], self._status.xpath("resources/*")) + + resource_dto_list = [] + for resource in resource_list: +@@ -328,7 +331,7 @@ class ClusterStatusParser: + # the implicitly created resource. + # We only skip such bundles while still providing status of the + # other resources. +- self.warnings.append( ++ self._warnings.append( + reports.ReportItem.warning( + reports.messages.ClusterStatusBundleMemberIdAsImplicit( + e.bundle_id, e.bad_ids +@@ -339,11 +342,11 @@ class ClusterStatusParser: + return ResourcesStatusDto(resource_dto_list) + + def get_warnings(self) -> reports.ReportItemList: +- return self.warnings ++ return self._warnings + + + def _get_resource_id(resource: _Element) -> str: +- resource_id = resource.get("id") ++ resource_id = resource.attrib["id"] + if not resource_id: + raise EmptyResourceIdError() + return str(resource_id) +@@ -374,7 +377,7 @@ def _remove_clone_suffix(resource_id: str) -> str: + def _replica_to_dto( + replica_el: _Element, bundle_id: str, bundle_type: str + ) -> BundleReplicaStatusDto: +- replica_id = str(replica_el.get("id")) ++ replica_id = str(replica_el.attrib["id"]) + + resource_list = [ + _primitive_to_dto(resource) +diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py +index 48eb730c..0ca95920 100644 +--- a/pcs_test/tier0/common/reports/test_messages.py ++++ b/pcs_test/tier0/common/reports/test_messages.py +@@ -2195,23 +2195,23 @@ class BadClusterStateFormat(NameBuildTest): + ) + + +-class BadClusterState(NameBuildTest): ++class BadClusterStateData(NameBuildTest): + def test_no_reason(self): + self.assert_message_from_report( + ( + "Cannot load cluster status, xml does not describe " +- "valid cluster status." ++ "valid cluster status" + ), +- reports.BadClusterState(), ++ reports.BadClusterStateData(), + ) + + def test_reason(self): + self.assert_message_from_report( + ( + "Cannot load cluster status, xml does not describe " +- "valid cluster status: sample reason." ++ "valid cluster status: sample reason" + ), +- reports.BadClusterState("sample reason"), ++ reports.BadClusterStateData("sample reason"), + ) + + +@@ -5843,7 +5843,7 @@ class ClusterStatusBundleMemberIdAsImplicit(NameBuildTest): + self.assert_message_from_report( + ( + "Skipping bundle 'resource-bundle': resource 'resource' has " +- "the same id as some of the implicit bundle resources." ++ "the same id as some of the implicit bundle resources" + ), + reports.ClusterStatusBundleMemberIdAsImplicit( + "resource-bundle", ["resource"] +@@ -5855,7 +5855,7 @@ class ClusterStatusBundleMemberIdAsImplicit(NameBuildTest): + ( + "Skipping bundle 'resource-bundle': resources 'resource-0', " + "'resource-1' have the same id as some of the implicit bundle " +- "resources." ++ "resources" + ), + reports.ClusterStatusBundleMemberIdAsImplicit( + "resource-bundle", ["resource-0", "resource-1"] +diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py +index 3b6b7665..b12e9531 100644 +--- a/pcs_test/tier0/lib/commands/test_status.py ++++ b/pcs_test/tier0/lib/commands/test_status.py +@@ -1342,8 +1342,8 @@ class ResourcesStatus(TestCase): + lambda: status.resources_status(self.env_assist.get_env()), + [ + fixture.error( +- report_codes.BAD_CLUSTER_STATE, +- reason="Resource with id 'R7' contains unknown pcmk role 'NotPcmkRole'.", ++ report_codes.BAD_CLUSTER_STATE_DATA, ++ reason="Resource 'R7' contains an unknown role 'NotPcmkRole'", + ), + ], + False, +diff --git a/pcs_test/tier0/lib/pacemaker/test_status.py b/pcs_test/tier0/lib/pacemaker/test_status.py +index 778e97a6..ced1a47e 100644 +--- a/pcs_test/tier0/lib/pacemaker/test_status.py ++++ b/pcs_test/tier0/lib/pacemaker/test_status.py +@@ -12,6 +12,7 @@ from pcs.common import reports + from pcs.common.const import ( + PCMK_ROLE_STARTED, + PCMK_ROLES, ++ PCMK_STATUS_ROLE_PROMOTED, + PCMK_STATUS_ROLE_STARTED, + PCMK_STATUS_ROLE_STOPPED, + PCMK_STATUS_ROLE_UNPROMOTED, +@@ -334,8 +335,8 @@ class TestParsingErrorToReport(TestCase): + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, +- reason="Resource with empty id.", ++ reports.codes.BAD_CLUSTER_STATE_DATA, ++ reason="Resource with an empty id", + ), + ) + +@@ -346,8 +347,8 @@ class TestParsingErrorToReport(TestCase): + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, +- reason="Resource with id 'resource' contains node with empty name.", ++ reports.codes.BAD_CLUSTER_STATE_DATA, ++ reason="Resource 'resource' contains a node with an empty name", + ), + ) + +@@ -358,25 +359,25 @@ class TestParsingErrorToReport(TestCase): + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, +- reason="Resource with id 'resource' contains unknown pcmk role 'NotPcmkRole'.", ++ reports.codes.BAD_CLUSTER_STATE_DATA, ++ reason="Resource 'resource' contains an unknown role 'NotPcmkRole'", + ), + ) + + def test_unexpected_member_group(self): + report = status.cluster_status_parsing_error_to_report( + status.UnexpectedMemberError( +- "resource", "group", "member", ["primitive"] ++ "resource", "group", "member", "bundle", ["primitive"] + ) + ) + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, ++ reports.codes.BAD_CLUSTER_STATE_DATA, + reason=( +- "Unexpected resource 'member' inside of resource " +- "'resource' of type 'group'. Only resources of type " +- "'primitive' can be in group." ++ "Unexpected resource 'member' of type 'bundle' inside of " ++ "resource 'resource' of type 'group'. Only resources of " ++ "type 'primitive' can be in a group" + ), + ), + ) +@@ -384,17 +385,17 @@ class TestParsingErrorToReport(TestCase): + def test_unexpected_member_clone(self): + report = status.cluster_status_parsing_error_to_report( + status.UnexpectedMemberError( +- "resource", "clone", "member", ["primitive", "group"] ++ "resource", "clone", "member", "bundle", ["primitive", "group"] + ) + ) + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, ++ reports.codes.BAD_CLUSTER_STATE_DATA, + reason=( +- "Unexpected resource 'member' inside of resource " +- "'resource' of type 'clone'. Only resources of type " +- "'group'|'primitive' can be in clone." ++ "Unexpected resource 'member' of type 'bundle' inside of " ++ "resource 'resource' of type 'clone'. Only resources of " ++ "type 'group', 'primitive' can be in a clone" + ), + ), + ) +@@ -406,8 +407,8 @@ class TestParsingErrorToReport(TestCase): + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, +- reason="Primitive and group members mixed in clone 'resource'.", ++ reports.codes.BAD_CLUSTER_STATE_DATA, ++ reason="Primitive and group members mixed in clone 'resource'", + ), + ) + +@@ -418,8 +419,8 @@ class TestParsingErrorToReport(TestCase): + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, +- reason="Members with different ids in resource 'resource'.", ++ reports.codes.BAD_CLUSTER_STATE_DATA, ++ reason="Members with different ids in clone 'resource'", + ), + ) + +@@ -432,8 +433,8 @@ class TestParsingErrorToReport(TestCase): + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, +- reason="Replica '0' of bundle 'resource' is missing implicit container resource.", ++ reports.codes.BAD_CLUSTER_STATE_DATA, ++ reason="Replica '0' of bundle 'resource' is missing implicit container resource", + ), + ) + +@@ -444,8 +445,8 @@ class TestParsingErrorToReport(TestCase): + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, +- reason="Replica '0' of bundle 'resource' has invalid number of members.", ++ reports.codes.BAD_CLUSTER_STATE_DATA, ++ reason="Replica '0' of bundle 'resource' has invalid number of members", + ), + ) + +@@ -456,8 +457,8 @@ class TestParsingErrorToReport(TestCase): + assert_report_item_equal( + report, + fixture.error( +- reports.codes.BAD_CLUSTER_STATE, +- reason="Replicas of bundle 'resource' are not the same.", ++ reports.codes.BAD_CLUSTER_STATE_DATA, ++ reason="Replicas of bundle 'resource' are not the same", + ), + ) + +@@ -549,6 +550,7 @@ class TestPrimitiveStatusToDto(TestCase): + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._primitive_to_dto(primitive_xml) + self.assertEqual(cm.exception.resource_id, "resource") ++ self.assertEqual(cm.exception.role, "NotPcmkRole") + + def test_target_role(self): + for role in PCMK_ROLES: +@@ -573,6 +575,7 @@ class TestPrimitiveStatusToDto(TestCase): + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._primitive_to_dto(primitive_xml) + self.assertEqual(cm.exception.resource_id, "resource") ++ self.assertEqual(cm.exception.role, value) + + + class TestGroupStatusToDto(TestCase): +@@ -695,7 +698,11 @@ class TestGroupStatusToDto(TestCase): + with self.assertRaises(status.UnexpectedMemberError) as cm: + status._group_to_dto(group_xml) + self.assertEqual(cm.exception.resource_id, "outer-group") ++ self.assertEqual(cm.exception.resource_type, "group") + self.assertEqual(cm.exception.member_id, resource_id) ++ self.assertEqual( ++ cm.exception.member_type, resource_id.split("-")[1] ++ ) + self.assertEqual(cm.exception.expected_types, ["primitive"]) + + def test_remove_clone_suffix(self): +@@ -796,7 +803,7 @@ class TestCloneStatusToDto(TestCase): + fixture_clone_xml( + multi_state=True, + instances=[ +- fixture_primitive_xml(role=PCMK_STATUS_ROLE_UNPROMOTED), ++ fixture_primitive_xml(role=PCMK_STATUS_ROLE_PROMOTED), + fixture_primitive_xml( + role=PCMK_STATUS_ROLE_UNPROMOTED, node_names=["node2"] + ), +@@ -810,7 +817,7 @@ class TestCloneStatusToDto(TestCase): + fixture_clone_dto( + multi_state=True, + instances=[ +- fixture_primitive_dto(role=PCMK_STATUS_ROLE_UNPROMOTED), ++ fixture_primitive_dto(role=PCMK_STATUS_ROLE_PROMOTED), + fixture_primitive_dto( + role=PCMK_STATUS_ROLE_UNPROMOTED, node_names=["node2"] + ), +@@ -1003,9 +1010,12 @@ class TestCloneStatusToDto(TestCase): + + with self.assertRaises(status.UnexpectedMemberError) as cm: + status._clone_to_dto(clone_xml) +- + self.assertEqual(cm.exception.resource_id, "outer-clone") ++ self.assertEqual(cm.exception.resource_type, "clone") + self.assertEqual(cm.exception.member_id, resource_id) ++ self.assertEqual( ++ cm.exception.member_type, resource_id.split("-")[1] ++ ) + self.assertEqual( + cm.exception.expected_types, ["primitive", "group"] + ) +-- +2.25.1 + +From c32249a39ef262e3f2106eb8ca01b6efb8e74707 Mon Sep 17 00:00:00 2001 +From: Peter Romancik +Date: Thu, 1 Feb 2024 17:45:20 +0100 +Subject: [PATCH 2/2] store clone instance id in resource status dtos + +--- + pcs/common/status_dto.py | 2 ++ + pcs/lib/pacemaker/status.py | 19 +++++++---- + pcs_test/tier0/lib/commands/test_status.py | 3 ++ + pcs_test/tier0/lib/pacemaker/test_status.py | 36 ++++++++++++++++----- + 4 files changed, 46 insertions(+), 14 deletions(-) + +diff --git a/pcs/common/status_dto.py b/pcs/common/status_dto.py +index dcc94eca..240ff930 100644 +--- a/pcs/common/status_dto.py ++++ b/pcs/common/status_dto.py +@@ -16,6 +16,7 @@ from pcs.common.interface.dto import DataTransferObject + class PrimitiveStatusDto(DataTransferObject): + # pylint: disable=too-many-instance-attributes + resource_id: str ++ clone_instance_id: Optional[str] + resource_agent: str + role: PcmkStatusRoleType + target_role: Optional[PcmkRoleType] +@@ -35,6 +36,7 @@ class PrimitiveStatusDto(DataTransferObject): + @dataclass(frozen=True) + class GroupStatusDto(DataTransferObject): + resource_id: str ++ clone_instance_id: Optional[str] + maintenance: bool + description: Optional[str] + managed: bool +diff --git a/pcs/lib/pacemaker/status.py b/pcs/lib/pacemaker/status.py +index deb8aa0d..6b37d6cb 100644 +--- a/pcs/lib/pacemaker/status.py ++++ b/pcs/lib/pacemaker/status.py +@@ -152,8 +152,9 @@ def _primitive_to_dto( + primitive_el: _Element, remove_clone_suffix: bool = False + ) -> PrimitiveStatusDto: + resource_id = _get_resource_id(primitive_el) ++ clone_suffix = None + if remove_clone_suffix: +- resource_id = _remove_clone_suffix(resource_id) ++ resource_id, clone_suffix = _remove_clone_suffix(resource_id) + + role = _get_role(primitive_el) + target_role = _get_target_role(primitive_el) +@@ -167,6 +168,7 @@ def _primitive_to_dto( + + return PrimitiveStatusDto( + resource_id, ++ clone_suffix, + str(primitive_el.attrib["resource_agent"]), + role, + target_role, +@@ -187,8 +189,11 @@ def _primitive_to_dto( + def _group_to_dto( + group_el: _Element, remove_clone_suffix: bool = False + ) -> GroupStatusDto: +- # clone suffix is added even when the clone is non unique +- group_id = _remove_clone_suffix(_get_resource_id(group_el)) ++ # clone instance id present even when the clone is non unique ++ group_id, clone_instance_id = _remove_clone_suffix( ++ _get_resource_id(group_el) ++ ) ++ + member_list = [] + + for member in group_el: +@@ -205,6 +210,7 @@ def _group_to_dto( + + return GroupStatusDto( + group_id, ++ clone_instance_id, + is_true(group_el.get("maintenance", "false")), + group_el.get("description"), + is_true(group_el.get("managed", "false")), +@@ -368,10 +374,11 @@ def _get_target_role(resource: _Element) -> Optional[PcmkRoleType]: + return PcmkRoleType(target_role) + + +-def _remove_clone_suffix(resource_id: str) -> str: ++def _remove_clone_suffix(resource_id: str) -> tuple[str, Optional[str]]: + if ":" in resource_id: +- return resource_id.rsplit(":", 1)[0] +- return resource_id ++ resource_id, clone_suffix = resource_id.rsplit(":", 1) ++ return resource_id, clone_suffix ++ return resource_id, None + + + def _replica_to_dto( +diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py +index b12e9531..c7c808a3 100644 +--- a/pcs_test/tier0/lib/commands/test_status.py ++++ b/pcs_test/tier0/lib/commands/test_status.py +@@ -1280,6 +1280,7 @@ def _fixture_primitive_resource_dto( + ) -> PrimitiveStatusDto: + return PrimitiveStatusDto( + resource_id=resource_id, ++ clone_instance_id=None, + resource_agent=resource_agent, + role=PCMK_STATUS_ROLE_STOPPED, + target_role=target_role, +@@ -1448,6 +1449,7 @@ class ResourcesStatus(TestCase): + ), + GroupStatusDto( + resource_id="G2", ++ clone_instance_id=None, + maintenance=False, + description=None, + managed=True, +@@ -1475,6 +1477,7 @@ class ResourcesStatus(TestCase): + instances=[ + GroupStatusDto( + resource_id="G1", ++ clone_instance_id="0", + maintenance=False, + description=None, + managed=True, +diff --git a/pcs_test/tier0/lib/pacemaker/test_status.py b/pcs_test/tier0/lib/pacemaker/test_status.py +index ced1a47e..a852d45b 100644 +--- a/pcs_test/tier0/lib/pacemaker/test_status.py ++++ b/pcs_test/tier0/lib/pacemaker/test_status.py +@@ -85,6 +85,7 @@ def fixture_primitive_xml( + + def fixture_primitive_dto( + resource_id: str = "resource", ++ clone_instance_id: Optional[str] = None, + resource_agent: str = "ocf:heartbeat:Dummy", + role: PcmkStatusRoleType = PCMK_STATUS_ROLE_STARTED, + target_role: Optional[str] = None, +@@ -94,6 +95,7 @@ def fixture_primitive_dto( + ) -> PrimitiveStatusDto: + return PrimitiveStatusDto( + resource_id, ++ clone_instance_id, + resource_agent, + role, + target_role, +@@ -136,11 +138,13 @@ def fixture_group_xml( + + def fixture_group_dto( + resource_id: str = "resource-group", ++ clone_instance_id: Optional[str] = None, + description: Optional[str] = None, + members: Sequence[PrimitiveStatusDto] = (), + ) -> GroupStatusDto: + return GroupStatusDto( + resource_id, ++ clone_instance_id, + maintenance=False, + description=description, + managed=True, +@@ -506,7 +510,7 @@ class TestPrimitiveStatusToDto(TestCase): + + result = status._primitive_to_dto(primitive_xml, True) + +- self.assertEqual(result, fixture_primitive_dto()) ++ self.assertEqual(result, fixture_primitive_dto(clone_instance_id="0")) + + def test_running_on_multiple_nodes(self): + primitive_xml = etree.fromstring( +@@ -716,7 +720,10 @@ class TestGroupStatusToDto(TestCase): + result = status._group_to_dto(group_xml, True) + self.assertEqual( + result, +- fixture_group_dto(members=[fixture_primitive_dto()]), ++ fixture_group_dto( ++ clone_instance_id="0", ++ members=[fixture_primitive_dto(clone_instance_id="0")], ++ ), + ) + + +@@ -792,8 +799,10 @@ class TestCloneStatusToDto(TestCase): + fixture_clone_dto( + unique=True, + instances=[ +- fixture_primitive_dto(), +- fixture_primitive_dto(node_names=["node2"]), ++ fixture_primitive_dto(clone_instance_id="0"), ++ fixture_primitive_dto( ++ clone_instance_id="1", node_names=["node2"] ++ ), + ], + ), + ) +@@ -886,9 +895,12 @@ class TestCloneStatusToDto(TestCase): + result, + fixture_clone_dto( + instances=[ +- fixture_group_dto(members=[fixture_primitive_dto()]), + fixture_group_dto( +- members=[fixture_primitive_dto(node_names=["node2"])] ++ clone_instance_id="0", members=[fixture_primitive_dto()] ++ ), ++ fixture_group_dto( ++ clone_instance_id="1", ++ members=[fixture_primitive_dto(node_names=["node2"])], + ), + ], + ), +@@ -923,9 +935,17 @@ class TestCloneStatusToDto(TestCase): + fixture_clone_dto( + unique=True, + instances=[ +- fixture_group_dto(members=[fixture_primitive_dto()]), + fixture_group_dto( +- members=[fixture_primitive_dto(node_names=["node2"])] ++ clone_instance_id="0", ++ members=[fixture_primitive_dto(clone_instance_id="0")], ++ ), ++ fixture_group_dto( ++ clone_instance_id="1", ++ members=[ ++ fixture_primitive_dto( ++ clone_instance_id="1", node_names=["node2"] ++ ) ++ ], + ), + ], + ), +-- +2.25.1 +