Skip to content

Commit 18bbb15

Browse files
committed
Add unit tests for get_cluster_status_scs module and remove legacy tests
1 parent ee9113f commit 18bbb15

File tree

8 files changed

+919
-493
lines changed

8 files changed

+919
-493
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""
5+
Base class for cluster status checking implementations.
6+
"""
7+
8+
import logging
9+
import xml.etree.ElementTree as ET
10+
from datetime import datetime
11+
from typing import Dict, Any
12+
13+
try:
14+
from ansible.module_utils.sap_automation_qa import SapAutomationQA, TestStatus
15+
from ansible.module_utils.commands import (
16+
STONITH_ACTION,
17+
PACEMAKER_STATUS,
18+
CLUSTER_STATUS,
19+
)
20+
except ImportError:
21+
from src.module_utils.sap_automation_qa import SapAutomationQA, TestStatus
22+
from src.module_utils.commands import (
23+
STONITH_ACTION,
24+
PACEMAKER_STATUS,
25+
CLUSTER_STATUS,
26+
)
27+
28+
29+
class BaseClusterStatusChecker(SapAutomationQA):
30+
"""
31+
Base class to check the status of a pacemaker cluster.
32+
"""
33+
34+
def __init__(self, ansible_os_family: str = ""):
35+
super().__init__()
36+
self.ansible_os_family = ansible_os_family
37+
self.result.update(
38+
{
39+
"cluster_status": "",
40+
"start": datetime.now(),
41+
"end": None,
42+
"pacemaker_status": "",
43+
"stonith_action": "",
44+
}
45+
)
46+
47+
def _get_stonith_action(self) -> None:
48+
"""
49+
Retrieves the stonith action from the system.
50+
"""
51+
try:
52+
stonith_action = self.execute_command_subprocess(STONITH_ACTION[self.ansible_os_family])
53+
stonith_action = (
54+
stonith_action.split("stonith-action:")[-1]
55+
if self.ansible_os_family == "REDHAT"
56+
else stonith_action
57+
)
58+
self.result["stonith_action"] = stonith_action.strip()
59+
except Exception:
60+
self.result["stonith_action"] = "reboot"
61+
62+
def _validate_cluster_basic_status(self, cluster_status_xml: ET.Element) -> bool:
63+
"""
64+
Validate the basic status of the cluster.
65+
66+
:param cluster_status_xml: XML element containing cluster status.
67+
:type cluster_status_xml: ET.Element
68+
:return: True if the cluster is stable, False otherwise.
69+
:rtype: bool
70+
"""
71+
# Check pacemaker status
72+
if self.execute_command_subprocess(PACEMAKER_STATUS).strip() == "active":
73+
self.result["pacemaker_status"] = "running"
74+
else:
75+
self.result["pacemaker_status"] = "stopped"
76+
self.log(logging.INFO, f"Pacemaker status: {self.result['pacemaker_status']}")
77+
78+
# Check node count
79+
if int(cluster_status_xml.find("summary").find("nodes_configured").attrib["number"]) < 2:
80+
self.result["message"] = "Pacemaker cluster isn't stable (insufficient nodes)"
81+
self.log(logging.WARNING, self.result["message"])
82+
return False
83+
84+
# Check if all nodes are online
85+
nodes = cluster_status_xml.find("nodes")
86+
for node in nodes:
87+
if node.attrib["online"] != "true":
88+
self.result["message"] = f"Node {node.attrib['name']} is not online"
89+
self.log(logging.WARNING, self.result["message"])
90+
return False
91+
92+
return True
93+
94+
def _process_node_attributes(self, node_attributes: ET.Element) -> Dict[str, Any]:
95+
"""
96+
Abstract method to process node attributes.
97+
98+
:param node_attributes: XML element containing node attributes.
99+
:type node_attributes: ET.Element
100+
:raises NotImplementedError: If the method is not implemented in a child class.
101+
:return: Dictionary with node attributes.
102+
:rtype: Dict[str, Any]
103+
"""
104+
raise NotImplementedError("Child classes must implement this method")
105+
106+
def run(self) -> Dict[str, str]:
107+
"""
108+
Run the cluster status check.
109+
110+
:return: Result of the cluster status check.
111+
:rtype: Dict[str, str]
112+
"""
113+
self.log(logging.INFO, "Starting cluster status check")
114+
self._get_stonith_action()
115+
116+
try:
117+
# Implementing a common loop structure
118+
while not self._is_cluster_ready():
119+
self.result["cluster_status"] = self.execute_command_subprocess(CLUSTER_STATUS)
120+
cluster_status_xml = ET.fromstring(self.result["cluster_status"])
121+
self.log(logging.INFO, "Cluster status retrieved")
122+
123+
if self._validate_cluster_basic_status(cluster_status_xml):
124+
self._process_node_attributes(cluster_status_xml.find("node_attributes"))
125+
126+
if not self._is_cluster_stable():
127+
self.result["message"] = "Pacemaker cluster isn't stable"
128+
self.log(logging.WARNING, self.result["message"])
129+
130+
except Exception as e:
131+
self.handle_error(e)
132+
133+
self.result["end"] = datetime.now()
134+
self.result["status"] = TestStatus.SUCCESS.value
135+
self.log(logging.INFO, "Cluster status check completed")
136+
return self.result
137+
138+
def _is_cluster_ready(self) -> bool:
139+
"""
140+
Abstract method to check if the cluster is ready.
141+
To be implemented by child classes.
142+
143+
:raises NotImplementedError: If the method is not implemented in a child class.
144+
:return: True if the cluster is ready, False otherwise.
145+
:rtype: bool
146+
"""
147+
raise NotImplementedError("Child classes must implement this method")
148+
149+
def _is_cluster_stable(self) -> bool:
150+
"""
151+
Abstract method to check if the cluster is in a stable state.
152+
To be implemented by child classes.
153+
154+
:raises NotImplementedError: If the method is not implemented in a child class.
155+
:return: True if the cluster is ready, False otherwise.
156+
:rtype: bool
157+
"""
158+
raise NotImplementedError("Child classes must implement this method")

0 commit comments

Comments
 (0)