3142 lines
111 KiB
Diff
3142 lines
111 KiB
Diff
|
|
From 713ede0d903f4a66a17ba30b627af921c3dbbb45 Mon Sep 17 00:00:00 2001
|
||
|
|
From: Peter Romancik <promanci@redhat.com>
|
||
|
|
Date: Fri, 2 Feb 2024 10:51:27 +0100
|
||
|
|
Subject: [PATCH] add dtos and converting functions for resources status
|
||
|
|
|
||
|
|
---
|
||
|
|
pcs/Makefile.am | 2 +
|
||
|
|
pcs/cli/common/lib_wrapper.py | 1 +
|
||
|
|
pcs/common/const.py | 23 +
|
||
|
|
pcs/common/reports/codes.py | 22 +
|
||
|
|
pcs/common/reports/messages.py | 214 ++
|
||
|
|
pcs/common/status_dto.py | 91 +
|
||
|
|
pcs/lib/commands/status.py | 13 +
|
||
|
|
pcs/lib/pacemaker/status.py | 509 +++++
|
||
|
|
pcs_test/Makefile.am | 2 +
|
||
|
|
pcs_test/resources/crm_mon.all_resources.xml | 40 +
|
||
|
|
.../tier0/common/reports/test_messages.py | 142 ++
|
||
|
|
pcs_test/tier0/lib/commands/test_status.py | 139 ++
|
||
|
|
pcs_test/tier0/lib/pacemaker/test_status.py | 1741 +++++++++++++++++
|
||
|
|
13 files changed, 2939 insertions(+)
|
||
|
|
create mode 100644 pcs/common/status_dto.py
|
||
|
|
create mode 100644 pcs/lib/pacemaker/status.py
|
||
|
|
create mode 100644 pcs_test/resources/crm_mon.all_resources.xml
|
||
|
|
create mode 100644 pcs_test/tier0/lib/pacemaker/test_status.py
|
||
|
|
|
||
|
|
diff --git a/pcs/Makefile.am b/pcs/Makefile.am
|
||
|
|
index ce10b49e..88ee8b7f 100644
|
||
|
|
--- a/pcs/Makefile.am
|
||
|
|
+++ b/pcs/Makefile.am
|
||
|
|
@@ -179,6 +179,7 @@ EXTRA_DIST = \
|
||
|
|
common/tools.py \
|
||
|
|
common/types.py \
|
||
|
|
common/validate.py \
|
||
|
|
+ common/status_dto.py \
|
||
|
|
config.py \
|
||
|
|
constraint.py \
|
||
|
|
daemon/app/common.py \
|
||
|
|
@@ -363,6 +364,7 @@ EXTRA_DIST = \
|
||
|
|
lib/pacemaker/live.py \
|
||
|
|
lib/pacemaker/simulate.py \
|
||
|
|
lib/pacemaker/state.py \
|
||
|
|
+ lib/pacemaker/status.py \
|
||
|
|
lib/pacemaker/values.py \
|
||
|
|
lib/permissions/__init__.py \
|
||
|
|
lib/permissions/checker.py \
|
||
|
|
diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py
|
||
|
|
index 447cf9d8..2fd5b1b6 100644
|
||
|
|
--- a/pcs/cli/common/lib_wrapper.py
|
||
|
|
+++ b/pcs/cli/common/lib_wrapper.py
|
||
|
|
@@ -448,6 +448,7 @@ def load_module(env, middleware_factory, name):
|
||
|
|
"full_cluster_status_plaintext": (
|
||
|
|
status.full_cluster_status_plaintext
|
||
|
|
),
|
||
|
|
+ "resources_status": status.resources_status,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
diff --git a/pcs/common/const.py b/pcs/common/const.py
|
||
|
|
index 311f5171..32175677 100644
|
||
|
|
--- a/pcs/common/const.py
|
||
|
|
+++ b/pcs/common/const.py
|
||
|
|
@@ -3,6 +3,7 @@ from typing import NewType
|
||
|
|
from pcs.common.tools import Version
|
||
|
|
|
||
|
|
PcmkRoleType = NewType("PcmkRoleType", str)
|
||
|
|
+PcmkStatusRoleType = NewType("PcmkStatusRoleType", str)
|
||
|
|
PcmkOnFailAction = NewType("PcmkOnFailAction", str)
|
||
|
|
PcmkAction = NewType("PcmkAction", str)
|
||
|
|
|
||
|
|
@@ -13,6 +14,14 @@ 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")
|
||
|
|
@@ -29,6 +38,20 @@ PCMK_ROLES_RUNNING = (
|
||
|
|
(PCMK_ROLE_STARTED,) + PCMK_ROLES_PROMOTED + PCMK_ROLES_UNPROMOTED
|
||
|
|
)
|
||
|
|
PCMK_ROLES = (PCMK_ROLE_STOPPED,) + PCMK_ROLES_RUNNING
|
||
|
|
+PCMK_STATUS_ROLES_RUNNING = (
|
||
|
|
+ PCMK_STATUS_ROLE_STARTED,
|
||
|
|
+ PCMK_STATUS_ROLE_PROMOTED,
|
||
|
|
+ PCMK_STATUS_ROLE_UNPROMOTED,
|
||
|
|
+)
|
||
|
|
+PCMK_STATUS_ROLES_PENDING = (
|
||
|
|
+ PCMK_STATUS_ROLE_STARTING,
|
||
|
|
+ PCMK_STATUS_ROLE_STOPPING,
|
||
|
|
+)
|
||
|
|
+PCMK_STATUS_ROLES = (
|
||
|
|
+ PCMK_STATUS_ROLES_RUNNING
|
||
|
|
+ + PCMK_STATUS_ROLES_PENDING
|
||
|
|
+ + (PCMK_STATUS_ROLE_STOPPED,)
|
||
|
|
+)
|
||
|
|
PCMK_ACTION_START = PcmkAction("start")
|
||
|
|
PCMK_ACTION_STOP = PcmkAction("stop")
|
||
|
|
PCMK_ACTION_PROMOTE = PcmkAction("promote")
|
||
|
|
diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py
|
||
|
|
index 188295f2..417a3f4a 100644
|
||
|
|
--- a/pcs/common/reports/codes.py
|
||
|
|
+++ b/pcs/common/reports/codes.py
|
||
|
|
@@ -156,6 +156,28 @@ 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 e37cdf7c..1e98711c 100644
|
||
|
|
--- a/pcs/common/reports/messages.py
|
||
|
|
+++ b/pcs/common/reports/messages.py
|
||
|
|
@@ -3276,6 +3276,220 @@ class BadClusterStateFormat(ReportItemMessage):
|
||
|
|
return "cannot load cluster status, xml does not conform to the schema"
|
||
|
|
|
||
|
|
|
||
|
|
+@dataclass(frozen=True)
|
||
|
|
+class ClusterStatusUnknownPcmkRole(ReportItemMessage):
|
||
|
|
+ """
|
||
|
|
+ Value of pcmk role in the status xml is not valid
|
||
|
|
+
|
||
|
|
+ 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):
|
||
|
|
+ """
|
||
|
|
+ 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
|
||
|
|
+
|
||
|
|
+ @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."
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@dataclass(frozen=True)
|
||
|
|
+class ClusterStatusBundleMemberIdAsImplicit(ReportItemMessage):
|
||
|
|
+ """
|
||
|
|
+ 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
|
||
|
|
+ """
|
||
|
|
+
|
||
|
|
+ bundle_id: str
|
||
|
|
+ bad_ids: list[str]
|
||
|
|
+ _code = codes.CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT
|
||
|
|
+
|
||
|
|
+ @property
|
||
|
|
+ def message(self) -> str:
|
||
|
|
+ return (
|
||
|
|
+ "Skipping bundle '{bundle_id}': {resource_word} "
|
||
|
|
+ "{bad_ids} {has} the same id as some of the "
|
||
|
|
+ "implicit bundle resources."
|
||
|
|
+ ).format(
|
||
|
|
+ bundle_id=self.bundle_id,
|
||
|
|
+ resource_word=format_plural(self.bad_ids, "resource"),
|
||
|
|
+ bad_ids=format_list(self.bad_ids),
|
||
|
|
+ has=format_plural(self.bad_ids, "has"),
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@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/common/status_dto.py b/pcs/common/status_dto.py
|
||
|
|
new file mode 100644
|
||
|
|
index 00000000..dcc94eca
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/pcs/common/status_dto.py
|
||
|
|
@@ -0,0 +1,91 @@
|
||
|
|
+from dataclasses import dataclass
|
||
|
|
+from typing import (
|
||
|
|
+ Optional,
|
||
|
|
+ Sequence,
|
||
|
|
+ Union,
|
||
|
|
+)
|
||
|
|
+
|
||
|
|
+from pcs.common.const import (
|
||
|
|
+ PcmkRoleType,
|
||
|
|
+ PcmkStatusRoleType,
|
||
|
|
+)
|
||
|
|
+from pcs.common.interface.dto import DataTransferObject
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@dataclass(frozen=True)
|
||
|
|
+class PrimitiveStatusDto(DataTransferObject):
|
||
|
|
+ # pylint: disable=too-many-instance-attributes
|
||
|
|
+ resource_id: str
|
||
|
|
+ resource_agent: str
|
||
|
|
+ role: PcmkStatusRoleType
|
||
|
|
+ target_role: Optional[PcmkRoleType]
|
||
|
|
+ active: bool
|
||
|
|
+ orphaned: bool
|
||
|
|
+ blocked: bool
|
||
|
|
+ maintenance: bool
|
||
|
|
+ description: Optional[str]
|
||
|
|
+ failed: bool
|
||
|
|
+ managed: bool
|
||
|
|
+ failure_ignored: bool
|
||
|
|
+ node_names: list[str]
|
||
|
|
+ pending: Optional[str]
|
||
|
|
+ locked_to: Optional[str]
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@dataclass(frozen=True)
|
||
|
|
+class GroupStatusDto(DataTransferObject):
|
||
|
|
+ resource_id: str
|
||
|
|
+ maintenance: bool
|
||
|
|
+ description: Optional[str]
|
||
|
|
+ managed: bool
|
||
|
|
+ disabled: bool
|
||
|
|
+ members: Sequence[PrimitiveStatusDto]
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@dataclass(frozen=True)
|
||
|
|
+class CloneStatusDto(DataTransferObject):
|
||
|
|
+ # pylint: disable=too-many-instance-attributes
|
||
|
|
+ resource_id: str
|
||
|
|
+ multi_state: bool
|
||
|
|
+ unique: bool
|
||
|
|
+ maintenance: bool
|
||
|
|
+ description: Optional[str]
|
||
|
|
+ managed: bool
|
||
|
|
+ disabled: bool
|
||
|
|
+ failed: bool
|
||
|
|
+ failure_ignored: bool
|
||
|
|
+ target_role: Optional[PcmkRoleType]
|
||
|
|
+ instances: Union[Sequence[PrimitiveStatusDto], Sequence[GroupStatusDto]]
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@dataclass(frozen=True)
|
||
|
|
+class BundleReplicaStatusDto(DataTransferObject):
|
||
|
|
+ replica_id: str
|
||
|
|
+ member: Optional[PrimitiveStatusDto]
|
||
|
|
+ remote: Optional[PrimitiveStatusDto]
|
||
|
|
+ container: PrimitiveStatusDto
|
||
|
|
+ ip_address: Optional[PrimitiveStatusDto]
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@dataclass(frozen=True)
|
||
|
|
+class BundleStatusDto(DataTransferObject):
|
||
|
|
+ # pylint: disable=too-many-instance-attributes
|
||
|
|
+ resource_id: str
|
||
|
|
+ type: str
|
||
|
|
+ image: str
|
||
|
|
+ unique: bool
|
||
|
|
+ maintenance: bool
|
||
|
|
+ description: Optional[str]
|
||
|
|
+ managed: bool
|
||
|
|
+ failed: bool
|
||
|
|
+ replicas: Sequence[BundleReplicaStatusDto]
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+AnyResourceStatusDto = Union[
|
||
|
|
+ PrimitiveStatusDto, GroupStatusDto, CloneStatusDto, BundleStatusDto
|
||
|
|
+]
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@dataclass(frozen=True)
|
||
|
|
+class ResourcesStatusDto(DataTransferObject):
|
||
|
|
+ resources: Sequence[AnyResourceStatusDto]
|
||
|
|
diff --git a/pcs/lib/commands/status.py b/pcs/lib/commands/status.py
|
||
|
|
index ec7848d1..8b644ac1 100644
|
||
|
|
--- a/pcs/lib/commands/status.py
|
||
|
|
+++ b/pcs/lib/commands/status.py
|
||
|
|
@@ -17,6 +17,7 @@ from pcs.common.node_communicator import Communicator
|
||
|
|
from pcs.common.reports import ReportProcessor
|
||
|
|
from pcs.common.reports.item import ReportItem
|
||
|
|
from pcs.common.services.interfaces import ServiceManagerInterface
|
||
|
|
+from pcs.common.status_dto import ResourcesStatusDto
|
||
|
|
from pcs.common.str_tools import (
|
||
|
|
format_list,
|
||
|
|
indent,
|
||
|
|
@@ -48,6 +49,7 @@ 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.resource_agent.const import STONITH_ACTION_REPLACED_BY
|
||
|
|
from pcs.lib.sbd import get_sbd_service_name
|
||
|
|
|
||
|
|
@@ -69,6 +71,17 @@ def pacemaker_status_xml(env: LibraryEnvironment) -> str:
|
||
|
|
raise LibraryError(output=stdout)
|
||
|
|
|
||
|
|
|
||
|
|
+def resources_status(env: LibraryEnvironment) -> ResourcesStatusDto:
|
||
|
|
+ """
|
||
|
|
+ Return pacemaker status of configured resources as DTO
|
||
|
|
+
|
||
|
|
+ env -- LibraryEnvironment
|
||
|
|
+ """
|
||
|
|
+ status_xml = env.get_cluster_state()
|
||
|
|
+
|
||
|
|
+ return status_xml_to_dto(env.report_processor, status_xml)
|
||
|
|
+
|
||
|
|
+
|
||
|
|
def full_cluster_status_plaintext(
|
||
|
|
env: LibraryEnvironment,
|
||
|
|
hide_inactive_resources: bool = False,
|
||
|
|
diff --git a/pcs/lib/pacemaker/status.py b/pcs/lib/pacemaker/status.py
|
||
|
|
new file mode 100644
|
||
|
|
index 00000000..722ce03f
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/pcs/lib/pacemaker/status.py
|
||
|
|
@@ -0,0 +1,509 @@
|
||
|
|
+from typing import (
|
||
|
|
+ Optional,
|
||
|
|
+ Sequence,
|
||
|
|
+ Union,
|
||
|
|
+ cast,
|
||
|
|
+)
|
||
|
|
+
|
||
|
|
+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,
|
||
|
|
+ BundleStatusDto,
|
||
|
|
+ CloneStatusDto,
|
||
|
|
+ GroupStatusDto,
|
||
|
|
+ PrimitiveStatusDto,
|
||
|
|
+ ResourcesStatusDto,
|
||
|
|
+)
|
||
|
|
+from pcs.lib.errors import LibraryError
|
||
|
|
+from pcs.lib.pacemaker.values import is_true
|
||
|
|
+
|
||
|
|
+_PRIMITIVE_TAG = "resource"
|
||
|
|
+_GROUP_TAG = "group"
|
||
|
|
+_CLONE_TAG = "clone"
|
||
|
|
+_BUNDLE_TAG = "bundle"
|
||
|
|
+_REPLICA_TAG = "replica"
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _primitive_to_dto(
|
||
|
|
+ reporter: ReportProcessor,
|
||
|
|
+ primitive_el: _Element,
|
||
|
|
+ remove_clone_suffix: bool = False,
|
||
|
|
+) -> PrimitiveStatusDto:
|
||
|
|
+ resource_id = _get_resource_id(reporter, 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)
|
||
|
|
+
|
||
|
|
+ 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()
|
||
|
|
+
|
||
|
|
+ return PrimitiveStatusDto(
|
||
|
|
+ resource_id,
|
||
|
|
+ str(primitive_el.get("resource_agent")),
|
||
|
|
+ role,
|
||
|
|
+ target_role,
|
||
|
|
+ is_true(primitive_el.get("active", "false")),
|
||
|
|
+ is_true(primitive_el.get("orphaned", "false")),
|
||
|
|
+ is_true(primitive_el.get("blocked", "false")),
|
||
|
|
+ is_true(primitive_el.get("maintenance", "false")),
|
||
|
|
+ primitive_el.get("description"),
|
||
|
|
+ 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")],
|
||
|
|
+ primitive_el.get("pending"),
|
||
|
|
+ primitive_el.get("locked_to"),
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _group_to_dto(
|
||
|
|
+ reporter: ReportProcessor,
|
||
|
|
+ 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 = []
|
||
|
|
+
|
||
|
|
+ for member in group_el:
|
||
|
|
+ if member.tag == _PRIMITIVE_TAG:
|
||
|
|
+ members.append(
|
||
|
|
+ _primitive_to_dto(reporter, member, remove_clone_suffix)
|
||
|
|
+ )
|
||
|
|
+ else:
|
||
|
|
+ reporter.report(
|
||
|
|
+ reports.ReportItem.error(
|
||
|
|
+ reports.messages.ClusterStatusUnexpectedMember(
|
||
|
|
+ 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,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _clone_to_dto(
|
||
|
|
+ reporter: ReportProcessor,
|
||
|
|
+ clone_el: _Element,
|
||
|
|
+ _remove_clone_suffix: bool = False,
|
||
|
|
+) -> CloneStatusDto:
|
||
|
|
+ clone_id = _get_resource_id(reporter, clone_el)
|
||
|
|
+ is_unique = is_true(clone_el.get("unique", "false"))
|
||
|
|
+
|
||
|
|
+ target_role = _get_target_role(reporter, clone_el, clone_id)
|
||
|
|
+
|
||
|
|
+ primitives = []
|
||
|
|
+ groups = []
|
||
|
|
+
|
||
|
|
+ for member in clone_el:
|
||
|
|
+ if member.tag == _PRIMITIVE_TAG:
|
||
|
|
+ primitives.append(_primitive_to_dto(reporter, member, is_unique))
|
||
|
|
+ elif member.tag == _GROUP_TAG:
|
||
|
|
+ groups.append(_group_to_dto(reporter, member, is_unique))
|
||
|
|
+ else:
|
||
|
|
+ reporter.report(
|
||
|
|
+ reports.ReportItem.error(
|
||
|
|
+ reports.messages.ClusterStatusUnexpectedMember(
|
||
|
|
+ clone_id,
|
||
|
|
+ "clone",
|
||
|
|
+ str(member.get("id")),
|
||
|
|
+ ["primitive", "group"],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ reporter.report_list(
|
||
|
|
+ _validate_mixed_instance_types(primitives, groups, clone_id)
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ instances: Union[list[PrimitiveStatusDto], list[GroupStatusDto]]
|
||
|
|
+ if primitives:
|
||
|
|
+ reporter.report_list(
|
||
|
|
+ _validate_primitive_instance_ids(primitives, clone_id)
|
||
|
|
+ )
|
||
|
|
+ instances = primitives
|
||
|
|
+ else:
|
||
|
|
+ reporter.report_list(_validate_group_instance_ids(groups, clone_id))
|
||
|
|
+ instances = groups
|
||
|
|
+
|
||
|
|
+ if reporter.has_errors:
|
||
|
|
+ raise LibraryError()
|
||
|
|
+
|
||
|
|
+ return CloneStatusDto(
|
||
|
|
+ clone_id,
|
||
|
|
+ is_true(clone_el.get("multi_state", "false")),
|
||
|
|
+ is_unique,
|
||
|
|
+ is_true(clone_el.get("maintenance", "false")),
|
||
|
|
+ clone_el.get("description"),
|
||
|
|
+ is_true(clone_el.get("managed", "false")),
|
||
|
|
+ is_true(clone_el.get("disabled", "false")),
|
||
|
|
+ is_true(clone_el.get("failed", "false")),
|
||
|
|
+ is_true(clone_el.get("failure_ignored", "false")),
|
||
|
|
+ target_role,
|
||
|
|
+ instances,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+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_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))
|
||
|
|
+
|
||
|
|
+ if reporter.has_errors:
|
||
|
|
+ raise LibraryError()
|
||
|
|
+
|
||
|
|
+ return BundleStatusDto(
|
||
|
|
+ bundle_id,
|
||
|
|
+ bundle_type,
|
||
|
|
+ str(bundle_el.get("image")),
|
||
|
|
+ is_true(bundle_el.get("unique", "false")),
|
||
|
|
+ is_true(bundle_el.get("maintenance", "false")),
|
||
|
|
+ bundle_el.get("description"),
|
||
|
|
+ is_true(bundle_el.get("managed", "false")),
|
||
|
|
+ is_true(bundle_el.get("failed", "false")),
|
||
|
|
+ replicas,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+_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
|
||
|
|
+ ]
|
||
|
|
+
|
||
|
|
+ if reporter.has_errors:
|
||
|
|
+ raise LibraryError()
|
||
|
|
+
|
||
|
|
+ return ResourcesStatusDto(
|
||
|
|
+ cast(
|
||
|
|
+ list[AnyResourceStatusDto],
|
||
|
|
+ [dto for dto in resource_dtos if dto is not None],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _get_resource_id(reporter: ReportProcessor, resource: _Element) -> str:
|
||
|
|
+ resource_id = resource.get("id")
|
||
|
|
+ if not resource_id:
|
||
|
|
+ reporter.report(
|
||
|
|
+ reports.ReportItem.error(
|
||
|
|
+ reports.messages.InvalidIdIsEmpty("resource id")
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ return str(resource_id)
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _get_role(
|
||
|
|
+ reporter: ReportProcessor, resource: _Element, resource_id: str
|
||
|
|
+) -> 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
|
||
|
|
+ return PcmkStatusRoleType(role)
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _get_target_role(
|
||
|
|
+ reporter: ReportProcessor, resource: _Element, resource_id: str
|
||
|
|
+) -> 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
|
||
|
|
+ return PcmkRoleType(target_role)
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _remove_clone_suffix(resource_id: str) -> str:
|
||
|
|
+ if ":" in resource_id:
|
||
|
|
+ return resource_id.rsplit(":", 1)[0]
|
||
|
|
+ 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_id = str(replica_el.get("id"))
|
||
|
|
+
|
||
|
|
+ resources = [
|
||
|
|
+ _primitive_to_dto(reporter, resource)
|
||
|
|
+ for resource in replica_el.iterfind(_PRIMITIVE_TAG)
|
||
|
|
+ ]
|
||
|
|
+
|
||
|
|
+ duplicate_ids = _find_duplicate_ids(resources)
|
||
|
|
+ if duplicate_ids:
|
||
|
|
+ reporter.report(
|
||
|
|
+ reports.ReportItem.warning(
|
||
|
|
+ reports.messages.ClusterStatusBundleMemberIdAsImplicit(
|
||
|
|
+ bundle_id, duplicate_ids
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ return None
|
||
|
|
+
|
||
|
|
+ # TODO pacemaker will probably add prefix
|
||
|
|
+ # "pcmk-internal" to all implicit resources
|
||
|
|
+
|
||
|
|
+ container_resource = _get_implicit_resource(
|
||
|
|
+ resources,
|
||
|
|
+ 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 LibraryError()
|
||
|
|
+
|
||
|
|
+ remote_resource = _get_implicit_resource(
|
||
|
|
+ resources, 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
|
||
|
|
+ ):
|
||
|
|
+ ip_resource = _get_implicit_resource(
|
||
|
|
+ resources, 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
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ raise LibraryError()
|
||
|
|
+
|
||
|
|
+ member = None
|
||
|
|
+ if remote_resource:
|
||
|
|
+ if len(resources) == 1:
|
||
|
|
+ member = resources[0]
|
||
|
|
+ else:
|
||
|
|
+ reporter.report(
|
||
|
|
+ reports.ReportItem.error(
|
||
|
|
+ reports.messages.ClusterStatusBundleReplicaInvalidCount(
|
||
|
|
+ bundle_id, replica_id
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ raise LibraryError()
|
||
|
|
+
|
||
|
|
+ return BundleReplicaStatusDto(
|
||
|
|
+ replica_id,
|
||
|
|
+ member,
|
||
|
|
+ remote_resource,
|
||
|
|
+ container_resource,
|
||
|
|
+ ip_resource,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+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],
|
||
|
|
+ expected_id: str,
|
||
|
|
+ exact_match: bool,
|
||
|
|
+ resource_agent: str,
|
||
|
|
+) -> Optional[PrimitiveStatusDto]:
|
||
|
|
+ for primitive in primitives:
|
||
|
|
+ matching_id = (
|
||
|
|
+ exact_match
|
||
|
|
+ and primitive.resource_id == expected_id
|
||
|
|
+ or not exact_match
|
||
|
|
+ and primitive.resource_id.startswith(expected_id)
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ if matching_id and primitive.resource_agent == resource_agent:
|
||
|
|
+ primitives.remove(primitive)
|
||
|
|
+ return primitive
|
||
|
|
+
|
||
|
|
+ return None
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _validate_replicas(
|
||
|
|
+ replicas: Sequence[BundleReplicaStatusDto], bundle_id: str
|
||
|
|
+) -> reports.ReportItemList:
|
||
|
|
+ if not replicas:
|
||
|
|
+ return []
|
||
|
|
+
|
||
|
|
+ member = replicas[0].member
|
||
|
|
+ ip = replicas[0].ip_address
|
||
|
|
+ container = replicas[0].container
|
||
|
|
+
|
||
|
|
+ for replica in replicas:
|
||
|
|
+ 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 []
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _cmp_replica_members(
|
||
|
|
+ left: Optional[PrimitiveStatusDto],
|
||
|
|
+ right: Optional[PrimitiveStatusDto],
|
||
|
|
+ compare_ids: bool,
|
||
|
|
+) -> bool:
|
||
|
|
+ if left is None and right is None:
|
||
|
|
+ return True
|
||
|
|
+ if left is None:
|
||
|
|
+ return False
|
||
|
|
+ if right is None:
|
||
|
|
+ return False
|
||
|
|
+
|
||
|
|
+ if left.resource_agent != right.resource_agent:
|
||
|
|
+ return False
|
||
|
|
+
|
||
|
|
+ return not compare_ids or left.resource_id == right.resource_id
|
||
|
|
diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am
|
||
|
|
index 32ac5eee..f036ded5 100644
|
||
|
|
--- a/pcs_test/Makefile.am
|
||
|
|
+++ b/pcs_test/Makefile.am
|
||
|
|
@@ -32,6 +32,7 @@ EXTRA_DIST = \
|
||
|
|
resources/corosync-qdevice.conf \
|
||
|
|
resources/corosync-some-node-names.conf \
|
||
|
|
resources/crm_mon.minimal.xml \
|
||
|
|
+ resources/crm_mon.all_resources.xml \
|
||
|
|
resources/fenced_metadata.xml \
|
||
|
|
resources/schedulerd_metadata.xml \
|
||
|
|
resources/pcmk_api_rng/api-result.rng \
|
||
|
|
@@ -322,6 +323,7 @@ EXTRA_DIST = \
|
||
|
|
tier0/lib/pacemaker/test_live.py \
|
||
|
|
tier0/lib/pacemaker/test_simulate.py \
|
||
|
|
tier0/lib/pacemaker/test_state.py \
|
||
|
|
+ tier0/lib/pacemaker/test_status.py \
|
||
|
|
tier0/lib/pacemaker/test_values.py \
|
||
|
|
tier0/lib/permissions/__init__.py \
|
||
|
|
tier0/lib/permissions/config/__init__.py \
|
||
|
|
diff --git a/pcs_test/resources/crm_mon.all_resources.xml b/pcs_test/resources/crm_mon.all_resources.xml
|
||
|
|
new file mode 100644
|
||
|
|
index 00000000..e493d308
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/pcs_test/resources/crm_mon.all_resources.xml
|
||
|
|
@@ -0,0 +1,40 @@
|
||
|
|
+<pacemaker-result api-version="2.30" request="crm_mon --output-as xml">
|
||
|
|
+ <summary>
|
||
|
|
+ <stack type="corosync" />
|
||
|
|
+ <current_dc present="false" />
|
||
|
|
+ <last_update time="Wed Nov 6 13:45:41 2019" />
|
||
|
|
+ <last_change time="Wed Nov 6 10:42:54 2019" user="hacluster" client="crmd" origin="node1" />
|
||
|
|
+ <nodes_configured number="0" />
|
||
|
|
+ <resources_configured number="0" disabled="0" blocked="0" />
|
||
|
|
+ <cluster_options stonith-enabled="true" symmetric-cluster="true" no-quorum-policy="stop" maintenance-mode="false" stop-all-resources="false" stonith-timeout-ms="60000" priority-fencing-delay-ms="0"/>
|
||
|
|
+ </summary>
|
||
|
|
+ <nodes>
|
||
|
|
+ <node name="node1" id="1" online="true" standby="false" standby_onfail="false" maintenance="false" pending="false" unclean="false" health="green" feature_set="3.17.4" shutdown="false" expected_up="true" is_dc="false" resources_running="16" type="member"/>
|
||
|
|
+ </nodes>
|
||
|
|
+ <resources>
|
||
|
|
+ <resource id="dummy" resource_agent="ocf:pacemaker:Dummy" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <group id="group" number_resources="2" maintenance="false" managed="true" disabled="false">
|
||
|
|
+ <resource id="grouped" resource_agent="ocf:pacemaker:Dummy" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </group>
|
||
|
|
+ <clone id="clone" multi_state="false" unique="false" maintenance="false" managed="true" disabled="false" failed="false" failure_ignored="false">
|
||
|
|
+ <resource id="cloned" resource_agent="ocf:pacemaker:Dummy" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </clone>
|
||
|
|
+ <bundle id="bundle" type="podman" image="localhost/pcmktest:http" unique="false" maintenance="false" managed="true" failed="false">
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="bundle-podman-0" resource_agent="ocf:heartbeat:podman" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ </bundle>
|
||
|
|
+ </resources>
|
||
|
|
+ <status code="0" message="OK" />
|
||
|
|
+</pacemaker-result>
|
||
|
|
diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py
|
||
|
|
index 58a70a37..b60360e4 100644
|
||
|
|
--- a/pcs_test/tier0/common/reports/test_messages.py
|
||
|
|
+++ b/pcs_test/tier0/common/reports/test_messages.py
|
||
|
|
@@ -5816,3 +5816,145 @@ class CannotCreateDefaultClusterPropertySet(NameBuildTest):
|
||
|
|
"cib-bootstrap-options"
|
||
|
|
),
|
||
|
|
)
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+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):
|
||
|
|
+ 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 "
|
||
|
|
+ "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."
|
||
|
|
+ ),
|
||
|
|
+ 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):
|
||
|
|
+ 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"],
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+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.ClusterStatusUnknownPcmkRole("NotValidRole", "resource"),
|
||
|
|
+ )
|
||
|
|
diff --git a/pcs_test/tier0/lib/commands/test_status.py b/pcs_test/tier0/lib/commands/test_status.py
|
||
|
|
index ce98ec63..a5a395b5 100644
|
||
|
|
--- a/pcs_test/tier0/lib/commands/test_status.py
|
||
|
|
+++ b/pcs_test/tier0/lib/commands/test_status.py
|
||
|
|
@@ -7,7 +7,16 @@ 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.reports import codes as report_codes
|
||
|
|
+from pcs.common.status_dto import (
|
||
|
|
+ BundleReplicaStatusDto,
|
||
|
|
+ BundleStatusDto,
|
||
|
|
+ CloneStatusDto,
|
||
|
|
+ GroupStatusDto,
|
||
|
|
+ PrimitiveStatusDto,
|
||
|
|
+ ResourcesStatusDto,
|
||
|
|
+)
|
||
|
|
from pcs.lib.booth import constants
|
||
|
|
from pcs.lib.commands import status
|
||
|
|
from pcs.lib.errors import LibraryError
|
||
|
|
@@ -22,6 +31,7 @@ from pcs_test.tools.command_env.config_runner_pcmk import (
|
||
|
|
RULE_EXPIRED_RETURNCODE,
|
||
|
|
RULE_IN_EFFECT_RETURNCODE,
|
||
|
|
)
|
||
|
|
+from pcs_test.tools.misc import get_test_resource as rc
|
||
|
|
from pcs_test.tools.misc import read_test_resource as rc_read
|
||
|
|
|
||
|
|
|
||
|
|
@@ -1254,3 +1264,132 @@ class FullClusterStatusPlaintextBoothWarning(FullClusterStatusPlaintextBase):
|
||
|
|
).encode("utf-8"),
|
||
|
|
)
|
||
|
|
self._assert_status_output()
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def _fixture_primitive_resource_dto(
|
||
|
|
+ resource_id: str, resource_agent: str
|
||
|
|
+) -> PrimitiveStatusDto:
|
||
|
|
+ return PrimitiveStatusDto(
|
||
|
|
+ resource_id,
|
||
|
|
+ resource_agent,
|
||
|
|
+ PCMK_STATUS_ROLE_STARTED,
|
||
|
|
+ None,
|
||
|
|
+ True,
|
||
|
|
+ False,
|
||
|
|
+ False,
|
||
|
|
+ False,
|
||
|
|
+ None,
|
||
|
|
+ False,
|
||
|
|
+ True,
|
||
|
|
+ False,
|
||
|
|
+ ["node1"],
|
||
|
|
+ None,
|
||
|
|
+ None,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+@mock.patch.object(
|
||
|
|
+ settings,
|
||
|
|
+ "pacemaker_api_result_schema",
|
||
|
|
+ rc("pcmk_api_rng/api-result.rng"),
|
||
|
|
+)
|
||
|
|
+class ResourcesStatus(TestCase):
|
||
|
|
+ def setUp(self):
|
||
|
|
+ self.env_assist, self.config = get_env_tools(self)
|
||
|
|
+
|
||
|
|
+ def test_empty_resources(self):
|
||
|
|
+ self.config.runner.pcmk.load_state()
|
||
|
|
+
|
||
|
|
+ result = status.resources_status(self.env_assist.get_env())
|
||
|
|
+ self.assertEqual(result, ResourcesStatusDto([]))
|
||
|
|
+
|
||
|
|
+ def test_bad_xml(self):
|
||
|
|
+ self.config.runner.pcmk.load_state(
|
||
|
|
+ resources="""
|
||
|
|
+ <resources>
|
||
|
|
+ <resource />
|
||
|
|
+ </resources>
|
||
|
|
+ """,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ self.env_assist.assert_raise_library_error(
|
||
|
|
+ lambda: status.resources_status(
|
||
|
|
+ self.env_assist.get_env(),
|
||
|
|
+ ),
|
||
|
|
+ [fixture.error(report_codes.BAD_CLUSTER_STATE_FORMAT)],
|
||
|
|
+ False,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_all_resources(self):
|
||
|
|
+ self.config.runner.pcmk.load_state(
|
||
|
|
+ filename=rc("crm_mon.all_resources.xml")
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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=[
|
||
|
|
+ _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=[
|
||
|
|
+ _fixture_primitive_resource_dto(
|
||
|
|
+ "cloned", "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"
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
diff --git a/pcs_test/tier0/lib/pacemaker/test_status.py b/pcs_test/tier0/lib/pacemaker/test_status.py
|
||
|
|
new file mode 100644
|
||
|
|
index 00000000..451fb584
|
||
|
|
--- /dev/null
|
||
|
|
+++ b/pcs_test/tier0/lib/pacemaker/test_status.py
|
||
|
|
@@ -0,0 +1,1741 @@
|
||
|
|
+# pylint: disable=too-many-lines
|
||
|
|
+from typing import (
|
||
|
|
+ Optional,
|
||
|
|
+ Sequence,
|
||
|
|
+ Union,
|
||
|
|
+)
|
||
|
|
+from unittest import TestCase
|
||
|
|
+
|
||
|
|
+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,
|
||
|
|
+ PCMK_STATUS_ROLES_RUNNING,
|
||
|
|
+ PcmkStatusRoleType,
|
||
|
|
+)
|
||
|
|
+from pcs.common.status_dto import (
|
||
|
|
+ BundleReplicaStatusDto,
|
||
|
|
+ BundleStatusDto,
|
||
|
|
+ CloneStatusDto,
|
||
|
|
+ GroupStatusDto,
|
||
|
|
+ PrimitiveStatusDto,
|
||
|
|
+ ResourcesStatusDto,
|
||
|
|
+)
|
||
|
|
+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_list_equal,
|
||
|
|
+)
|
||
|
|
+from pcs_test.tools.custom_mock import MockLibraryReportProcessor
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_primitive_xml(
|
||
|
|
+ resource_id: str = "resource",
|
||
|
|
+ resource_agent: str = "ocf:heartbeat:Dummy",
|
||
|
|
+ role: PcmkStatusRoleType = PCMK_STATUS_ROLE_STARTED,
|
||
|
|
+ target_role: Optional[str] = None,
|
||
|
|
+ managed: bool = True,
|
||
|
|
+ node_names: Sequence[str] = ("node1",),
|
||
|
|
+ add_optional_args: bool = False,
|
||
|
|
+) -> str:
|
||
|
|
+ target_role = (
|
||
|
|
+ f'target_role="{target_role}"' if target_role is not None else ""
|
||
|
|
+ )
|
||
|
|
+ active = role in PCMK_STATUS_ROLES_RUNNING
|
||
|
|
+ description = 'description="Test description"' if add_optional_args else ""
|
||
|
|
+ pending = 'pending="test"' if add_optional_args else ""
|
||
|
|
+ locked_to = 'locked_to="test"' if add_optional_args else ""
|
||
|
|
+
|
||
|
|
+ nodes = "\n".join(
|
||
|
|
+ f'<node name="{node}" id="{i}" cached="true"/>'
|
||
|
|
+ for (i, node) in enumerate(node_names)
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ return f"""
|
||
|
|
+ <resource
|
||
|
|
+ id="{resource_id}"
|
||
|
|
+ resource_agent="{resource_agent}"
|
||
|
|
+ role="{role}"
|
||
|
|
+ {target_role}
|
||
|
|
+ active="{active}"
|
||
|
|
+ orphaned="false"
|
||
|
|
+ blocked="false"
|
||
|
|
+ maintenance="false"
|
||
|
|
+ {description}
|
||
|
|
+ managed="{managed}"
|
||
|
|
+ failed="false"
|
||
|
|
+ failure_ignored="false"
|
||
|
|
+ {pending}
|
||
|
|
+ {locked_to}
|
||
|
|
+ nodes_running_on="{len(node_names)}"
|
||
|
|
+ >
|
||
|
|
+ {nodes}
|
||
|
|
+ </resource>
|
||
|
|
+ """
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_primitive_dto(
|
||
|
|
+ resource_id: str = "resource",
|
||
|
|
+ resource_agent: str = "ocf:heartbeat:Dummy",
|
||
|
|
+ role: PcmkStatusRoleType = PCMK_STATUS_ROLE_STARTED,
|
||
|
|
+ target_role: Optional[str] = None,
|
||
|
|
+ managed: bool = True,
|
||
|
|
+ node_names: Sequence[str] = ("node1",),
|
||
|
|
+ add_optional_args: bool = False,
|
||
|
|
+) -> PrimitiveStatusDto:
|
||
|
|
+ return PrimitiveStatusDto(
|
||
|
|
+ resource_id,
|
||
|
|
+ resource_agent,
|
||
|
|
+ role,
|
||
|
|
+ target_role,
|
||
|
|
+ active=role in PCMK_STATUS_ROLES_RUNNING,
|
||
|
|
+ orphaned=False,
|
||
|
|
+ blocked=False,
|
||
|
|
+ maintenance=False,
|
||
|
|
+ description="Test description" if add_optional_args else None,
|
||
|
|
+ managed=managed,
|
||
|
|
+ failed=False,
|
||
|
|
+ failure_ignored=False,
|
||
|
|
+ node_names=list(node_names),
|
||
|
|
+ pending="test" if add_optional_args else None,
|
||
|
|
+ locked_to="test" if add_optional_args else None,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_group_xml(
|
||
|
|
+ resource_id: str = "resource-group",
|
||
|
|
+ description: Optional[str] = None,
|
||
|
|
+ members: Sequence[str] = (),
|
||
|
|
+) -> str:
|
||
|
|
+ description = (
|
||
|
|
+ f'description="{description}"' if description is not None else ""
|
||
|
|
+ )
|
||
|
|
+ members = "\n".join(members)
|
||
|
|
+ return f"""
|
||
|
|
+ <group
|
||
|
|
+ id="{resource_id}"
|
||
|
|
+ number_resources="{len(members)}"
|
||
|
|
+ maintenance="false"
|
||
|
|
+ {description}
|
||
|
|
+ managed="true"
|
||
|
|
+ disabled="false"
|
||
|
|
+ >
|
||
|
|
+ {members}
|
||
|
|
+ </group>
|
||
|
|
+ """
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_group_dto(
|
||
|
|
+ resource_id: str = "resource-group",
|
||
|
|
+ description: Optional[str] = None,
|
||
|
|
+ members: Sequence[PrimitiveStatusDto] = (),
|
||
|
|
+) -> GroupStatusDto:
|
||
|
|
+ return GroupStatusDto(
|
||
|
|
+ resource_id,
|
||
|
|
+ maintenance=False,
|
||
|
|
+ description=description,
|
||
|
|
+ managed=True,
|
||
|
|
+ disabled=False,
|
||
|
|
+ members=list(members),
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_clone_xml(
|
||
|
|
+ resource_id: str = "resource-clone",
|
||
|
|
+ multi_state: bool = False,
|
||
|
|
+ unique: bool = False,
|
||
|
|
+ description: Optional[str] = None,
|
||
|
|
+ target_role: Optional[str] = None,
|
||
|
|
+ instances: Sequence[str] = (),
|
||
|
|
+) -> str:
|
||
|
|
+ description = (
|
||
|
|
+ f'description="{description}"' if description is not None else ""
|
||
|
|
+ )
|
||
|
|
+ target_role = (
|
||
|
|
+ f'target_role="{target_role}"' if target_role is not None else ""
|
||
|
|
+ )
|
||
|
|
+ instances = "\n".join(instances)
|
||
|
|
+ return f"""
|
||
|
|
+ <clone
|
||
|
|
+ id="{resource_id}"
|
||
|
|
+ multi_state="{multi_state}"
|
||
|
|
+ unique="{unique}"
|
||
|
|
+ maintenance="false"
|
||
|
|
+ {description}
|
||
|
|
+ managed="true"
|
||
|
|
+ disabled="false"
|
||
|
|
+ failed="false"
|
||
|
|
+ failure_ignored="false"
|
||
|
|
+ {target_role}
|
||
|
|
+ >
|
||
|
|
+ {instances}
|
||
|
|
+ </clone>
|
||
|
|
+ """
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_clone_dto(
|
||
|
|
+ resource_id: str = "resource-clone",
|
||
|
|
+ multi_state: bool = False,
|
||
|
|
+ unique: bool = False,
|
||
|
|
+ description: Optional[str] = None,
|
||
|
|
+ target_role: Optional[str] = None,
|
||
|
|
+ instances: Union[
|
||
|
|
+ Sequence[PrimitiveStatusDto], Sequence[GroupStatusDto]
|
||
|
|
+ ] = (),
|
||
|
|
+) -> CloneStatusDto:
|
||
|
|
+ return CloneStatusDto(
|
||
|
|
+ resource_id,
|
||
|
|
+ multi_state,
|
||
|
|
+ unique,
|
||
|
|
+ maintenance=False,
|
||
|
|
+ description=description,
|
||
|
|
+ managed=True,
|
||
|
|
+ disabled=False,
|
||
|
|
+ failed=False,
|
||
|
|
+ failure_ignored=False,
|
||
|
|
+ target_role=target_role,
|
||
|
|
+ instances=list(instances),
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_replica_xml(
|
||
|
|
+ bundle_id: str = "resource-bundle",
|
||
|
|
+ replica_id: str = "0",
|
||
|
|
+ bundle_type: str = "podman",
|
||
|
|
+ ip: bool = False,
|
||
|
|
+ node_name: str = "node1",
|
||
|
|
+ member: Optional[str] = None,
|
||
|
|
+) -> str:
|
||
|
|
+ ip_resource = fixture_primitive_xml(
|
||
|
|
+ resource_id=f"{bundle_id}-ip-192.168.122.{replica_id}",
|
||
|
|
+ resource_agent="ocf:heartbeat:IPaddr2",
|
||
|
|
+ node_names=[node_name],
|
||
|
|
+ )
|
||
|
|
+ remote_resource = fixture_primitive_xml(
|
||
|
|
+ resource_id=f"{bundle_id}-{replica_id}",
|
||
|
|
+ resource_agent="ocf:pacemaker:remote",
|
||
|
|
+ node_names=[node_name],
|
||
|
|
+ )
|
||
|
|
+ container_resource = fixture_primitive_xml(
|
||
|
|
+ resource_id=f"{bundle_id}-{bundle_type}-{replica_id}",
|
||
|
|
+ resource_agent=f"ocf:heartbeat:{bundle_type}",
|
||
|
|
+ node_names=[node_name],
|
||
|
|
+ )
|
||
|
|
+ return f"""
|
||
|
|
+ <replica id="{replica_id}">
|
||
|
|
+ {ip_resource if ip else ""}
|
||
|
|
+ {member if member is not None else ""}
|
||
|
|
+ {container_resource}
|
||
|
|
+ {remote_resource if member is not None else ""}
|
||
|
|
+ </replica>
|
||
|
|
+ """
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_replica_dto(
|
||
|
|
+ bundle_id: str = "resource-bundle",
|
||
|
|
+ replica_id: str = "0",
|
||
|
|
+ bundle_type: str = "podman",
|
||
|
|
+ ip: bool = False,
|
||
|
|
+ node_name: str = "node1",
|
||
|
|
+ member: Optional[PrimitiveStatusDto] = None,
|
||
|
|
+) -> BundleReplicaStatusDto:
|
||
|
|
+ ip_resource = fixture_primitive_dto(
|
||
|
|
+ resource_id=f"{bundle_id}-ip-192.168.122.{replica_id}",
|
||
|
|
+ resource_agent="ocf:heartbeat:IPaddr2",
|
||
|
|
+ node_names=[node_name],
|
||
|
|
+ )
|
||
|
|
+ remote_resource = fixture_primitive_dto(
|
||
|
|
+ resource_id=f"{bundle_id}-{replica_id}",
|
||
|
|
+ resource_agent="ocf:pacemaker:remote",
|
||
|
|
+ node_names=[node_name],
|
||
|
|
+ )
|
||
|
|
+ container_resource = fixture_primitive_dto(
|
||
|
|
+ resource_id=f"{bundle_id}-{bundle_type}-{replica_id}",
|
||
|
|
+ resource_agent=f"ocf:heartbeat:{bundle_type}",
|
||
|
|
+ node_names=[node_name],
|
||
|
|
+ )
|
||
|
|
+ return BundleReplicaStatusDto(
|
||
|
|
+ replica_id,
|
||
|
|
+ member,
|
||
|
|
+ remote_resource if member is not None else None,
|
||
|
|
+ container_resource,
|
||
|
|
+ ip_resource if ip else None,
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_bundle_xml(
|
||
|
|
+ resource_id: str = "resource-bundle", replicas: Sequence[str] = ()
|
||
|
|
+) -> str:
|
||
|
|
+ replicas = "\n".join(replicas)
|
||
|
|
+ return f"""
|
||
|
|
+ <bundle
|
||
|
|
+ id="{resource_id}"
|
||
|
|
+ type="podman"
|
||
|
|
+ image="localhost/pcmktest:http"
|
||
|
|
+ unique="false"
|
||
|
|
+ maintenance="false"
|
||
|
|
+ managed="true"
|
||
|
|
+ failed="false"
|
||
|
|
+ >
|
||
|
|
+ {replicas}
|
||
|
|
+ </bundle>
|
||
|
|
+ """
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_bundle_dto(
|
||
|
|
+ resource_id: str = "resource-bundle",
|
||
|
|
+ replicas: Sequence[BundleReplicaStatusDto] = (),
|
||
|
|
+) -> BundleStatusDto:
|
||
|
|
+ return BundleStatusDto(
|
||
|
|
+ resource_id,
|
||
|
|
+ "podman",
|
||
|
|
+ "localhost/pcmktest:http",
|
||
|
|
+ False,
|
||
|
|
+ False,
|
||
|
|
+ None,
|
||
|
|
+ True,
|
||
|
|
+ False,
|
||
|
|
+ list(replicas),
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+def fixture_crm_mon_xml(resources: list[str]) -> str:
|
||
|
|
+ # we only care about the resources element,
|
||
|
|
+ # omitting other parts to make the string shorter
|
||
|
|
+ resources = "\n".join(resources)
|
||
|
|
+ return f"""
|
||
|
|
+ <pacemaker-result
|
||
|
|
+ api-version="2.30"
|
||
|
|
+ request="/usr/sbin/crm_mon --one-shot --inactive --output-as xml"
|
||
|
|
+ >
|
||
|
|
+ <resources>
|
||
|
|
+ {resources}
|
||
|
|
+ </resources>
|
||
|
|
+ <status code="0" message="OK"/>
|
||
|
|
+ </pacemaker-result>
|
||
|
|
+ """
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+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)
|
||
|
|
+
|
||
|
|
+ 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)
|
||
|
|
+
|
||
|
|
+ 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(
|
||
|
|
+ fixture_primitive_xml(
|
||
|
|
+ target_role=PCMK_STATUS_ROLE_STOPPED, add_optional_args=True
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status._primitive_to_dto(self.report_processor, primitive_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_primitive_dto(
|
||
|
|
+ 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
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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)
|
||
|
|
+
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_role(self):
|
||
|
|
+ for role in PCMK_STATUS_ROLES:
|
||
|
|
+ with self.subTest(value=role):
|
||
|
|
+ primitive_xml = etree.fromstring(
|
||
|
|
+ fixture_primitive_xml(role=role)
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status._primitive_to_dto(
|
||
|
|
+ self.report_processor, 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_target_role(self):
|
||
|
|
+ for role in PCMK_ROLES:
|
||
|
|
+ with self.subTest(value=role):
|
||
|
|
+ primitive_xml = etree.fromstring(
|
||
|
|
+ fixture_primitive_xml(target_role=role)
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status._primitive_to_dto(
|
||
|
|
+ self.report_processor, 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+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)
|
||
|
|
+
|
||
|
|
+ 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)
|
||
|
|
+
|
||
|
|
+ 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(
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ members=[
|
||
|
|
+ fixture_primitive_xml(resource_id="resource1"),
|
||
|
|
+ fixture_primitive_xml(resource_id="resource2"),
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status._group_to_dto(self.report_processor, group_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_group_dto(
|
||
|
|
+ members=[
|
||
|
|
+ fixture_primitive_dto(resource_id="resource1"),
|
||
|
|
+ fixture_primitive_dto(resource_id="resource2"),
|
||
|
|
+ ]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_multiple_members_different_state(self):
|
||
|
|
+ group_xml = etree.fromstring(
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ members=[
|
||
|
|
+ fixture_primitive_xml(
|
||
|
|
+ resource_id="resource1",
|
||
|
|
+ role=PCMK_STATUS_ROLE_STOPPED,
|
||
|
|
+ managed=False,
|
||
|
|
+ node_names=[],
|
||
|
|
+ ),
|
||
|
|
+ fixture_primitive_xml(resource_id="resource2"),
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status._group_to_dto(self.report_processor, group_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_group_dto(
|
||
|
|
+ members=[
|
||
|
|
+ fixture_primitive_dto(
|
||
|
|
+ resource_id="resource1",
|
||
|
|
+ role=PCMK_STATUS_ROLE_STOPPED,
|
||
|
|
+ managed=False,
|
||
|
|
+ node_names=[],
|
||
|
|
+ ),
|
||
|
|
+ fixture_primitive_dto(resource_id="resource2"),
|
||
|
|
+ ]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_invalid_member(self):
|
||
|
|
+ resources = {
|
||
|
|
+ "inner-group": '<group id="inner-group" number_resources="0" maintenance="false" managed="true" disabled="false" />',
|
||
|
|
+ "inner-clone": '<clone id="inner-clone" multi_state="false" unique="false" maintenance="false" managed="true" disabled="false" failed="false" failure_ignored="false" />',
|
||
|
|
+ "inner-bundle": '<bundle id="inner-bundle" type="podman" image="localhost/pcmktest:http" unique="false" maintenance="false" managed="true" failed="false" />',
|
||
|
|
+ }
|
||
|
|
+
|
||
|
|
+ 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"],
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_remove_clone_suffix(self):
|
||
|
|
+ group_xml = etree.fromstring(
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="resource-group:0",
|
||
|
|
+ members=[fixture_primitive_xml(resource_id="resource:0")],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status._group_to_dto(self.report_processor, 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(
|
||
|
|
+ description="Test description",
|
||
|
|
+ target_role=PCMK_STATUS_ROLE_STARTED,
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status._clone_to_dto(self.report_processor, clone_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_clone_dto(
|
||
|
|
+ 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)
|
||
|
|
+
|
||
|
|
+ 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(
|
||
|
|
+ fixture_clone_xml(
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_primitive_xml(),
|
||
|
|
+ fixture_primitive_xml(node_names=["node2"]),
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status._clone_to_dto(self.report_processor, clone_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_clone_dto(
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_primitive_dto(),
|
||
|
|
+ fixture_primitive_dto(node_names=["node2"]),
|
||
|
|
+ ]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_primitive_member_unique(self):
|
||
|
|
+ clone_xml = etree.fromstring(
|
||
|
|
+ fixture_clone_xml(
|
||
|
|
+ unique=True,
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_primitive_xml(resource_id="resource:0"),
|
||
|
|
+ fixture_primitive_xml(
|
||
|
|
+ resource_id="resource:1", node_names=["node2"]
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status._clone_to_dto(self.report_processor, clone_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_clone_dto(
|
||
|
|
+ unique=True,
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_primitive_dto(),
|
||
|
|
+ fixture_primitive_dto(node_names=["node2"]),
|
||
|
|
+ ],
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_primitive_member_promotable(self):
|
||
|
|
+ clone_xml = etree.fromstring(
|
||
|
|
+ fixture_clone_xml(
|
||
|
|
+ multi_state=True,
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_primitive_xml(role=PCMK_STATUS_ROLE_UNPROMOTED),
|
||
|
|
+ fixture_primitive_xml(
|
||
|
|
+ role=PCMK_STATUS_ROLE_UNPROMOTED, node_names=["node2"]
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ result = status._clone_to_dto(self.report_processor, clone_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_clone_dto(
|
||
|
|
+ multi_state=True,
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_primitive_dto(role=PCMK_STATUS_ROLE_UNPROMOTED),
|
||
|
|
+ fixture_primitive_dto(
|
||
|
|
+ role=PCMK_STATUS_ROLE_UNPROMOTED, node_names=["node2"]
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_primitive_member_different_ids(self):
|
||
|
|
+ clone_xml = etree.fromstring(
|
||
|
|
+ fixture_clone_xml(
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_primitive_xml(),
|
||
|
|
+ fixture_primitive_xml(
|
||
|
|
+ resource_id="not_the_same_id", node_names=["node2"]
|
||
|
|
+ ),
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_group_member(self):
|
||
|
|
+ clone_xml = etree.fromstring(
|
||
|
|
+ fixture_clone_xml(
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="resource-group:0",
|
||
|
|
+ members=[fixture_primitive_xml()],
|
||
|
|
+ ),
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="resource-group:1",
|
||
|
|
+ members=[fixture_primitive_xml(node_names=["node2"])],
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ result = status._clone_to_dto(self.report_processor, clone_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_clone_dto(
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_group_dto(members=[fixture_primitive_dto()]),
|
||
|
|
+ fixture_group_dto(
|
||
|
|
+ members=[fixture_primitive_dto(node_names=["node2"])]
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_group_member_unique(self):
|
||
|
|
+ clone_xml = etree.fromstring(
|
||
|
|
+ fixture_clone_xml(
|
||
|
|
+ unique=True,
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="resource-group:0",
|
||
|
|
+ members=[
|
||
|
|
+ fixture_primitive_xml(resource_id="resource:0")
|
||
|
|
+ ],
|
||
|
|
+ ),
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="resource-group:1",
|
||
|
|
+ members=[
|
||
|
|
+ fixture_primitive_xml(
|
||
|
|
+ resource_id="resource:1", node_names=["node2"]
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ result = status._clone_to_dto(self.report_processor, clone_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_clone_dto(
|
||
|
|
+ unique=True,
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_group_dto(members=[fixture_primitive_dto()]),
|
||
|
|
+ fixture_group_dto(
|
||
|
|
+ members=[fixture_primitive_dto(node_names=["node2"])]
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_group_member_different_group_ids(self):
|
||
|
|
+ clone_xml = etree.fromstring(
|
||
|
|
+ fixture_clone_xml(
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="resource-group:0",
|
||
|
|
+ members=[fixture_primitive_xml()],
|
||
|
|
+ ),
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="another-id-:1",
|
||
|
|
+ members=[fixture_primitive_xml(node_names=["node2"])],
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_group_member_different_primitive_ids(self):
|
||
|
|
+ clone_xml = etree.fromstring(
|
||
|
|
+ fixture_clone_xml(
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="resource-group:0",
|
||
|
|
+ members=[fixture_primitive_xml()],
|
||
|
|
+ ),
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="resource-group:1",
|
||
|
|
+ members=[
|
||
|
|
+ fixture_primitive_xml(
|
||
|
|
+ resource_id="some-other-id",
|
||
|
|
+ node_names=["node2"],
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ ),
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_primitive_member_types_mixed(self):
|
||
|
|
+ clone_xml = etree.fromstring(
|
||
|
|
+ fixture_clone_xml(
|
||
|
|
+ instances=[
|
||
|
|
+ fixture_group_xml(
|
||
|
|
+ resource_id="resource",
|
||
|
|
+ members=[
|
||
|
|
+ fixture_primitive_xml(resource_id="inner-resource")
|
||
|
|
+ ],
|
||
|
|
+ ),
|
||
|
|
+ fixture_primitive_xml(node_names=["node2"]),
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_invalid_member(self):
|
||
|
|
+ resources = {
|
||
|
|
+ "inner-clone": '<clone id="inner-clone" multi_state="false" unique="false" maintenance="false" managed="true" disabled="false" failed="false" failure_ignored="false" />',
|
||
|
|
+ "inner-bundle": '<bundle id="inner-bundle" type="podman" image="localhost/pcmktest:http" unique="false" maintenance="false" managed="true" failed="false" />',
|
||
|
|
+ }
|
||
|
|
+ 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
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ 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"],
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+class TestBundleReplicaStatusToDto(TestCase):
|
||
|
|
+ # pylint: disable=protected-access
|
||
|
|
+ def setUp(self):
|
||
|
|
+ self.report_processor = MockLibraryReportProcessor()
|
||
|
|
+
|
||
|
|
+ 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
|
||
|
|
+ )
|
||
|
|
+ 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
|
||
|
|
+ )
|
||
|
|
+ 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(
|
||
|
|
+ fixture_replica_xml(
|
||
|
|
+ ip=True,
|
||
|
|
+ member=fixture_primitive_xml(
|
||
|
|
+ node_names=["resource-bundle-0"],
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ bundle_id = "resource-bundle"
|
||
|
|
+ bundle_type = "podman"
|
||
|
|
+ result = status._replica_to_dto(
|
||
|
|
+ self.report_processor, replica_xml, bundle_id, bundle_type
|
||
|
|
+ )
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_replica_dto(
|
||
|
|
+ ip=True,
|
||
|
|
+ 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(
|
||
|
|
+ fixture_replica_xml(
|
||
|
|
+ member=fixture_primitive_xml(
|
||
|
|
+ node_names=["resource-bundle-0"],
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ bundle_id = "resource-bundle"
|
||
|
|
+ bundle_type = "podman"
|
||
|
|
+ result = status._replica_to_dto(
|
||
|
|
+ self.report_processor, replica_xml, bundle_id, bundle_type
|
||
|
|
+ )
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_replica_dto(
|
||
|
|
+ member=fixture_primitive_dto(node_names=["resource-bundle-0"])
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_no_container(self):
|
||
|
|
+ replica_xml = etree.fromstring(
|
||
|
|
+ """
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="apa" resource_agent="ocf:heartbeat:apache" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="resource-bundle-0" id="resource-bundle-0" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:pacemaker:remote" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ """
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_empty_replica(self):
|
||
|
|
+ replica_xml = etree.fromstring('<replica id="0" />')
|
||
|
|
+
|
||
|
|
+ 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
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_member_no_remote(self):
|
||
|
|
+ replica_xml = etree.fromstring(
|
||
|
|
+ """
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="apa" resource_agent="ocf:heartbeat:apache" role="Stopped" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="0"/>
|
||
|
|
+ <resource id="resource-bundle-podman-0" resource_agent="ocf:heartbeat:podman" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ """
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_member_same_id_as_container(self):
|
||
|
|
+ # xml taken from crm_mon output
|
||
|
|
+ replica_xml = etree.fromstring(
|
||
|
|
+ """
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="r92-1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-podman-0" resource_agent="ocf:heartbeat:apache" role="Stopped" active="false" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="0"/>
|
||
|
|
+ <resource id="resource-bundle-podman-0" resource_agent="ocf:heartbeat:podman" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="4">
|
||
|
|
+ <node name="r92-1" id="1" cached="true"/>
|
||
|
|
+ <node name="resource-bundle-1" id="resource-bundle-1" cached="true"/>
|
||
|
|
+ <node name="resource-bundle-0" id="resource-bundle-0" cached="true"/>
|
||
|
|
+ <node name="resource-bundle-2" id="resource-bundle-2" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:pacemaker:remote" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="r92-1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ """
|
||
|
|
+ )
|
||
|
|
+ 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"],
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_member_same_id_as_remote(self):
|
||
|
|
+ # xml taken from crm_mon output
|
||
|
|
+ replica_xml = etree.fromstring(
|
||
|
|
+ """
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:heartbeat:apache" role="Stopped" active="false" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="0"/>
|
||
|
|
+ <resource id="resource-bundle-podman-0" resource_agent="ocf:heartbeat:podman" role="Stopping" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="true" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:pacemaker:remote" role="Started" active="false" orphaned="false" blocked="false" maintenance="false" managed="true" failed="true" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="resource-bundle-0" id="resource-bundle-0" cached="false"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ """
|
||
|
|
+ )
|
||
|
|
+ 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"],
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_member_same_id_as_ip(self):
|
||
|
|
+ # xml taken from crm_mon output
|
||
|
|
+ replica_xml = etree.fromstring(
|
||
|
|
+ """
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="4">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ <node name="resource-bundle-0" id="resource-bundle-0" cached="true"/>
|
||
|
|
+ <node name="resource-bundle-1" id="resource-bundle-1" cached="true"/>
|
||
|
|
+ <node name="resource-bundle-2" id="resource-bundle-2" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:apache" role="Stopped" active="false" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="0"/>
|
||
|
|
+ <resource id="resource-bundle-podman-0" resource_agent="ocf:heartbeat:podman" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:pacemaker:remote" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ """
|
||
|
|
+ )
|
||
|
|
+ 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"],
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_too_many_members(self):
|
||
|
|
+ replica_xml = etree.fromstring(
|
||
|
|
+ """
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="r92-1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="apa1" resource_agent="ocf:heartbeat:apache" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="resource-bundle-0" id="resource-bundle-0" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-podman-0" resource_agent="ocf:heartbeat:podman" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="r92-1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:pacemaker:remote" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="r92-1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="apa2" resource_agent="ocf:heartbeat:apache" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="resource-bundle-0" id="resource-bundle-0" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ """
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+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)
|
||
|
|
+ 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(
|
||
|
|
+ fixture_bundle_xml(
|
||
|
|
+ replicas=[
|
||
|
|
+ fixture_replica_xml(
|
||
|
|
+ ip=True,
|
||
|
|
+ member=fixture_primitive_xml(
|
||
|
|
+ node_names=["resource-bundle-0"]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ result = status._bundle_to_dto(self.report_processor, bundle_xml, False)
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_bundle_dto(
|
||
|
|
+ replicas=[
|
||
|
|
+ fixture_replica_dto(
|
||
|
|
+ ip=True,
|
||
|
|
+ member=fixture_primitive_dto(
|
||
|
|
+ node_names=["resource-bundle-0"]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ ]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_multiple_replicas(self):
|
||
|
|
+ bundle_xml = etree.fromstring(
|
||
|
|
+ fixture_bundle_xml(
|
||
|
|
+ replicas=[
|
||
|
|
+ fixture_replica_xml(
|
||
|
|
+ ip=True,
|
||
|
|
+ member=fixture_primitive_xml(
|
||
|
|
+ node_names=["resource-bundle-0"]
|
||
|
|
+ ),
|
||
|
|
+ ),
|
||
|
|
+ fixture_replica_xml(
|
||
|
|
+ ip=True,
|
||
|
|
+ replica_id="1",
|
||
|
|
+ node_name="node2",
|
||
|
|
+ member=fixture_primitive_xml(
|
||
|
|
+ node_names=["resource-bundle-1"]
|
||
|
|
+ ),
|
||
|
|
+ ),
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ result = status._bundle_to_dto(self.report_processor, bundle_xml, False)
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ fixture_bundle_dto(
|
||
|
|
+ replicas=[
|
||
|
|
+ fixture_replica_dto(
|
||
|
|
+ ip=True,
|
||
|
|
+ member=fixture_primitive_dto(
|
||
|
|
+ node_names=["resource-bundle-0"]
|
||
|
|
+ ),
|
||
|
|
+ ),
|
||
|
|
+ fixture_replica_dto(
|
||
|
|
+ replica_id="1",
|
||
|
|
+ ip=True,
|
||
|
|
+ node_name="node2",
|
||
|
|
+ member=fixture_primitive_dto(
|
||
|
|
+ node_names=["resource-bundle-1"]
|
||
|
|
+ ),
|
||
|
|
+ ),
|
||
|
|
+ ]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_same_id_as_implicit(self):
|
||
|
|
+ bundle_xml = etree.fromstring(
|
||
|
|
+ """
|
||
|
|
+ <bundle id="resource-bundle" type="podman" image="localhost/pcmktest:http" unique="false" maintenance="false" managed="true" failed="false">
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:heartbeat:apache" role="Stopped" active="false" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="0"/>
|
||
|
|
+ <resource id="resource-bundle-podman-0" resource_agent="ocf:heartbeat:podman" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:pacemaker:remote" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="2">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ <node name="resource-bundle-0" id="resource-bundle-0" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ </bundle>
|
||
|
|
+ """
|
||
|
|
+ )
|
||
|
|
+ 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"],
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_same_id_as_implicit_multiple_replicas(self):
|
||
|
|
+ bundle_xml = etree.fromstring(
|
||
|
|
+ """
|
||
|
|
+ <bundle id="resource-bundle" type="podman" image="localhost/pcmktest:http" unique="false" maintenance="false" managed="true" failed="false">
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-1" resource_agent="ocf:heartbeat:apache" role="Stopped" active="false" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="0"/>
|
||
|
|
+ <resource id="resource-bundle-podman-0" resource_agent="ocf:heartbeat:podman" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:pacemaker:remote" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ <replica id="1">
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.251" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node2" id="2" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-1" resource_agent="ocf:heartbeat:apache" role="Stopped" active="false" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="0"/>
|
||
|
|
+ <resource id="resource-bundle-podman-1" resource_agent="ocf:heartbeat:podman" role="Stopping" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="true" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node2" id="2" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-1" resource_agent="ocf:pacemaker:remote" role="Started" active="false" orphaned="false" blocked="false" maintenance="false" managed="true" failed="true" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="resource-bundle-1" id="resource-bundle-1" cached="false"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ </bundle>
|
||
|
|
+ """
|
||
|
|
+ )
|
||
|
|
+ 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"],
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_replicas_different(self):
|
||
|
|
+ replicas = {
|
||
|
|
+ "no-ip": fixture_replica_xml(
|
||
|
|
+ ip=False, member=fixture_primitive_xml()
|
||
|
|
+ ),
|
||
|
|
+ "different-member-id": fixture_replica_xml(
|
||
|
|
+ ip=True, member=fixture_primitive_xml(resource_id="another-id")
|
||
|
|
+ ),
|
||
|
|
+ "no-member": fixture_replica_xml(ip=True, member=None),
|
||
|
|
+ "different-member-agent": fixture_replica_xml(
|
||
|
|
+ ip=True,
|
||
|
|
+ member=fixture_primitive_xml(
|
||
|
|
+ resource_agent="ocf:heartbeat:apache"
|
||
|
|
+ ),
|
||
|
|
+ ),
|
||
|
|
+ }
|
||
|
|
+ for name, element in replicas.items():
|
||
|
|
+ with self.subTest(value=name):
|
||
|
|
+ self.setUp()
|
||
|
|
+
|
||
|
|
+ bundle_xml = etree.fromstring(
|
||
|
|
+ fixture_bundle_xml(
|
||
|
|
+ replicas=[
|
||
|
|
+ element,
|
||
|
|
+ fixture_replica_xml(
|
||
|
|
+ ip=True,
|
||
|
|
+ replica_id="1",
|
||
|
|
+ member=fixture_primitive_xml(),
|
||
|
|
+ ),
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ # 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",
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+
|
||
|
|
+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)
|
||
|
|
+ self.assertEqual(result, ResourcesStatusDto([]))
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ 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)
|
||
|
|
+ self.assertEqual(result, ResourcesStatusDto([fixture_primitive_dto()]))
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_single_group(self):
|
||
|
|
+ status_xml = etree.fromstring(
|
||
|
|
+ fixture_crm_mon_xml(
|
||
|
|
+ [fixture_group_xml(members=[fixture_primitive_xml()])]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status.status_xml_to_dto(self.report_processor, status_xml)
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ ResourcesStatusDto(
|
||
|
|
+ [fixture_group_dto(members=[fixture_primitive_dto()])]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_single_clone(self):
|
||
|
|
+ status_xml = etree.fromstring(
|
||
|
|
+ fixture_crm_mon_xml(
|
||
|
|
+ [fixture_clone_xml(instances=[fixture_primitive_xml()])]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status.status_xml_to_dto(self.report_processor, status_xml)
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ ResourcesStatusDto(
|
||
|
|
+ [fixture_clone_dto(instances=[fixture_primitive_dto()])]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_single_bundle(self):
|
||
|
|
+ status_xml = etree.fromstring(
|
||
|
|
+ fixture_crm_mon_xml(
|
||
|
|
+ [
|
||
|
|
+ fixture_bundle_xml(
|
||
|
|
+ replicas=[
|
||
|
|
+ fixture_replica_xml(
|
||
|
|
+ ip=True,
|
||
|
|
+ member=fixture_primitive_xml(
|
||
|
|
+ node_names=["resource-bundle-0"]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status.status_xml_to_dto(self.report_processor, status_xml)
|
||
|
|
+ self.assertEqual(
|
||
|
|
+ result,
|
||
|
|
+ ResourcesStatusDto(
|
||
|
|
+ [
|
||
|
|
+ fixture_bundle_dto(
|
||
|
|
+ replicas=[
|
||
|
|
+ fixture_replica_dto(
|
||
|
|
+ ip=True,
|
||
|
|
+ member=fixture_primitive_dto(
|
||
|
|
+ node_names=["resource-bundle-0"]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ ]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ assert_report_item_list_equal(
|
||
|
|
+ self.report_processor.report_item_list, []
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_all_resource_types(self):
|
||
|
|
+ status_xml = etree.fromstring(
|
||
|
|
+ fixture_crm_mon_xml(
|
||
|
|
+ [
|
||
|
|
+ fixture_primitive_xml(),
|
||
|
|
+ fixture_group_xml(members=[fixture_primitive_xml()]),
|
||
|
|
+ fixture_clone_xml(instances=[fixture_primitive_xml()]),
|
||
|
|
+ fixture_bundle_xml(
|
||
|
|
+ replicas=[
|
||
|
|
+ fixture_replica_xml(
|
||
|
|
+ ip=True,
|
||
|
|
+ member=fixture_primitive_xml(
|
||
|
|
+ node_names=["resource-bundle-0"]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ ]
|
||
|
|
+ ),
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+ result = status.status_xml_to_dto(self.report_processor, status_xml)
|
||
|
|
+
|
||
|
|
+ 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"]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+ ]
|
||
|
|
+ ),
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ def test_skip_bundle(self):
|
||
|
|
+ status_xml = etree.fromstring(
|
||
|
|
+ fixture_crm_mon_xml(
|
||
|
|
+ [
|
||
|
|
+ fixture_primitive_xml(),
|
||
|
|
+ """
|
||
|
|
+ <bundle id="resource-bundle" type="podman" image="localhost/pcmktest:http" unique="false" maintenance="false" managed="true" failed="false">
|
||
|
|
+ <replica id="0">
|
||
|
|
+ <resource id="resource-bundle-ip-192.168.122.250" resource_agent="ocf:heartbeat:IPaddr2" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:heartbeat:apache" role="Stopped" active="false" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="0"/>
|
||
|
|
+ <resource id="resource-bundle-podman-0" resource_agent="ocf:heartbeat:podman" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="1">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ <resource id="resource-bundle-0" resource_agent="ocf:pacemaker:remote" role="Started" active="true" orphaned="false" blocked="false" maintenance="false" managed="true" failed="false" failure_ignored="false" nodes_running_on="2">
|
||
|
|
+ <node name="node1" id="1" cached="true"/>
|
||
|
|
+ <node name="resource-bundle-0" id="resource-bundle-0" cached="true"/>
|
||
|
|
+ </resource>
|
||
|
|
+ </replica>
|
||
|
|
+ </bundle>
|
||
|
|
+ """,
|
||
|
|
+ ]
|
||
|
|
+ )
|
||
|
|
+ )
|
||
|
|
+
|
||
|
|
+ result = status.status_xml_to_dto(self.report_processor, status_xml)
|
||
|
|
+
|
||
|
|
+ self.assertEqual(result, ResourcesStatusDto([fixture_primitive_dto()]))
|
||
|
|
+ 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"],
|
||
|
|
+ )
|
||
|
|
+ ],
|
||
|
|
+ )
|
||
|
|
--
|
||
|
|
2.25.1
|
||
|
|
|