From 75a8e52584a71087ae734c427870c9ea8d7935c7 Mon Sep 17 00:00:00 2001 From: Peter Romancik Date: Tue, 30 Jan 2024 18:02:50 +0100 Subject: [PATCH] fixes after review --- pcs/common/const.py | 2 - pcs/common/reports/codes.py | 20 +- pcs/common/reports/messages.py | 187 +--- pcs/lib/commands/status.py | 16 +- pcs/lib/pacemaker/status.py | 527 ++++++----- pcs_test/resources/crm_mon.all_resources.xml | 75 +- .../tier0/common/reports/test_messages.py | 155 +--- pcs_test/tier0/lib/commands/test_status.py | 303 ++++-- pcs_test/tier0/lib/pacemaker/test_status.py | 865 ++++++++---------- 9 files changed, 968 insertions(+), 1182 deletions(-) diff --git a/pcs/common/const.py b/pcs/common/const.py index 32175677..00d1b7e7 100644 --- a/pcs/common/const.py +++ b/pcs/common/const.py @@ -14,14 +14,12 @@ PCMK_ROLE_PROMOTED = PcmkRoleType("Promoted") PCMK_ROLE_UNPROMOTED = PcmkRoleType("Unpromoted") PCMK_ROLE_PROMOTED_LEGACY = PcmkRoleType("Master") PCMK_ROLE_UNPROMOTED_LEGACY = PcmkRoleType("Slave") -PCMK_ROLE_UNKNOWN = PcmkRoleType("Unknown") PCMK_STATUS_ROLE_STARTED = PcmkStatusRoleType("Started") PCMK_STATUS_ROLE_STOPPED = PcmkStatusRoleType("Stopped") PCMK_STATUS_ROLE_PROMOTED = PcmkStatusRoleType("Promoted") PCMK_STATUS_ROLE_UNPROMOTED = PcmkStatusRoleType("Unpromoted") PCMK_STATUS_ROLE_STARTING = PcmkStatusRoleType("Starting") PCMK_STATUS_ROLE_STOPPING = PcmkStatusRoleType("Stopping") -PCMK_STATUS_ROLE_UNKNOWN = PcmkStatusRoleType("Unknown") PCMK_ON_FAIL_ACTION_IGNORE = PcmkOnFailAction("ignore") PCMK_ON_FAIL_ACTION_BLOCK = PcmkOnFailAction("block") PCMK_ON_FAIL_ACTION_DEMOTE = PcmkOnFailAction("demote") diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py index 417a3f4a..f9614331 100644 --- a/pcs/common/reports/codes.py +++ b/pcs/common/reports/codes.py @@ -50,6 +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") 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") @@ -156,28 +157,9 @@ CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES = M( CLUSTER_SETUP_SUCCESS = M("CLUSTER_SETUP_SUCCESS") CLUSTER_START_STARTED = M("CLUSTER_START_STARTED") CLUSTER_START_SUCCESS = M("CLUSTER_START_SUCCESS") -CLUSTER_STATUS_BUNDLE_DIFFERENT_REPLICAS = M( - "CLUSTER_STATUS_BUNDLE_DIFFERENT_REPLICAS" -) CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT = M( "CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT" ) -CLUSTER_STATUS_BUNDLE_REPLICA_INVALID_COUNT = M( - "CLUSTER_STATUS_BUNDLE_REPLICA_INVALID_COUNT" -) -CLUSTER_STATUS_BUNDLE_REPLICA_MISSING_REMOTE = M( - "CLUSTER_STATUS_BUNDLE_REPLICA_MISSING_REMOTE" -) -CLUSTER_STATUS_BUNDLE_REPLICA_NO_CONTAINER = M( - "CLUSTER_STATUS_BUNDLE_REPLICA_NO_CONTAINER" -) -CLUSTER_STATUS_CLONE_MEMBERS_DIFFERENT_IDS = M( - "CLUSTER_STATUS_CLONE_MEMBERS_DIFFERENT_IDS" -) -CLUSTER_STATUS_CLONE_MIXED_MEMBERS = M("CLUSTER_STATUS_CLONE_MIXED_MEMBERS") -CLUSTER_STATUS_EMPTY_NODE_NAME = M("CLUSTER_STATUS_EMPTY_NODE_NAME") -CLUSTER_STATUS_UNEXPECTED_MEMBER = M("CLUSTER_STATUS_UNEXPECTED_MEMBER") -CLUSTER_STATUS_UNKNOWN_PCMK_ROLE = M("CLUSTER_STATUS_UNKNOWN_PCMK_ROLE") CLUSTER_UUID_ALREADY_SET = M("CLUSTER_UUID_ALREADY_SET") CLUSTER_WILL_BE_DESTROYED = M("CLUSTER_WILL_BE_DESTROYED") COMMAND_INVALID_PAYLOAD = M("COMMAND_INVALID_PAYLOAD") diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py index 1e98711c..8b9bc63e 100644 --- a/pcs/common/reports/messages.py +++ b/pcs/common/reports/messages.py @@ -3277,183 +3277,32 @@ class BadClusterStateFormat(ReportItemMessage): @dataclass(frozen=True) -class ClusterStatusUnknownPcmkRole(ReportItemMessage): +class BadClusterState(ReportItemMessage): """ - Value of pcmk role in the status xml is not valid + crm_mon xml output is invalid despite conforming to the schema - role -- value of the role attribute - resource_id -- id of the resource - """ - - role: Optional[str] - resource_id: str - _code = codes.CLUSTER_STATUS_UNKNOWN_PCMK_ROLE - - @property - def message(self) -> str: - return ( - "Attribute of resource with id '{id}' " - "contains {invalid} pcmk role{role}." - ).format( - id=self.resource_id, - invalid="empty" if not self.role else "invalid", - role=f" '{self.role}'" if self.role else "", - ) - - -@dataclass(frozen=True) -class ClusterStatusEmptyNodeName(ReportItemMessage): - """ - Resource in the status xml contains node with empty name - - resource_id -- id of the resource - """ - - resource_id: str - _code = codes.CLUSTER_STATUS_EMPTY_NODE_NAME - - @property - def message(self) -> str: - return ( - f"Resource with id '{self.resource_id}' contains node " - "with empty name." - ) - - -@dataclass(frozen=True) -class ClusterStatusUnexpectedMember(ReportItemMessage): - """ - Unexpected resource type is present in present as child element - in another resource type - - resource_id -- id of the outer resource - resource_type -- type of the outer resource - member_id -- id of the unexpected member - expected_type -- valid types for members - """ - - resource_id: str - resource_type: str - member_id: str - expected_types: list[str] - _code = codes.CLUSTER_STATUS_UNEXPECTED_MEMBER - - @property - def message(self) -> str: - return ( - f"Unexpected resource '{self.member_id}' inside of resource " - f"'{self.resource_id}' of type '{self.resource_type}'. " - f"Only resources of type {format_list(self.expected_types, '|')} " - f"can be in {self.resource_type}." - ) - - -@dataclass(frozen=True) -class ClusterStatusCloneMixedMembers(ReportItemMessage): - """ - Members of multiple types are present in a clone in the status xml - - member_id -- id of the unexpected member - clone_id -- id of the clone - """ - - clone_id: str - _code = codes.CLUSTER_STATUS_CLONE_MIXED_MEMBERS - - @property - def message(self) -> str: - return f"Primitive and group members mixed in clone '{self.clone_id}'." - - -@dataclass(frozen=True) -class ClusterStatusCloneMembersDifferentIds(ReportItemMessage): - """ - Clone instances in crm_mon status xml have different ids - - clone_id -- id of the clone - """ - - clone_id: str - _code = codes.CLUSTER_STATUS_CLONE_MEMBERS_DIFFERENT_IDS - - @property - def message(self) -> str: - return f"Members with different ids in clone '{self.clone_id}'." - - -@dataclass(frozen=True) -class ClusterStatusBundleReplicaNoContainer(ReportItemMessage): - """ - Bundle replica is missing implicit container resource in the status xml - - bundle_id -- id of the bundle - replica_id -- id of the replica - """ - - bundle_id: str - replica_id: str - _code = codes.CLUSTER_STATUS_BUNDLE_REPLICA_NO_CONTAINER - - @property - def message(self) -> str: - return ( - f"Replica '{self.replica_id}' of bundle '{self.bundle_id}' " - "is missing implicit container resource." - ) - - -@dataclass(frozen=True) -class ClusterStatusBundleReplicaMissingRemote(ReportItemMessage): - """ - Bundle replica is missing implicit pacemaker remote resource - in the status xml - - bundle_id -- id of the bundle - replica_id -- id of the replica - """ - - bundle_id: str - replica_id: str - _code = codes.CLUSTER_STATUS_BUNDLE_REPLICA_MISSING_REMOTE - - @property - def message(self) -> str: - return ( - f"Replica '{self.replica_id}' of bundle '{self.bundle_id}' is " - "missing implicit pacemaker remote resource while it must be " - "present." - ) - - -@dataclass(frozen=True) -class ClusterStatusBundleReplicaInvalidCount(ReportItemMessage): + reason -- error description """ - Bundle replica is has invalid number of members in the status xml - bundle_id -- id of the bundle - replica_id -- id of the replica - """ - - bundle_id: str - replica_id: str - _code = codes.CLUSTER_STATUS_BUNDLE_REPLICA_INVALID_COUNT + reason: Optional[str] = None + _code = codes.BAD_CLUSTER_STATE @property def message(self) -> str: return ( - f"Replica '{self.replica_id}' of bundle '{self.bundle_id}' has " - f"invalid number of members. Expecting 2-4 members." + "Cannot load cluster status, xml does not describe valid cluster " + f"status{format_optional(self.reason, template=': {}')}." ) @dataclass(frozen=True) class ClusterStatusBundleMemberIdAsImplicit(ReportItemMessage): """ - Member of bundle in cluster status xml has the same id as one of - the implicit resources + Member of bundle in cluster status xml has the same id as one of the + implicit resources bundle_id -- id of the bundle - member_id -- id if the bundle member + bad_ids -- ids of the bad members """ bundle_id: str @@ -3474,22 +3323,6 @@ class ClusterStatusBundleMemberIdAsImplicit(ReportItemMessage): ) -@dataclass(frozen=True) -class ClusterStatusBundleDifferentReplicas(ReportItemMessage): - """ - Replicas of bundle are different in the cluster status xml - - bundle_id -- id of the bundle - """ - - bundle_id: str - _code = codes.CLUSTER_STATUS_BUNDLE_DIFFERENT_REPLICAS - - @property - def message(self) -> str: - return f"Replicas of bundle '{self.bundle_id}' are not the same." - - @dataclass(frozen=True) class WaitForIdleStarted(ReportItemMessage): """ diff --git a/pcs/lib/commands/status.py b/pcs/lib/commands/status.py index 8b644ac1..f9ec6160 100644 --- a/pcs/lib/commands/status.py +++ b/pcs/lib/commands/status.py @@ -49,7 +49,11 @@ from pcs.lib.pacemaker.live import ( get_cluster_status_xml_raw, get_ticket_status_text, ) -from pcs.lib.pacemaker.status import status_xml_to_dto +from pcs.lib.pacemaker.status import ( + ClusterStatusParser, + ClusterStatusParsingError, + cluster_status_parsing_error_to_report, +) from pcs.lib.resource_agent.const import STONITH_ACTION_REPLACED_BY from pcs.lib.sbd import get_sbd_service_name @@ -79,7 +83,15 @@ def resources_status(env: LibraryEnvironment) -> ResourcesStatusDto: """ status_xml = env.get_cluster_state() - return status_xml_to_dto(env.report_processor, status_xml) + parser = ClusterStatusParser(status_xml) + try: + dto = parser.status_xml_to_dto() + except ClusterStatusParsingError as e: + raise LibraryError(cluster_status_parsing_error_to_report(e)) from e + + env.report_processor.report_list(parser.get_warnings()) + + return dto def full_cluster_status_plaintext( diff --git a/pcs/lib/pacemaker/status.py b/pcs/lib/pacemaker/status.py index 722ce03f..a86ede55 100644 --- a/pcs/lib/pacemaker/status.py +++ b/pcs/lib/pacemaker/status.py @@ -1,3 +1,4 @@ +from collections import Counter from typing import ( Optional, Sequence, @@ -9,14 +10,11 @@ from lxml.etree import _Element from pcs.common import reports from pcs.common.const import ( - PCMK_ROLE_UNKNOWN, PCMK_ROLES, - PCMK_STATUS_ROLE_UNKNOWN, PCMK_STATUS_ROLES, PcmkRoleType, PcmkStatusRoleType, ) -from pcs.common.reports import ReportProcessor from pcs.common.status_dto import ( AnyResourceStatusDto, BundleReplicaStatusDto, @@ -26,7 +24,7 @@ from pcs.common.status_dto import ( PrimitiveStatusDto, ResourcesStatusDto, ) -from pcs.lib.errors import LibraryError +from pcs.common.str_tools import format_list from pcs.lib.pacemaker.values import is_true _PRIMITIVE_TAG = "resource" @@ -36,31 +34,137 @@ _BUNDLE_TAG = "bundle" _REPLICA_TAG = "replica" +class ClusterStatusParsingError(Exception): + def __init__(self, resource_id: str): + self.resource_id = resource_id + + +class EmptyResourceIdError(ClusterStatusParsingError): + def __init__(self): + super().__init__("") + + +class EmptyNodeNameError(ClusterStatusParsingError): + pass + + +class UnknownPcmkRoleError(ClusterStatusParsingError): + def __init__(self, resource_id: str, role: str): + super().__init__(resource_id) + self.role = role + + +class UnexpectedMemberError(ClusterStatusParsingError): + def __init__( + self, + resource_id: str, + resource_type: str, + member_id: str, + expected_types: list[str], + ): + super().__init__(resource_id) + self.resource_type = resource_type + self.member_id = member_id + self.expected_types = expected_types + + +class MixedMembersError(ClusterStatusParsingError): + pass + + +class DifferentMemberIdsError(ClusterStatusParsingError): + pass + + +class BundleReplicaMissingImplicitResourceError(ClusterStatusParsingError): + def __init__( + self, resource_id: str, replica_id: str, implicit_resource_type: str + ): + super().__init__(resource_id) + self.replica_id = replica_id + self.implicit_type = implicit_resource_type + + +class BundleReplicaInvalidMemberCountError(ClusterStatusParsingError): + def __init__(self, resource_id: str, replica_id: str): + super().__init__(resource_id) + self.replica_id = replica_id + + +class BundleDifferentReplicas(ClusterStatusParsingError): + pass + + +class BundleSameIdAsImplicitResourceError(Exception): + def __init__(self, bundle_id: str, bad_ids: list[str]): + self.bundle_id = bundle_id + self.bad_ids = bad_ids + + +def cluster_status_parsing_error_to_report( + e: ClusterStatusParsingError, +) -> reports.ReportItem: + reason = "" + if isinstance(e, EmptyResourceIdError): + reason = "Resource with empty id." + elif isinstance(e, EmptyNodeNameError): + reason = ( + f"Resource with id '{e.resource_id}' contains node with empty name." + ) + elif isinstance(e, UnknownPcmkRoleError): + reason = ( + f"Resource with id '{e.resource_id}' contains unknown " + f"pcmk 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}." + ) + + elif isinstance(e, MixedMembersError): + 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}'." + elif isinstance(e, BundleReplicaMissingImplicitResourceError): + reason = ( + f"Replica '{e.replica_id}' of bundle '{e.resource_id}' " + 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." + ) + elif isinstance(e, BundleDifferentReplicas): + reason = f"Replicas of bundle '{e.resource_id}' are not the same." + + return reports.ReportItem( + reports.ReportItemSeverity.error(), + reports.messages.BadClusterState(reason), + ) + + def _primitive_to_dto( - reporter: ReportProcessor, - primitive_el: _Element, - remove_clone_suffix: bool = False, + primitive_el: _Element, remove_clone_suffix: bool = False ) -> PrimitiveStatusDto: - resource_id = _get_resource_id(reporter, primitive_el) + resource_id = _get_resource_id(primitive_el) if remove_clone_suffix: resource_id = _remove_clone_suffix(resource_id) - role = _get_role(reporter, primitive_el, resource_id) - target_role = _get_target_role(reporter, primitive_el, resource_id) + role = _get_role(primitive_el) + target_role = _get_target_role(primitive_el) node_names = [ str(node.get("name")) for node in primitive_el.iterfind("node") ] if node_names and any(not name for name in node_names): - reporter.report( - reports.ReportItem.error( - reports.messages.ClusterStatusEmptyNodeName(resource_id) - ) - ) - - if reporter.has_errors: - raise LibraryError() + raise EmptyNodeNameError(resource_id) return PrimitiveStatusDto( resource_id, @@ -82,87 +186,70 @@ def _primitive_to_dto( def _group_to_dto( - reporter: ReportProcessor, - group_el: _Element, - remove_clone_suffix: bool = False, + 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(reporter, group_el)) - members = [] + group_id = _remove_clone_suffix(_get_resource_id(group_el)) + member_list = [] for member in group_el: if member.tag == _PRIMITIVE_TAG: - members.append( - _primitive_to_dto(reporter, member, remove_clone_suffix) - ) + member_list.append(_primitive_to_dto(member, remove_clone_suffix)) else: - reporter.report( - reports.ReportItem.error( - reports.messages.ClusterStatusUnexpectedMember( - group_id, "group", str(member.get("id")), ["primitive"] - ) - ) + raise UnexpectedMemberError( + group_id, "group", str(member.get("id")), ["primitive"] ) - if reporter.has_errors: - raise LibraryError() - return GroupStatusDto( group_id, is_true(group_el.get("maintenance", "false")), group_el.get("description"), is_true(group_el.get("managed", "false")), is_true(group_el.get("disabled", "false")), - members, + member_list, ) def _clone_to_dto( - reporter: ReportProcessor, - clone_el: _Element, - _remove_clone_suffix: bool = False, + clone_el: _Element, _remove_clone_suffix: bool = False ) -> CloneStatusDto: - clone_id = _get_resource_id(reporter, clone_el) + clone_id = _get_resource_id(clone_el) is_unique = is_true(clone_el.get("unique", "false")) - target_role = _get_target_role(reporter, clone_el, clone_id) + target_role = _get_target_role(clone_el) - primitives = [] - groups = [] + primitive_list = [] + group_list = [] for member in clone_el: if member.tag == _PRIMITIVE_TAG: - primitives.append(_primitive_to_dto(reporter, member, is_unique)) + primitive_list.append(_primitive_to_dto(member, is_unique)) elif member.tag == _GROUP_TAG: - groups.append(_group_to_dto(reporter, member, is_unique)) + group_list.append(_group_to_dto(member, is_unique)) else: - reporter.report( - reports.ReportItem.error( - reports.messages.ClusterStatusUnexpectedMember( - clone_id, - "clone", - str(member.get("id")), - ["primitive", "group"], - ) - ) + raise UnexpectedMemberError( + clone_id, "clone", str(member.get("id")), ["primitive", "group"] ) - reporter.report_list( - _validate_mixed_instance_types(primitives, groups, clone_id) - ) + if primitive_list and group_list: + raise MixedMembersError(clone_id) - instances: Union[list[PrimitiveStatusDto], list[GroupStatusDto]] - if primitives: - reporter.report_list( - _validate_primitive_instance_ids(primitives, clone_id) - ) - instances = primitives + 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: - reporter.report_list(_validate_group_instance_ids(groups, clone_id)) - instances = groups + 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) - if reporter.has_errors: - raise LibraryError() + instance_list = group_list return CloneStatusDto( clone_id, @@ -175,30 +262,23 @@ def _clone_to_dto( is_true(clone_el.get("failed", "false")), is_true(clone_el.get("failure_ignored", "false")), target_role, - instances, + instance_list, ) def _bundle_to_dto( - reporter: ReportProcessor, - bundle_el: _Element, - _remove_clone_suffix: bool = False, -) -> Optional[BundleStatusDto]: - bundle_id = _get_resource_id(reporter, bundle_el) + bundle_el: _Element, _remove_clone_suffix: bool = False +) -> BundleStatusDto: + bundle_id = _get_resource_id(bundle_el) bundle_type = str(bundle_el.get("type")) - replicas = [] - for replica in bundle_el.iterfind(_REPLICA_TAG): - replica_dto = _replica_to_dto(reporter, replica, bundle_id, bundle_type) - if replica_dto is None: - # skip this bundle in status - return None - replicas.append(replica_dto) - - reporter.report_list(_validate_replicas(replicas, bundle_id)) + replica_list = [ + _replica_to_dto(replica, bundle_id, bundle_type) + for replica in bundle_el.iterfind(_REPLICA_TAG) + ] - if reporter.has_errors: - raise LibraryError() + if not _replicas_valid(replica_list): + raise BundleDifferentReplicas(bundle_id) return BundleStatusDto( bundle_id, @@ -209,87 +289,79 @@ def _bundle_to_dto( bundle_el.get("description"), is_true(bundle_el.get("managed", "false")), is_true(bundle_el.get("failed", "false")), - replicas, + replica_list, ) -_TAG_TO_FUNCTION = { - _PRIMITIVE_TAG: _primitive_to_dto, - _GROUP_TAG: _group_to_dto, - _CLONE_TAG: _clone_to_dto, - _BUNDLE_TAG: _bundle_to_dto, -} - - -def status_xml_to_dto( - reporter: ReportProcessor, status: _Element -) -> ResourcesStatusDto: - """ - Return dto containing status of configured resources in the cluster - - reporter -- ReportProcessor - status -- status xml document from crm_mon, validated using - the appropriate rng schema - """ - resources = cast(list[_Element], status.xpath("resources/*")) - - resource_dtos = [ - _TAG_TO_FUNCTION[resource.tag](reporter, resource) - for resource in resources - if resource.tag in _TAG_TO_FUNCTION - ] +class ClusterStatusParser: + TAG_TO_FUNCTION = { + _PRIMITIVE_TAG: _primitive_to_dto, + _GROUP_TAG: _group_to_dto, + _CLONE_TAG: _clone_to_dto, + _BUNDLE_TAG: _bundle_to_dto, + } + + def __init__(self, status: _Element): + 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_dto_list = [] + for resource in resource_list: + try: + resource_dto = cast( + AnyResourceStatusDto, + self.TAG_TO_FUNCTION[resource.tag](resource), + ) + resource_dto_list.append(resource_dto) + except BundleSameIdAsImplicitResourceError as e: + # This is the only error that the user can cause directly by + # setting the name of the bundle member to be same as one of + # the implicitly created resource. + # We only skip such bundles while still providing status of the + # other resources. + self.warnings.append( + reports.ReportItem.warning( + reports.messages.ClusterStatusBundleMemberIdAsImplicit( + e.bundle_id, e.bad_ids + ) + ) + ) - if reporter.has_errors: - raise LibraryError() + return ResourcesStatusDto(resource_dto_list) - return ResourcesStatusDto( - cast( - list[AnyResourceStatusDto], - [dto for dto in resource_dtos if dto is not None], - ) - ) + def get_warnings(self) -> reports.ReportItemList: + return self.warnings -def _get_resource_id(reporter: ReportProcessor, resource: _Element) -> str: +def _get_resource_id(resource: _Element) -> str: resource_id = resource.get("id") if not resource_id: - reporter.report( - reports.ReportItem.error( - reports.messages.InvalidIdIsEmpty("resource id") - ) - ) + raise EmptyResourceIdError() return str(resource_id) -def _get_role( - reporter: ReportProcessor, resource: _Element, resource_id: str -) -> PcmkStatusRoleType: +def _get_role(resource: _Element) -> PcmkStatusRoleType: role = resource.get("role") if role is None or role not in PCMK_STATUS_ROLES: - reporter.report( - reports.ReportItem.warning( - reports.messages.ClusterStatusUnknownPcmkRole(role, resource_id) - ) - ) - return PCMK_STATUS_ROLE_UNKNOWN + raise UnknownPcmkRoleError(str(resource.get("id")), str(role)) return PcmkStatusRoleType(role) -def _get_target_role( - reporter: ReportProcessor, resource: _Element, resource_id: str -) -> Optional[PcmkRoleType]: +def _get_target_role(resource: _Element) -> Optional[PcmkRoleType]: target_role = resource.get("target_role") if target_role is None: return None if target_role not in PCMK_ROLES: - reporter.report( - reports.ReportItem.warning( - reports.messages.ClusterStatusUnknownPcmkRole( - target_role, resource_id - ) - ) - ) - return PCMK_ROLE_UNKNOWN + raise UnknownPcmkRoleError(str(resource.get("id")), target_role) return PcmkRoleType(target_role) @@ -299,130 +371,66 @@ def _remove_clone_suffix(resource_id: str) -> str: return resource_id -def _validate_mixed_instance_types( - primitives: list[PrimitiveStatusDto], - groups: list[GroupStatusDto], - clone_id: str, -) -> reports.ReportItemList: - if primitives and groups: - return [ - reports.ReportItem.error( - reports.messages.ClusterStatusCloneMixedMembers(clone_id) - ) - ] - return [] - - -def _validate_primitive_instance_ids( - instances: list[PrimitiveStatusDto], clone_id: str -) -> reports.ReportItemList: - if len(set(res.resource_id for res in instances)) > 1: - return [ - reports.ReportItem.error( - reports.messages.ClusterStatusCloneMembersDifferentIds(clone_id) - ) - ] - return [] - - -def _validate_group_instance_ids( - instances: list[GroupStatusDto], clone_id: str -) -> reports.ReportItemList: - group_ids = set(group.resource_id for group in instances) - children_ids = set( - tuple(child.resource_id for child in group.members) - for group in instances - ) - - if len(group_ids) > 1 or len(children_ids) > 1: - return [ - reports.ReportItem.error( - reports.messages.ClusterStatusCloneMembersDifferentIds(clone_id) - ) - ] - return [] - - def _replica_to_dto( - reporter: ReportProcessor, - replica_el: _Element, - bundle_id: str, - bundle_type: str, -) -> Optional[BundleReplicaStatusDto]: + replica_el: _Element, bundle_id: str, bundle_type: str +) -> BundleReplicaStatusDto: replica_id = str(replica_el.get("id")) - resources = [ - _primitive_to_dto(reporter, resource) + resource_list = [ + _primitive_to_dto(resource) for resource in replica_el.iterfind(_PRIMITIVE_TAG) ] - duplicate_ids = _find_duplicate_ids(resources) + duplicate_ids = [ + id + for id, count in Counter( + resource.resource_id for resource in resource_list + ).items() + if count > 1 + ] + if duplicate_ids: - reporter.report( - reports.ReportItem.warning( - reports.messages.ClusterStatusBundleMemberIdAsImplicit( - bundle_id, duplicate_ids - ) - ) - ) - return None + raise BundleSameIdAsImplicitResourceError(bundle_id, duplicate_ids) # TODO pacemaker will probably add prefix # "pcmk-internal" to all implicit resources - container_resource = _get_implicit_resource( - resources, + container_resource = _pop_implicit_resource( + resource_list, f"{bundle_id}-{bundle_type}-{replica_id}", True, f"ocf:heartbeat:{bundle_type}", ) if container_resource is None: - reporter.report( - reports.ReportItem.error( - reports.messages.ClusterStatusBundleReplicaNoContainer( - bundle_id, replica_id - ) - ) + raise BundleReplicaMissingImplicitResourceError( + bundle_id, replica_id, "container" ) - raise LibraryError() - remote_resource = _get_implicit_resource( - resources, f"{bundle_id}-{replica_id}", True, "ocf:pacemaker:remote" + remote_resource = _pop_implicit_resource( + resource_list, f"{bundle_id}-{replica_id}", True, "ocf:pacemaker:remote" ) # implicit ip address resource might be present ip_resource = None - if (remote_resource is not None and len(resources) == 2) or ( - remote_resource is None and len(resources) == 1 + if (remote_resource is not None and len(resource_list) == 2) or ( + remote_resource is None and len(resource_list) == 1 ): - ip_resource = _get_implicit_resource( - resources, f"{bundle_id}-ip-", False, "ocf:heartbeat:IPaddr2" + ip_resource = _pop_implicit_resource( + resource_list, f"{bundle_id}-ip-", False, "ocf:heartbeat:IPaddr2" ) - if remote_resource is None and resources: - reporter.report( - reports.ReportItem.error( - reports.messages.ClusterStatusBundleReplicaMissingRemote( - bundle_id, replica_id - ) - ) + if remote_resource is None and resource_list: + raise BundleReplicaMissingImplicitResourceError( + bundle_id, replica_id, "remote" ) - raise LibraryError() member = None if remote_resource: - if len(resources) == 1: - member = resources[0] + if len(resource_list) == 1: + member = resource_list[0] else: - reporter.report( - reports.ReportItem.error( - reports.messages.ClusterStatusBundleReplicaInvalidCount( - bundle_id, replica_id - ) - ) - ) - raise LibraryError() + raise BundleReplicaInvalidMemberCountError(bundle_id, replica_id) return BundleReplicaStatusDto( replica_id, @@ -433,24 +441,13 @@ def _replica_to_dto( ) -def _find_duplicate_ids(resources: Sequence[AnyResourceStatusDto]) -> list[str]: - seen = set() - duplicates = [] - for resource in resources: - if resource.resource_id in seen: - duplicates.append(resource.resource_id) - else: - seen.add(resource.resource_id) - return duplicates - - -def _get_implicit_resource( - primitives: list[PrimitiveStatusDto], +def _pop_implicit_resource( + primitive_list: list[PrimitiveStatusDto], expected_id: str, exact_match: bool, resource_agent: str, ) -> Optional[PrimitiveStatusDto]: - for primitive in primitives: + for primitive in primitive_list: matching_id = ( exact_match and primitive.resource_id == expected_id @@ -459,36 +456,28 @@ def _get_implicit_resource( ) if matching_id and primitive.resource_agent == resource_agent: - primitives.remove(primitive) + primitive_list.remove(primitive) return primitive return None -def _validate_replicas( - replicas: Sequence[BundleReplicaStatusDto], bundle_id: str -) -> reports.ReportItemList: - if not replicas: - return [] +def _replicas_valid(replica_list: Sequence[BundleReplicaStatusDto]) -> bool: + if not replica_list: + return True - member = replicas[0].member - ip = replicas[0].ip_address - container = replicas[0].container + member = replica_list[0].member + ip = replica_list[0].ip_address + container = replica_list[0].container - for replica in replicas: + for replica in replica_list: if ( not _cmp_replica_members(member, replica.member, True) or not _cmp_replica_members(ip, replica.ip_address, False) or not _cmp_replica_members(container, replica.container, False) ): - return [ - reports.ReportItem.error( - reports.messages.ClusterStatusBundleDifferentReplicas( - bundle_id - ) - ) - ] - return [] + return False + return True def _cmp_replica_members( diff --git a/pcs_test/resources/crm_mon.all_resources.xml b/pcs_test/resources/crm_mon.all_resources.xml index e493d308..f11db064 100644 --- a/pcs_test/resources/crm_mon.all_resources.xml +++ b/pcs_test/resources/crm_mon.all_resources.xml @@ -1,40 +1,53 @@ - + - - - - - - + + + + + + - - - + - - - - - - - - - - - - - - + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py index b60360e4..48eb730c 100644 --- a/pcs_test/tier0/common/reports/test_messages.py +++ b/pcs_test/tier0/common/reports/test_messages.py @@ -2195,6 +2195,26 @@ class BadClusterStateFormat(NameBuildTest): ) +class BadClusterState(NameBuildTest): + def test_no_reason(self): + self.assert_message_from_report( + ( + "Cannot load cluster status, xml does not describe " + "valid cluster status." + ), + reports.BadClusterState(), + ) + + def test_reason(self): + self.assert_message_from_report( + ( + "Cannot load cluster status, xml does not describe " + "valid cluster status: sample reason." + ), + reports.BadClusterState("sample reason"), + ) + + class WaitForIdleStarted(NameBuildTest): def test_timeout(self): timeout = 20 @@ -5818,143 +5838,26 @@ class CannotCreateDefaultClusterPropertySet(NameBuildTest): ) -class ClusterStatusBundleDifferentReplicas(NameBuildTest): - def test_message(self): - self.assert_message_from_report( - "Replicas of bundle 'bundle' are not the same.", - reports.ClusterStatusBundleDifferentReplicas("bundle"), - ) - - class ClusterStatusBundleMemberIdAsImplicit(NameBuildTest): - def test_message(self): + def test_one(self): self.assert_message_from_report( ( - "Skipping bundle 'bundle': resource 'test' has " - "the same id as some of the implicit bundle resources." - ), - reports.ClusterStatusBundleMemberIdAsImplicit("bundle", ["test"]), - ) - - def test_multiple_ids(self): - self.assert_message_from_report( - ( - "Skipping bundle 'bundle': resources 'test1', 'test2' have " + "Skipping bundle 'resource-bundle': resource 'resource' has " "the same id as some of the implicit bundle resources." ), reports.ClusterStatusBundleMemberIdAsImplicit( - "bundle", ["test1", "test2"] - ), - ) - - -class ClusterStatusBundleReplicaInvalidCount(NameBuildTest): - def test_message(self): - self.assert_message_from_report( - ( - "Replica '0' of bundle 'bundle' has invalid number of members. " - "Expecting 2-4 members." + "resource-bundle", ["resource"] ), - reports.ClusterStatusBundleReplicaInvalidCount("bundle", "0"), ) - -class ClusterStatusBundleReplicaMissingRemote(NameBuildTest): - def test_message(self): - self.assert_message_from_report( - ( - "Replica '0' of bundle 'bundle' is missing implicit pacemaker " - "remote resource while it must be present." - ), - reports.ClusterStatusBundleReplicaMissingRemote("bundle", "0"), - ) - - -class ClusterStatusBundleReplicaNoContainer(NameBuildTest): - def test_message(self): - self.assert_message_from_report( - ( - "Replica '0' of bundle 'bundle' is missing implicit container " - "resource." - ), - reports.ClusterStatusBundleReplicaNoContainer("bundle", "0"), - ) - - -class ClusterStatusCloneMembersDifferentIds(NameBuildTest): - def test_message(self): - self.assert_message_from_report( - "Members with different ids in clone 'clone'.", - reports.ClusterStatusCloneMembersDifferentIds("clone"), - ) - - -class ClusterStatusCloneMixedMembers(NameBuildTest): - def test_message(self): - self.assert_message_from_report( - "Primitive and group members mixed in clone 'clone'.", - reports.ClusterStatusCloneMixedMembers("clone"), - ) - - -class ClusterStatusEmptyNodeName(NameBuildTest): - def test_message(self): - self.assert_message_from_report( - "Resource with id 'resource' contains node with empty name.", - reports.ClusterStatusEmptyNodeName("resource"), - ) - - -class ClusterStatusUnexpectedMember(NameBuildTest): - def test_one_expected(self): - self.assert_message_from_report( - ( - "Unexpected resource 'member' inside of resource 'resource' of " - "type 'group'. Only resources of type 'primitive' " - "can be in group." - ), - reports.ClusterStatusUnexpectedMember( - resource_id="resource", - resource_type="group", - member_id="member", - expected_types=["primitive"], - ), - ) - - def test_multiple_expected(self): + def test_multiple(self): self.assert_message_from_report( ( - "Unexpected resource 'member' inside of resource 'resource' of " - "type 'clone'. Only resources of type 'group'|'primitive' " - "can be in clone." - ), - reports.ClusterStatusUnexpectedMember( - resource_id="resource", - resource_type="clone", - member_id="member", - expected_types=["primitive", "group"], + "Skipping bundle 'resource-bundle': resources 'resource-0', " + "'resource-1' have the same id as some of the implicit bundle " + "resources." ), - ) - - -class ClusterStatusUnknownPcmkRole(NameBuildTest): - def test_no_role(self): - self.assert_message_from_report( - "Attribute of resource with id 'resource' contains empty pcmk role.", - reports.ClusterStatusUnknownPcmkRole(None, "resource"), - ) - - def test_empty_role(self): - self.assert_message_from_report( - "Attribute of resource with id 'resource' contains empty pcmk role.", - reports.ClusterStatusUnknownPcmkRole("", "resource"), - ) - - def test_role(self): - self.assert_message_from_report( - ( - "Attribute of resource with id 'resource' contains invalid " - "pcmk role 'NotValidRole'." + reports.ClusterStatusBundleMemberIdAsImplicit( + "resource-bundle", ["resource-0", "resource-1"] ), - reports.ClusterStatusUnknownPcmkRole("NotValidRole", "resource"), ) diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py index a5a395b5..3b6b7665 100644 --- a/pcs_test/tier0/lib/commands/test_status.py +++ b/pcs_test/tier0/lib/commands/test_status.py @@ -1,5 +1,7 @@ +# pylint: disable=too-many-lines import os from textwrap import dedent +from typing import Optional from unittest import ( TestCase, mock, @@ -7,7 +9,11 @@ from unittest import ( from pcs import settings from pcs.common import file_type_codes -from pcs.common.const import PCMK_STATUS_ROLE_STARTED +from pcs.common.const import ( + PCMK_ROLE_STOPPED, + PCMK_STATUS_ROLE_STOPPED, + PcmkRoleType, +) from pcs.common.reports import codes as report_codes from pcs.common.status_dto import ( BundleReplicaStatusDto, @@ -1267,24 +1273,27 @@ class FullClusterStatusPlaintextBoothWarning(FullClusterStatusPlaintextBase): def _fixture_primitive_resource_dto( - resource_id: str, resource_agent: str + resource_id: str, + resource_agent: str, + target_role: Optional[PcmkRoleType] = None, + managed: bool = True, ) -> PrimitiveStatusDto: return PrimitiveStatusDto( - resource_id, - resource_agent, - PCMK_STATUS_ROLE_STARTED, - None, - True, - False, - False, - False, - None, - False, - True, - False, - ["node1"], - None, - None, + resource_id=resource_id, + resource_agent=resource_agent, + role=PCMK_STATUS_ROLE_STOPPED, + target_role=target_role, + active=False, + orphaned=False, + blocked=False, + maintenance=False, + description=None, + failed=False, + managed=managed, + failure_ignored=False, + node_names=[], + pending=None, + locked_to=None, ) @@ -1303,7 +1312,7 @@ class ResourcesStatus(TestCase): result = status.resources_status(self.env_assist.get_env()) self.assertEqual(result, ResourcesStatusDto([])) - def test_bad_xml(self): + def test_bad_xml_format(self): self.config.runner.pcmk.load_state( resources=""" @@ -1320,6 +1329,26 @@ class ResourcesStatus(TestCase): False, ) + def test_bad_xml(self): + self.config.runner.pcmk.load_state( + resources=""" + + + + """, + ) + + self.env_assist.assert_raise_library_error( + 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'.", + ), + ], + False, + ) + def test_all_resources(self): self.config.runner.pcmk.load_state( filename=rc("crm_mon.all_resources.xml") @@ -1327,69 +1356,187 @@ class ResourcesStatus(TestCase): result = status.resources_status(self.env_assist.get_env()) - self.assertTrue(len(result.resources) == 4) self.assertEqual( - result.resources[0], - _fixture_primitive_resource_dto("dummy", "ocf:pacemaker:Dummy"), - ) - self.assertEqual( - result.resources[1], - GroupStatusDto( - "group", - False, - None, - True, - False, - members=[ + result, + ResourcesStatusDto( + [ + BundleStatusDto( + resource_id="B1", + type="docker", + image="pcs:test", + unique=True, + maintenance=False, + description=None, + managed=False, + failed=False, + replicas=[ + BundleReplicaStatusDto( + replica_id="0", + member=None, + remote=None, + container=_fixture_primitive_resource_dto( + "B1-docker-0", + "ocf:heartbeat:docker", + target_role=PCMK_ROLE_STOPPED, + managed=False, + ), + ip_address=_fixture_primitive_resource_dto( + "B1-ip-192.168.100.200", + "ocf:heartbeat:IPaddr2", + target_role=PCMK_ROLE_STOPPED, + managed=False, + ), + ), + BundleReplicaStatusDto( + replica_id="1", + member=None, + remote=None, + container=_fixture_primitive_resource_dto( + "B1-docker-1", + "ocf:heartbeat:docker", + target_role=PCMK_ROLE_STOPPED, + managed=False, + ), + ip_address=_fixture_primitive_resource_dto( + "B1-ip-192.168.100.201", + "ocf:heartbeat:IPaddr2", + target_role=PCMK_ROLE_STOPPED, + managed=False, + ), + ), + BundleReplicaStatusDto( + replica_id="2", + member=None, + remote=None, + container=_fixture_primitive_resource_dto( + "B1-docker-2", + "ocf:heartbeat:docker", + target_role=PCMK_ROLE_STOPPED, + managed=False, + ), + ip_address=_fixture_primitive_resource_dto( + "B1-ip-192.168.100.202", + "ocf:heartbeat:IPaddr2", + target_role=PCMK_ROLE_STOPPED, + managed=False, + ), + ), + BundleReplicaStatusDto( + replica_id="3", + member=None, + remote=None, + container=_fixture_primitive_resource_dto( + "B1-docker-3", + "ocf:heartbeat:docker", + target_role=PCMK_ROLE_STOPPED, + managed=False, + ), + ip_address=_fixture_primitive_resource_dto( + "B1-ip-192.168.100.203", + "ocf:heartbeat:IPaddr2", + target_role=PCMK_ROLE_STOPPED, + managed=False, + ), + ), + ], + ), _fixture_primitive_resource_dto( - "grouped", "ocf:pacemaker:Dummy" - ) - ], - ), - ) - self.assertEqual( - result.resources[2], - CloneStatusDto( - "clone", - False, - False, - False, - None, - True, - False, - False, - False, - None, - instances=[ + "R7", "ocf:pacemaker:Dummy" + ), _fixture_primitive_resource_dto( - "cloned", "ocf:pacemaker:Dummy" - ) - ], + "S2", "stonith:fence_kdump" + ), + GroupStatusDto( + resource_id="G2", + maintenance=False, + description=None, + managed=True, + disabled=False, + members=[ + _fixture_primitive_resource_dto( + "R5", "ocf:pacemaker:Dummy" + ), + _fixture_primitive_resource_dto( + "S1", "stonith:fence_kdump" + ), + ], + ), + CloneStatusDto( + resource_id="G1-clone", + multi_state=True, + unique=False, + maintenance=False, + description=None, + managed=True, + disabled=False, + failed=False, + failure_ignored=False, + target_role=None, + instances=[ + GroupStatusDto( + resource_id="G1", + maintenance=False, + description=None, + managed=True, + disabled=False, + members=[ + _fixture_primitive_resource_dto( + "R2", "ocf:pacemaker:Stateful" + ), + _fixture_primitive_resource_dto( + "R3", "ocf:pacemaker:Stateful" + ), + _fixture_primitive_resource_dto( + "R4", "ocf:pacemaker:Stateful" + ), + ], + ) + ], + ), + CloneStatusDto( + resource_id="R6-clone", + multi_state=False, + unique=False, + maintenance=False, + description=None, + managed=True, + disabled=False, + failed=False, + failure_ignored=False, + target_role=None, + instances=[ + _fixture_primitive_resource_dto( + "R6", "ocf:pacemaker:Dummy" + ) + ], + ), + ] ), ) - self.assertEqual( - result.resources[3], - BundleStatusDto( - "bundle", - "podman", - "localhost/pcmktest:http", - False, - False, - None, - True, - False, - [ - BundleReplicaStatusDto( - "0", - None, - None, - _fixture_primitive_resource_dto( - "bundle-podman-0", "ocf:heartbeat:podman" - ), - _fixture_primitive_resource_dto( - "bundle-ip-192.168.122.250", "ocf:heartbeat:IPaddr2" - ), - ) - ], - ), + + def test_bundle_skip(self): + self.config.runner.pcmk.load_state( + resources=""" + + + + + + + + + + """, + ) + + result = status.resources_status(self.env_assist.get_env()) + self.assertEqual(result, ResourcesStatusDto([])) + self.env_assist.assert_reports( + [ + fixture.warn( + report_codes.CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT, + bundle_id="B1", + bad_ids=["B1-0"], + ) + ] ) diff --git a/pcs_test/tier0/lib/pacemaker/test_status.py b/pcs_test/tier0/lib/pacemaker/test_status.py index 451fb584..778e97a6 100644 --- a/pcs_test/tier0/lib/pacemaker/test_status.py +++ b/pcs_test/tier0/lib/pacemaker/test_status.py @@ -11,11 +11,9 @@ from lxml import etree from pcs.common import reports from pcs.common.const import ( PCMK_ROLE_STARTED, - PCMK_ROLE_UNKNOWN, PCMK_ROLES, PCMK_STATUS_ROLE_STARTED, PCMK_STATUS_ROLE_STOPPED, - PCMK_STATUS_ROLE_UNKNOWN, PCMK_STATUS_ROLE_UNPROMOTED, PCMK_STATUS_ROLES, PCMK_STATUS_ROLES_PENDING, @@ -34,10 +32,9 @@ from pcs.lib.pacemaker import status from pcs_test.tools import fixture from pcs_test.tools.assertions import ( - assert_raise_library_error, + assert_report_item_equal, assert_report_item_list_equal, ) -from pcs_test.tools.custom_mock import MockLibraryReportProcessor def fixture_primitive_xml( @@ -327,34 +324,163 @@ def fixture_crm_mon_xml(resources: list[str]) -> str: """ +class TestParsingErrorToReport(TestCase): + # pylint: disable=no-self-use + + def test_empty_resource_id(self): + report = status.cluster_status_parsing_error_to_report( + status.EmptyResourceIdError() + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason="Resource with empty id.", + ), + ) + + def test_empty_node_name(self): + report = status.cluster_status_parsing_error_to_report( + status.EmptyNodeNameError("resource") + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason="Resource with id 'resource' contains node with empty name.", + ), + ) + + def test_unknow_pcmk_role(self): + report = status.cluster_status_parsing_error_to_report( + status.UnknownPcmkRoleError("resource", "NotPcmkRole") + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason="Resource with id 'resource' contains unknown pcmk role 'NotPcmkRole'.", + ), + ) + + def test_unexpected_member_group(self): + report = status.cluster_status_parsing_error_to_report( + status.UnexpectedMemberError( + "resource", "group", "member", ["primitive"] + ) + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason=( + "Unexpected resource 'member' inside of resource " + "'resource' of type 'group'. Only resources of type " + "'primitive' can be in group." + ), + ), + ) + + def test_unexpected_member_clone(self): + report = status.cluster_status_parsing_error_to_report( + status.UnexpectedMemberError( + "resource", "clone", "member", ["primitive", "group"] + ) + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason=( + "Unexpected resource 'member' inside of resource " + "'resource' of type 'clone'. Only resources of type " + "'group'|'primitive' can be in clone." + ), + ), + ) + + def test_mixed_members(self): + report = status.cluster_status_parsing_error_to_report( + status.MixedMembersError("resource") + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason="Primitive and group members mixed in clone 'resource'.", + ), + ) + + def test_different_member_ids(self): + report = status.cluster_status_parsing_error_to_report( + status.DifferentMemberIdsError("resource") + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason="Members with different ids in resource 'resource'.", + ), + ) + + def test_bundle_replica_missing_implicit(self): + report = status.cluster_status_parsing_error_to_report( + status.BundleReplicaMissingImplicitResourceError( + "resource", "0", "container" + ) + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason="Replica '0' of bundle 'resource' is missing implicit container resource.", + ), + ) + + def test_bundle_replica_invalid_member_count(self): + report = status.cluster_status_parsing_error_to_report( + status.BundleReplicaInvalidMemberCountError("resource", "0") + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason="Replica '0' of bundle 'resource' has invalid number of members.", + ), + ) + + def test_bundle_different_replicas(self): + report = status.cluster_status_parsing_error_to_report( + status.BundleDifferentReplicas("resource") + ) + assert_report_item_equal( + report, + fixture.error( + reports.codes.BAD_CLUSTER_STATE, + reason="Replicas of bundle 'resource' are not the same.", + ), + ) + + class TestPrimitiveStatusToDto(TestCase): # pylint: disable=protected-access - def setUp(self): - self.report_processor = MockLibraryReportProcessor() - def test_simple(self): primitive_xml = etree.fromstring(fixture_primitive_xml()) - result = status._primitive_to_dto(self.report_processor, primitive_xml) + result = status._primitive_to_dto(primitive_xml) self.assertEqual(result, fixture_primitive_dto()) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_empty_node_list(self): primitive_xml = etree.fromstring( fixture_primitive_xml(role=PCMK_STATUS_ROLE_STOPPED, node_names=[]) ) - result = status._primitive_to_dto(self.report_processor, primitive_xml) + result = status._primitive_to_dto(primitive_xml) self.assertEqual( result, fixture_primitive_dto(role=PCMK_STATUS_ROLE_STOPPED, node_names=[]), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_all_attributes(self): primitive_xml = etree.fromstring( @@ -363,7 +489,7 @@ class TestPrimitiveStatusToDto(TestCase): ) ) - result = status._primitive_to_dto(self.report_processor, primitive_xml) + result = status._primitive_to_dto(primitive_xml) self.assertEqual( result, @@ -371,74 +497,40 @@ class TestPrimitiveStatusToDto(TestCase): target_role=PCMK_STATUS_ROLE_STOPPED, add_optional_args=True ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_remove_clone_suffix(self): primitive_xml = etree.fromstring( fixture_primitive_xml(resource_id="resource:0") ) - result = status._primitive_to_dto( - self.report_processor, primitive_xml, True - ) + result = status._primitive_to_dto(primitive_xml, True) self.assertEqual(result, fixture_primitive_dto()) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_running_on_multiple_nodes(self): primitive_xml = etree.fromstring( fixture_primitive_xml(node_names=["node1", "node2", "node3"]) ) - result = status._primitive_to_dto(self.report_processor, primitive_xml) + result = status._primitive_to_dto(primitive_xml) self.assertEqual( result, fixture_primitive_dto(node_names=["node1", "node2", "node3"]), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_empty_node_name(self): primitive_xml = etree.fromstring(fixture_primitive_xml(node_names=[""])) - assert_raise_library_error( - lambda: status._primitive_to_dto( - self.report_processor, primitive_xml - ) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_EMPTY_NODE_NAME, - resource_id="resource", - ) - ], - ) + with self.assertRaises(status.EmptyNodeNameError) as cm: + status._primitive_to_dto(primitive_xml) + self.assertEqual(cm.exception.resource_id, "resource") def test_empty_resource_id(self): primitive_xml = etree.fromstring(fixture_primitive_xml(resource_id="")) - assert_raise_library_error( - lambda: status._primitive_to_dto( - self.report_processor, primitive_xml - ) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.INVALID_ID_IS_EMPTY, - id_description="resource id", - ) - ], - ) + with self.assertRaises(status.EmptyResourceIdError): + status._primitive_to_dto(primitive_xml) def test_role(self): for role in PCMK_STATUS_ROLES: @@ -446,35 +538,17 @@ class TestPrimitiveStatusToDto(TestCase): primitive_xml = etree.fromstring( fixture_primitive_xml(role=role) ) - - result = status._primitive_to_dto( - self.report_processor, primitive_xml - ) + result = status._primitive_to_dto(primitive_xml) self.assertEqual(result, fixture_primitive_dto(role=role)) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_invalid_role(self): primitive_xml = etree.fromstring( fixture_primitive_xml(role="NotPcmkRole") ) - result = status._primitive_to_dto(self.report_processor, primitive_xml) - - self.assertEqual( - result, fixture_primitive_dto(role=PCMK_STATUS_ROLE_UNKNOWN) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.warn( - reports.codes.CLUSTER_STATUS_UNKNOWN_PCMK_ROLE, - role="NotPcmkRole", - resource_id="resource", - ) - ], - ) + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._primitive_to_dto(primitive_xml) + self.assertEqual(cm.exception.resource_id, "resource") def test_target_role(self): for role in PCMK_ROLES: @@ -483,76 +557,47 @@ class TestPrimitiveStatusToDto(TestCase): fixture_primitive_xml(target_role=role) ) - result = status._primitive_to_dto( - self.report_processor, primitive_xml - ) + result = status._primitive_to_dto(primitive_xml) self.assertEqual( result, fixture_primitive_dto(target_role=role) ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_invalid_target_role(self): for value in PCMK_STATUS_ROLES_PENDING + ("NotPcmkRole",): with self.subTest(value=value): - self.setUp() primitive_xml = etree.fromstring( fixture_primitive_xml(target_role=value) ) - result = status._primitive_to_dto( - self.report_processor, primitive_xml - ) - - self.assertEqual( - result, fixture_primitive_dto(target_role=PCMK_ROLE_UNKNOWN) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.warn( - reports.codes.CLUSTER_STATUS_UNKNOWN_PCMK_ROLE, - role=value, - resource_id="resource", - ) - ], - ) + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._primitive_to_dto(primitive_xml) + self.assertEqual(cm.exception.resource_id, "resource") class TestGroupStatusToDto(TestCase): # pylint: disable=protected-access - def setUp(self): - self.report_processor = MockLibraryReportProcessor() - def test_all_attributes(self): group_xml = etree.fromstring( fixture_group_xml(description="Test description") ) - result = status._group_to_dto(self.report_processor, group_xml) + result = status._group_to_dto(group_xml) self.assertEqual( result, fixture_group_dto(description="Test description") ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_single_member(self): group_xml = etree.fromstring( fixture_group_xml(members=[fixture_primitive_xml()]) ) - result = status._group_to_dto(self.report_processor, group_xml) + result = status._group_to_dto(group_xml) self.assertEqual( result, fixture_group_dto(members=[fixture_primitive_dto()]) ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_multiple_members(self): group_xml = etree.fromstring( @@ -564,7 +609,7 @@ class TestGroupStatusToDto(TestCase): ) ) - result = status._group_to_dto(self.report_processor, group_xml) + result = status._group_to_dto(group_xml) self.assertEqual( result, @@ -575,9 +620,6 @@ class TestGroupStatusToDto(TestCase): ] ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_multiple_members_different_state(self): group_xml = etree.fromstring( @@ -594,7 +636,7 @@ class TestGroupStatusToDto(TestCase): ) ) - result = status._group_to_dto(self.report_processor, group_xml) + result = status._group_to_dto(group_xml) self.assertEqual( result, @@ -610,10 +652,31 @@ class TestGroupStatusToDto(TestCase): ] ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] + + def test_member_invalid_role(self): + group_xml = etree.fromstring( + fixture_group_xml( + members=[fixture_primitive_xml(role="NotPcmkRole")] + ) + ) + + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._group_to_dto(group_xml) + self.assertEqual(cm.exception.resource_id, "resource") + self.assertEqual(cm.exception.role, "NotPcmkRole") + + def test_member_invalid_target_role(self): + group_xml = etree.fromstring( + fixture_group_xml( + members=[fixture_primitive_xml(target_role="NotPcmkRole")] + ) ) + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._group_to_dto(group_xml) + self.assertEqual(cm.exception.resource_id, "resource") + self.assertEqual(cm.exception.role, "NotPcmkRole") + def test_invalid_member(self): resources = { "inner-group": '', @@ -623,31 +686,17 @@ class TestGroupStatusToDto(TestCase): for resource_id, member in resources.items(): with self.subTest(value=resource_id): - self.setUp() group_xml = etree.fromstring( fixture_group_xml( resource_id="outer-group", members=[member] ) ) - # pylint: disable=cell-var-from-loop - assert_raise_library_error( - lambda: status._group_to_dto( - self.report_processor, group_xml - ) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_UNEXPECTED_MEMBER, - resource_id="outer-group", - resource_type="group", - member_id=resource_id, - expected_types=["primitive"], - ) - ], - ) + 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.member_id, resource_id) + self.assertEqual(cm.exception.expected_types, ["primitive"]) def test_remove_clone_suffix(self): group_xml = etree.fromstring( @@ -657,21 +706,15 @@ class TestGroupStatusToDto(TestCase): ) ) - result = status._group_to_dto(self.report_processor, group_xml, True) + result = status._group_to_dto(group_xml, True) self.assertEqual( result, fixture_group_dto(members=[fixture_primitive_dto()]), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) class TestCloneStatusToDto(TestCase): # pylint: disable=protected-access - def setUp(self): - self.report_processor = MockLibraryReportProcessor() - def test_all_attributes(self): clone_xml = etree.fromstring( fixture_clone_xml( @@ -680,7 +723,7 @@ class TestCloneStatusToDto(TestCase): ) ) - result = status._clone_to_dto(self.report_processor, clone_xml) + result = status._clone_to_dto(clone_xml) self.assertEqual( result, @@ -688,23 +731,17 @@ class TestCloneStatusToDto(TestCase): description="Test description", target_role=PCMK_ROLE_STARTED ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_primitive_member(self): clone_xml = etree.fromstring( fixture_clone_xml(instances=[fixture_primitive_xml()]) ) - result = status._clone_to_dto(self.report_processor, clone_xml) + result = status._clone_to_dto(clone_xml) self.assertEqual( result, fixture_clone_dto(instances=[fixture_primitive_dto()]) ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_primitive_member_multiple(self): clone_xml = etree.fromstring( @@ -716,7 +753,7 @@ class TestCloneStatusToDto(TestCase): ) ) - result = status._clone_to_dto(self.report_processor, clone_xml) + result = status._clone_to_dto(clone_xml) self.assertEqual( result, @@ -727,9 +764,6 @@ class TestCloneStatusToDto(TestCase): ] ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_primitive_member_unique(self): clone_xml = etree.fromstring( @@ -744,7 +778,7 @@ class TestCloneStatusToDto(TestCase): ) ) - result = status._clone_to_dto(self.report_processor, clone_xml) + result = status._clone_to_dto(clone_xml) self.assertEqual( result, @@ -756,9 +790,6 @@ class TestCloneStatusToDto(TestCase): ], ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_primitive_member_promotable(self): clone_xml = etree.fromstring( @@ -772,7 +803,7 @@ class TestCloneStatusToDto(TestCase): ], ) ) - result = status._clone_to_dto(self.report_processor, clone_xml) + result = status._clone_to_dto(clone_xml) self.assertEqual( result, @@ -786,10 +817,31 @@ class TestCloneStatusToDto(TestCase): ], ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] + + def test_primitive_member_invalid_role(self): + clone_xml = etree.fromstring( + fixture_clone_xml( + instances=[fixture_primitive_xml(role="NotPcmkRole")] + ) + ) + + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._clone_to_dto(clone_xml) + self.assertEqual(cm.exception.resource_id, "resource") + self.assertEqual(cm.exception.role, "NotPcmkRole") + + def test_primitive_member_invalid_target_role(self): + clone_xml = etree.fromstring( + fixture_clone_xml( + instances=[fixture_primitive_xml(target_role="NotPcmkRole")] + ) ) + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._clone_to_dto(clone_xml) + self.assertEqual(cm.exception.resource_id, "resource") + self.assertEqual(cm.exception.role, "NotPcmkRole") + def test_primitive_member_different_ids(self): clone_xml = etree.fromstring( fixture_clone_xml( @@ -802,18 +854,9 @@ class TestCloneStatusToDto(TestCase): ) ) - assert_raise_library_error( - lambda: status._clone_to_dto(self.report_processor, clone_xml) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_CLONE_MEMBERS_DIFFERENT_IDS, - clone_id="resource-clone", - ) - ], - ) + with self.assertRaises(status.DifferentMemberIdsError) as cm: + status._clone_to_dto(clone_xml) + self.assertEqual(cm.exception.resource_id, "resource-clone") def test_group_member(self): clone_xml = etree.fromstring( @@ -830,7 +873,7 @@ class TestCloneStatusToDto(TestCase): ], ) ) - result = status._clone_to_dto(self.report_processor, clone_xml) + result = status._clone_to_dto(clone_xml) self.assertEqual( result, @@ -843,9 +886,6 @@ class TestCloneStatusToDto(TestCase): ], ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_group_member_unique(self): clone_xml = etree.fromstring( @@ -869,7 +909,7 @@ class TestCloneStatusToDto(TestCase): ], ) ) - result = status._clone_to_dto(self.report_processor, clone_xml) + result = status._clone_to_dto(clone_xml) self.assertEqual( result, @@ -883,9 +923,6 @@ class TestCloneStatusToDto(TestCase): ], ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_group_member_different_group_ids(self): clone_xml = etree.fromstring( @@ -903,18 +940,9 @@ class TestCloneStatusToDto(TestCase): ) ) - assert_raise_library_error( - lambda: status._clone_to_dto(self.report_processor, clone_xml) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_CLONE_MEMBERS_DIFFERENT_IDS, - clone_id="resource-clone", - ) - ], - ) + with self.assertRaises(status.DifferentMemberIdsError) as cm: + status._clone_to_dto(clone_xml) + self.assertEqual(cm.exception.resource_id, "resource-clone") def test_group_member_different_primitive_ids(self): clone_xml = etree.fromstring( @@ -937,18 +965,9 @@ class TestCloneStatusToDto(TestCase): ) ) - assert_raise_library_error( - lambda: status._clone_to_dto(self.report_processor, clone_xml) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_CLONE_MEMBERS_DIFFERENT_IDS, - clone_id="resource-clone", - ) - ], - ) + with self.assertRaises(status.DifferentMemberIdsError) as cm: + status._clone_to_dto(clone_xml) + self.assertEqual(cm.exception.resource_id, "resource-clone") def test_primitive_member_types_mixed(self): clone_xml = etree.fromstring( @@ -965,18 +984,9 @@ class TestCloneStatusToDto(TestCase): ) ) - assert_raise_library_error( - lambda: status._clone_to_dto(self.report_processor, clone_xml) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_CLONE_MIXED_MEMBERS, - clone_id="resource-clone", - ) - ], - ) + with self.assertRaises(status.MixedMembersError) as cm: + status._clone_to_dto(clone_xml) + self.assertEqual(cm.exception.resource_id, "resource-clone") def test_invalid_member(self): resources = { @@ -985,61 +995,43 @@ class TestCloneStatusToDto(TestCase): } for resource_id, element in resources.items(): with self.subTest(value=resource_id): - self.setUp() clone_xml = etree.fromstring( - fixture_clone_xml(instances=[element]) - ) - - # pylint: disable=cell-var-from-loop - assert_raise_library_error( - lambda: status._clone_to_dto( - self.report_processor, clone_xml + fixture_clone_xml( + resource_id="outer-clone", instances=[element] ) ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_UNEXPECTED_MEMBER, - resource_id="resource-clone", - resource_type="clone", - member_id=resource_id, - expected_types=["primitive", "group"], - ) - ], + + 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.member_id, resource_id) + self.assertEqual( + cm.exception.expected_types, ["primitive", "group"] ) class TestBundleReplicaStatusToDto(TestCase): # pylint: disable=protected-access def setUp(self): - self.report_processor = MockLibraryReportProcessor() + self.bundle_id = "resource-bundle" + self.bundle_type = "podman" def test_no_member_no_ip(self): replica_xml = etree.fromstring(fixture_replica_xml()) - bundle_id = "resource-bundle" - bundle_type = "podman" result = status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type + replica_xml, self.bundle_id, self.bundle_type ) self.assertEqual(result, fixture_replica_dto()) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_no_member(self): replica_xml = etree.fromstring(fixture_replica_xml(ip=True)) - bundle_id = "resource-bundle" - bundle_type = "podman" result = status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type + replica_xml, self.bundle_id, self.bundle_type ) self.assertEqual(result, fixture_replica_dto(ip=True)) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_member(self): replica_xml = etree.fromstring( @@ -1051,10 +1043,8 @@ class TestBundleReplicaStatusToDto(TestCase): ) ) - bundle_id = "resource-bundle" - bundle_type = "podman" result = status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type + replica_xml, self.bundle_id, self.bundle_type ) self.assertEqual( result, @@ -1063,9 +1053,6 @@ class TestBundleReplicaStatusToDto(TestCase): member=fixture_primitive_dto(node_names=["resource-bundle-0"]), ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_member_no_ip(self): replica_xml = etree.fromstring( @@ -1076,10 +1063,8 @@ class TestBundleReplicaStatusToDto(TestCase): ) ) - bundle_id = "resource-bundle" - bundle_type = "podman" result = status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type + replica_xml, self.bundle_id, self.bundle_type ) self.assertEqual( result, @@ -1087,10 +1072,35 @@ class TestBundleReplicaStatusToDto(TestCase): member=fixture_primitive_dto(node_names=["resource-bundle-0"]) ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] + + def test_invalid_role(self): + replica_xml = etree.fromstring( + fixture_replica_xml( + member=fixture_primitive_xml(role="NotPcmkRole") + ) + ) + + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._replica_to_dto( + replica_xml, self.bundle_id, self.bundle_type + ) + self.assertEqual(cm.exception.resource_id, "resource") + self.assertEqual(cm.exception.role, "NotPcmkRole") + + def test_invalid_target_role(self): + replica_xml = etree.fromstring( + fixture_replica_xml( + member=fixture_primitive_xml(target_role="NotPcmkRole") + ) ) + with self.assertRaises(status.UnknownPcmkRoleError) as cm: + status._replica_to_dto( + replica_xml, self.bundle_id, self.bundle_type + ) + self.assertEqual(cm.exception.resource_id, "resource") + self.assertEqual(cm.exception.role, "NotPcmkRole") + def test_no_container(self): replica_xml = etree.fromstring( """ @@ -1108,44 +1118,28 @@ class TestBundleReplicaStatusToDto(TestCase): """ ) - bundle_id = "resource-bundle" - bundle_type = "podman" - assert_raise_library_error( - lambda: status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type + with self.assertRaises( + status.BundleReplicaMissingImplicitResourceError + ) as cm: + status._replica_to_dto( + replica_xml, self.bundle_id, self.bundle_type ) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_BUNDLE_REPLICA_NO_CONTAINER, - bundle_id=bundle_id, - replica_id="0", - ) - ], - ) + self.assertEqual(cm.exception.resource_id, self.bundle_id) + self.assertEqual(cm.exception.replica_id, "0") + self.assertEqual(cm.exception.implicit_type, "container") def test_empty_replica(self): replica_xml = etree.fromstring('') - bundle_id = "resource-bundle" - bundle_type = "podman" - assert_raise_library_error( - lambda: status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type + with self.assertRaises( + status.BundleReplicaMissingImplicitResourceError + ) as cm: + status._replica_to_dto( + replica_xml, self.bundle_id, self.bundle_type ) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_BUNDLE_REPLICA_NO_CONTAINER, - bundle_id=bundle_id, - replica_id="0", - ) - ], - ) + self.assertEqual(cm.exception.resource_id, self.bundle_id) + self.assertEqual(cm.exception.replica_id, "0") + self.assertEqual(cm.exception.implicit_type, "container") def test_member_no_remote(self): replica_xml = etree.fromstring( @@ -1159,23 +1153,15 @@ class TestBundleReplicaStatusToDto(TestCase): """ ) - bundle_id = "resource-bundle" - bundle_type = "podman" - assert_raise_library_error( - lambda: status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type + with self.assertRaises( + status.BundleReplicaMissingImplicitResourceError + ) as cm: + status._replica_to_dto( + replica_xml, self.bundle_id, self.bundle_type ) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_BUNDLE_REPLICA_MISSING_REMOTE, - bundle_id=bundle_id, - replica_id="0", - ) - ], - ) + self.assertEqual(cm.exception.resource_id, self.bundle_id) + self.assertEqual(cm.exception.replica_id, "0") + self.assertEqual(cm.exception.implicit_type, "remote") def test_member_same_id_as_container(self): # xml taken from crm_mon output @@ -1198,22 +1184,14 @@ class TestBundleReplicaStatusToDto(TestCase): """ ) - bundle_id = "resource-bundle" - bundle_type = "podman" - result = status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type - ) - self.assertTrue(result is None) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.warn( - reports.codes.CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT, - bundle_id=bundle_id, - bad_ids=["resource-bundle-podman-0"], - ) - ], - ) + with self.assertRaises( + status.BundleSameIdAsImplicitResourceError + ) as cm: + status._replica_to_dto( + replica_xml, self.bundle_id, self.bundle_type + ) + self.assertEqual(cm.exception.bundle_id, self.bundle_id) + self.assertEqual(cm.exception.bad_ids, ["resource-bundle-podman-0"]) def test_member_same_id_as_remote(self): # xml taken from crm_mon output @@ -1233,22 +1211,14 @@ class TestBundleReplicaStatusToDto(TestCase): """ ) - bundle_id = "resource-bundle" - bundle_type = "podman" - result = status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type - ) - self.assertTrue(result is None) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.warn( - reports.codes.CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT, - bundle_id=bundle_id, - bad_ids=["resource-bundle-0"], - ) - ], - ) + with self.assertRaises( + status.BundleSameIdAsImplicitResourceError + ) as cm: + status._replica_to_dto( + replica_xml, self.bundle_id, self.bundle_type + ) + self.assertEqual(cm.exception.bundle_id, self.bundle_id) + self.assertEqual(cm.exception.bad_ids, ["resource-bundle-0"]) def test_member_same_id_as_ip(self): # xml taken from crm_mon output @@ -1271,22 +1241,15 @@ class TestBundleReplicaStatusToDto(TestCase): """ ) - bundle_id = "resource-bundle" - bundle_type = "podman" - - result = status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type - ) - self.assertTrue(result is None) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.warn( - reports.codes.CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT, - bundle_id=bundle_id, - bad_ids=["resource-bundle-ip-192.168.122.250"], - ) - ], + with self.assertRaises( + status.BundleSameIdAsImplicitResourceError + ) as cm: + status._replica_to_dto( + replica_xml, self.bundle_id, self.bundle_type + ) + self.assertEqual(cm.exception.bundle_id, self.bundle_id) + self.assertEqual( + cm.exception.bad_ids, ["resource-bundle-ip-192.168.122.250"] ) def test_too_many_members(self): @@ -1312,42 +1275,27 @@ class TestBundleReplicaStatusToDto(TestCase): """ ) - bundle_id = "resource-bundle" - bundle_type = "podman" - assert_raise_library_error( - lambda: status._replica_to_dto( - self.report_processor, replica_xml, bundle_id, bundle_type + with self.assertRaises( + status.BundleReplicaInvalidMemberCountError + ) as cm: + status._replica_to_dto( + replica_xml, self.bundle_id, self.bundle_type ) - ) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_BUNDLE_REPLICA_INVALID_COUNT, - bundle_id=bundle_id, - replica_id="0", - ) - ], - ) + self.assertEqual(cm.exception.resource_id, self.bundle_id) + self.assertEqual(cm.exception.replica_id, "0") class TestBundleStatusToDto(TestCase): # pylint: disable=protected-access - def setUp(self): - self.report_processor = MockLibraryReportProcessor() - def test_no_member(self): bundle_xml = etree.fromstring( fixture_bundle_xml(replicas=[fixture_replica_xml()]) ) - result = status._bundle_to_dto(self.report_processor, bundle_xml, False) + result = status._bundle_to_dto(bundle_xml, False) self.assertEqual( result, fixture_bundle_dto(replicas=[fixture_replica_dto()]) ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_member(self): bundle_xml = etree.fromstring( @@ -1362,7 +1310,7 @@ class TestBundleStatusToDto(TestCase): ] ) ) - result = status._bundle_to_dto(self.report_processor, bundle_xml, False) + result = status._bundle_to_dto(bundle_xml, False) self.assertEqual( result, fixture_bundle_dto( @@ -1376,9 +1324,6 @@ class TestBundleStatusToDto(TestCase): ] ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_multiple_replicas(self): bundle_xml = etree.fromstring( @@ -1401,7 +1346,7 @@ class TestBundleStatusToDto(TestCase): ] ) ) - result = status._bundle_to_dto(self.report_processor, bundle_xml, False) + result = status._bundle_to_dto(bundle_xml, False) self.assertEqual( result, fixture_bundle_dto( @@ -1423,9 +1368,6 @@ class TestBundleStatusToDto(TestCase): ] ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) def test_same_id_as_implicit(self): bundle_xml = etree.fromstring( @@ -1447,18 +1389,13 @@ class TestBundleStatusToDto(TestCase): """ ) - result = status._bundle_to_dto(self.report_processor, bundle_xml, False) - self.assertTrue(result is None) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.warn( - reports.codes.CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT, - bundle_id="resource-bundle", - bad_ids=["resource-bundle-0"], - ) - ], - ) + + with self.assertRaises( + status.BundleSameIdAsImplicitResourceError + ) as cm: + status._bundle_to_dto(bundle_xml, False) + self.assertEqual(cm.exception.bundle_id, "resource-bundle") + self.assertEqual(cm.exception.bad_ids, ["resource-bundle-0"]) def test_same_id_as_implicit_multiple_replicas(self): bundle_xml = etree.fromstring( @@ -1491,18 +1428,12 @@ class TestBundleStatusToDto(TestCase): """ ) - result = status._bundle_to_dto(self.report_processor, bundle_xml, False) - self.assertTrue(result is None) - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.warn( - reports.codes.CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT, - bundle_id="resource-bundle", - bad_ids=["resource-bundle-1"], - ) - ], - ) + with self.assertRaises( + status.BundleSameIdAsImplicitResourceError + ) as cm: + status._bundle_to_dto(bundle_xml, False) + self.assertEqual(cm.exception.bundle_id, "resource-bundle") + self.assertEqual(cm.exception.bad_ids, ["resource-bundle-1"]) def test_replicas_different(self): replicas = { @@ -1522,8 +1453,6 @@ class TestBundleStatusToDto(TestCase): } for name, element in replicas.items(): with self.subTest(value=name): - self.setUp() - bundle_xml = etree.fromstring( fixture_bundle_xml( replicas=[ @@ -1537,47 +1466,29 @@ class TestBundleStatusToDto(TestCase): ) ) - # pylint: disable=cell-var-from-loop - assert_raise_library_error( - lambda: status._bundle_to_dto( - self.report_processor, bundle_xml - ) - ) - - assert_report_item_list_equal( - self.report_processor.report_item_list, - [ - fixture.error( - reports.codes.CLUSTER_STATUS_BUNDLE_DIFFERENT_REPLICAS, - bundle_id="resource-bundle", - ) - ], - ) + with self.assertRaises(status.BundleDifferentReplicas) as cm: + status._bundle_to_dto(bundle_xml) + self.assertEqual(cm.exception.resource_id, "resource-bundle") class TestResourcesStatusToDto(TestCase): - def setUp(self): - self.report_processor = MockLibraryReportProcessor() - def test_empty_resources(self): status_xml = etree.fromstring(fixture_crm_mon_xml([])) - result = status.status_xml_to_dto(self.report_processor, status_xml) + parser = status.ClusterStatusParser(status_xml) + result = parser.status_xml_to_dto() self.assertEqual(result, ResourcesStatusDto([])) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) + assert_report_item_list_equal(parser.get_warnings(), []) def test_single_primitive(self): status_xml = etree.fromstring( fixture_crm_mon_xml([fixture_primitive_xml()]) ) - result = status.status_xml_to_dto(self.report_processor, status_xml) + parser = status.ClusterStatusParser(status_xml) + result = parser.status_xml_to_dto() self.assertEqual(result, ResourcesStatusDto([fixture_primitive_dto()])) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) + assert_report_item_list_equal(parser.get_warnings(), []) def test_single_group(self): status_xml = etree.fromstring( @@ -1586,16 +1497,15 @@ class TestResourcesStatusToDto(TestCase): ) ) - result = status.status_xml_to_dto(self.report_processor, status_xml) + parser = status.ClusterStatusParser(status_xml) + result = parser.status_xml_to_dto() self.assertEqual( result, ResourcesStatusDto( [fixture_group_dto(members=[fixture_primitive_dto()])] ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) + assert_report_item_list_equal(parser.get_warnings(), []) def test_single_clone(self): status_xml = etree.fromstring( @@ -1604,16 +1514,15 @@ class TestResourcesStatusToDto(TestCase): ) ) - result = status.status_xml_to_dto(self.report_processor, status_xml) + parser = status.ClusterStatusParser(status_xml) + result = parser.status_xml_to_dto() self.assertEqual( result, ResourcesStatusDto( [fixture_clone_dto(instances=[fixture_primitive_dto()])] ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) + assert_report_item_list_equal(parser.get_warnings(), []) def test_single_bundle(self): status_xml = etree.fromstring( @@ -1633,7 +1542,8 @@ class TestResourcesStatusToDto(TestCase): ) ) - result = status.status_xml_to_dto(self.report_processor, status_xml) + parser = status.ClusterStatusParser(status_xml) + result = parser.status_xml_to_dto() self.assertEqual( result, ResourcesStatusDto( @@ -1651,9 +1561,7 @@ class TestResourcesStatusToDto(TestCase): ] ), ) - assert_report_item_list_equal( - self.report_processor.report_item_list, [] - ) + assert_report_item_list_equal(parser.get_warnings(), []) def test_all_resource_types(self): status_xml = etree.fromstring( @@ -1675,30 +1583,30 @@ class TestResourcesStatusToDto(TestCase): ] ) ) - result = status.status_xml_to_dto(self.report_processor, status_xml) + parser = status.ClusterStatusParser(status_xml) + result = parser.status_xml_to_dto() - self.assertEqual(result.resources[0], fixture_primitive_dto()) - self.assertEqual( - result.resources[1], - fixture_group_dto(members=[fixture_primitive_dto()]), - ) self.assertEqual( - result.resources[2], - fixture_clone_dto(instances=[fixture_primitive_dto()]), - ) - self.assertEqual( - result.resources[3], - fixture_bundle_dto( - replicas=[ - fixture_replica_dto( - ip=True, - member=fixture_primitive_dto( - node_names=["resource-bundle-0"] - ), - ) + result, + ResourcesStatusDto( + [ + fixture_primitive_dto(), + fixture_group_dto(members=[fixture_primitive_dto()]), + fixture_clone_dto(instances=[fixture_primitive_dto()]), + fixture_bundle_dto( + replicas=[ + fixture_replica_dto( + ip=True, + member=fixture_primitive_dto( + node_names=["resource-bundle-0"] + ), + ) + ] + ), ] ), ) + assert_report_item_list_equal(parser.get_warnings(), []) def test_skip_bundle(self): status_xml = etree.fromstring( @@ -1726,11 +1634,12 @@ class TestResourcesStatusToDto(TestCase): ) ) - result = status.status_xml_to_dto(self.report_processor, status_xml) + parser = status.ClusterStatusParser(status_xml) + result = parser.status_xml_to_dto() self.assertEqual(result, ResourcesStatusDto([fixture_primitive_dto()])) assert_report_item_list_equal( - self.report_processor.report_item_list, + parser.get_warnings(), [ fixture.warn( reports.codes.CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT, -- 2.25.1