Skip to content

Commit b9b61ad

Browse files
Resource validation for azure events (#130)
1 parent 2f657b7 commit b9b61ad

File tree

8 files changed

+312
-13
lines changed

8 files changed

+312
-13
lines changed

src/module_utils/commands.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,21 @@
6666
}
6767

6868
CIB_ADMIN = lambda scope: ["cibadmin", "--query", "--scope", scope]
69+
70+
RECOMMENDATION_MESSAGES = {
71+
"priority-fencing-delay": (
72+
"The 'priority-fencing-delay' setting is not configured. "
73+
"In a two-node cluster, configure priority-fencing-delay to enhance the "
74+
"highest-priority node's survival odds during a fence race condition. "
75+
"For more details on the setup, check official cluster pacemaker configuration "
76+
"documentation in learn.microsoft.com"
77+
),
78+
"azureevents": (
79+
"The Azure scheduled events resource is not configured. "
80+
"It is advised to setup this agent in your cluster to monitor the Instance Metadata "
81+
"Service (IMDS) for platform maintenance events, allowing it to proactively drain "
82+
"resources or initiate a clean failover before Azure maintenance impacts the node. "
83+
"For more details on the setup, check official cluster pacemaker configuration "
84+
"documentation in learn.microsoft.com"
85+
),
86+
}

src/module_utils/get_pcmk_properties.py

Lines changed: 126 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@
1111
BaseHAClusterValidator: Base validator class for cluster configurations.
1212
"""
1313

14+
import logging
1415
from abc import ABC
1516

1617
try:
1718
from ansible.module_utils.sap_automation_qa import SapAutomationQA
1819
from ansible.module_utils.enums import OperatingSystemFamily, Parameters, TestStatus
19-
from ansible.module_utils.commands import CIB_ADMIN
20+
from ansible.module_utils.commands import CIB_ADMIN, RECOMMENDATION_MESSAGES
2021
except ImportError:
2122
from src.module_utils.sap_automation_qa import SapAutomationQA
2223
from src.module_utils.enums import OperatingSystemFamily, Parameters, TestStatus
23-
from src.module_utils.commands import CIB_ADMIN
24+
from src.module_utils.commands import CIB_ADMIN, RECOMMENDATION_MESSAGES
2425

2526

2627
class BaseHAClusterValidator(SapAutomationQA, ABC):
@@ -79,6 +80,7 @@ def __init__(
7980
self.fencing_mechanism = fencing_mechanism
8081
self.constants = constants
8182
self.cib_output = cib_output
83+
self.missing_required_items = []
8284

8385
def _get_expected_value(self, category, name):
8486
"""
@@ -190,6 +192,15 @@ def _create_parameter(
190192

191193
status = self._determine_parameter_status(value, expected_config)
192194

195+
if status == TestStatus.WARNING.value and not value:
196+
self._handle_missing_required_parameter(
197+
expected_config=expected_config,
198+
name=name,
199+
category=category,
200+
subcategory=subcategory,
201+
op_name=op_name,
202+
)
203+
193204
display_expected_value = None
194205
if expected_config is None:
195206
display_expected_value = ""
@@ -289,6 +300,47 @@ def _determine_parameter_status(self, value, expected_config):
289300
else TestStatus.ERROR.value
290301
)
291302

303+
def _handle_missing_required_parameter(
304+
self, expected_config, name, category, subcategory=None, op_name=None
305+
):
306+
"""
307+
Handle warnings for missing required parameters.
308+
Logs warning message and updates result when a required parameter has no value.
309+
310+
:param expected_config: The expected configuration (tuple or dict)
311+
:type expected_config: tuple or dict
312+
:param name: The parameter name
313+
:type name: str
314+
:param category: The parameter category
315+
:type category: str
316+
:param subcategory: The parameter subcategory, defaults to None
317+
:type subcategory: str, optional
318+
:param op_name: The operation name (if applicable), defaults to None
319+
:type op_name: str, optional
320+
"""
321+
is_required = False
322+
if isinstance(expected_config, tuple) and len(expected_config) == 2:
323+
is_required = expected_config[1]
324+
elif isinstance(expected_config, dict):
325+
is_required = expected_config.get("required", False)
326+
327+
if is_required:
328+
param_display_name = f"{op_name}_{name}" if op_name else name
329+
category_display = f"{category}_{subcategory}" if subcategory else category
330+
self.missing_required_items.append(
331+
{
332+
"type": "parameter",
333+
"name": name,
334+
"display_name": param_display_name,
335+
"category": category_display,
336+
}
337+
)
338+
warning_msg = (
339+
f"Required parameter '{param_display_name}' in category '{category_display}' "
340+
+ "has no value configured."
341+
)
342+
self.log(logging.WARNING, warning_msg)
343+
292344
def _parse_nvpair_elements(self, elements, category, subcategory=None, op_name=None):
293345
"""
294346
Parse nvpair elements and create parameter dictionaries.
@@ -511,6 +563,8 @@ def validate_from_constants(self):
511563
overall_status = TestStatus.ERROR.value
512564
elif warning_parameters:
513565
overall_status = TestStatus.WARNING.value
566+
elif self.result.get("status") == TestStatus.WARNING.value:
567+
overall_status = TestStatus.WARNING.value
514568
else:
515569
overall_status = TestStatus.SUCCESS.value
516570

@@ -520,7 +574,10 @@ def validate_from_constants(self):
520574
"status": overall_status,
521575
}
522576
)
523-
self.result["message"] += "HA Parameter Validation completed successfully. "
577+
self.result["message"] += "HA parameter validation completed successfully. "
578+
recommendation_message = self._generate_recommendation_message()
579+
if recommendation_message:
580+
self.result["message"] += recommendation_message
524581

525582
def _validate_basic_constants(self, category):
526583
"""
@@ -618,6 +675,72 @@ def _validate_resource_constants(self):
618675
"""
619676
return []
620677

678+
def _check_required_resources(self):
679+
"""
680+
Check if required resources are present in the cluster.
681+
Adds warnings to result message for missing required resources.
682+
"""
683+
if "RESOURCE_DEFAULTS" not in self.constants:
684+
return
685+
686+
try:
687+
if self.cib_output:
688+
resource_scope = self._get_scope_from_cib("resources")
689+
else:
690+
resource_scope = self.parse_xml_output(
691+
self.execute_command_subprocess(CIB_ADMIN(scope="resources"))
692+
)
693+
if resource_scope is None:
694+
return
695+
696+
for resource_type, resource_config in (
697+
self.constants["RESOURCE_DEFAULTS"].get(self.os_type, {}).items()
698+
):
699+
if not isinstance(resource_config, dict):
700+
continue
701+
if resource_config.get("required", False):
702+
if resource_type in self.RESOURCE_CATEGORIES:
703+
xpath = self.RESOURCE_CATEGORIES[resource_type]
704+
elements = resource_scope.findall(xpath)
705+
if not elements:
706+
self.missing_required_items.append(
707+
{"type": "resource", "name": resource_type, "xpath": xpath}
708+
)
709+
self.result["status"] = TestStatus.WARNING.value
710+
except Exception as ex:
711+
self.result["message"] += f"Error checking required resources: {str(ex)} "
712+
713+
def _generate_recommendation_message(self):
714+
"""
715+
Generate recommendation message based on missing required items.
716+
Uses centralized RECOMMENDATION_MESSAGES dictionary for consistent messaging.
717+
718+
:return: Formatted recommendation message
719+
:rtype: str
720+
"""
721+
recommendations = []
722+
723+
for item in self.missing_required_items:
724+
if item["name"] in RECOMMENDATION_MESSAGES:
725+
recommendations.append(RECOMMENDATION_MESSAGES[item["name"]])
726+
else:
727+
if item["type"] == "parameter":
728+
recommendations.append(
729+
f"The '{item['display_name']}' parameter in category "
730+
f"'{item['category']}' is not configured."
731+
)
732+
elif item["type"] == "resource":
733+
recommendations.append(
734+
f"The required resource '{item['name']}' is not found in cluster config."
735+
)
736+
737+
if recommendations:
738+
recommendation_header = "\n\nRecommendation for warnings:\n"
739+
recommendation_body = "\n".join(recommendations)
740+
return recommendation_header + recommendation_body
741+
742+
return ""
743+
621744
def _validate_constraint_constants(self):
622745
"""
623746
Validate constraint constants with offline validation support.

src/modules/get_azure_lb.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,17 +247,17 @@ def get_load_balancers_details(self) -> None:
247247
parameters = []
248248

249249
def check_parameters(entity, parameters_dict, entity_type):
250-
for key, expected_value in parameters_dict.items():
250+
for key, value_object in parameters_dict.items():
251251
parameters.append(
252252
Parameters(
253253
category=entity_type,
254254
id=entity["name"],
255255
name=key,
256256
value=str(entity[key]),
257-
expected_value=str(expected_value),
257+
expected_value=str(value_object.get("value", "")),
258258
status=(
259259
TestStatus.SUCCESS.value
260-
if entity[key] == expected_value
260+
if entity[key] == value_object.get("value", "")
261261
else TestStatus.ERROR.value
262262
),
263263
).to_dict()

src/modules/get_pcmk_properties_db.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ class HAClusterValidator(BaseHAClusterValidator):
171171
"azurelb": ".//primitive[@type='azure-lb']",
172172
"angi_filesystem": ".//primitive[@type='SAPHanaFilesystem']",
173173
"angi_hana": ".//primitive[@type='SAPHanaController']",
174+
"azureevents": ".//primitive[@type='azure-events-az']",
174175
}
175176

176177
def __init__(
@@ -227,6 +228,7 @@ def _validate_resource_constants(self):
227228
"""
228229
Resource validation with HANA-specific logic and offline validation support.
229230
Validates resource constants by iterating through expected parameters.
231+
Also checks for required resources.
230232
231233
:return: A list of parameter dictionaries
232234
:rtype: list
@@ -243,6 +245,7 @@ def _validate_resource_constants(self):
243245
if resource_scope is not None:
244246
parameters.extend(self._parse_resources_section(resource_scope))
245247

248+
self._check_required_resources()
246249
except Exception as ex:
247250
self.result["message"] += f"Error validating resource constants: {str(ex)} "
248251

src/modules/get_pcmk_properties_scs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ def _validate_resource_constants(self):
224224
"""
225225
Resource validation with SCS-specific logic and offline validation support.
226226
Validates resource constants by iterating through expected parameters.
227+
Also checks for required resources.
227228
228229
:return: A list of parameter dictionaries
229230
:rtype: list
@@ -240,6 +241,7 @@ def _validate_resource_constants(self):
240241

241242
if resource_scope is not None:
242243
parameters.extend(self._parse_resources_section(resource_scope))
244+
self._check_required_resources()
243245

244246
except Exception as ex:
245247
self.result["message"] += f"Error validating resource constants: {str(ex)} "

src/roles/ha_db_hana/tasks/files/constants.yaml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ CRM_CONFIG_DEFAULTS:
1919
stonith-enabled:
2020
value: "true"
2121
required: false
22+
stonith-timeout:
23+
value: ["210", "210s"]
24+
required: false
2225
concurrent-fencing:
2326
value: "true"
2427
required: false
@@ -451,6 +454,39 @@ RESOURCE_DEFAULTS:
451454
timeout:
452455
value: ["20", "20s"]
453456
required: false
457+
458+
azureevents:
459+
required: true
460+
meta_attributes:
461+
allow-unhealthy-nodes:
462+
value: "true"
463+
required: false
464+
failure-timeout:
465+
value: "120s"
466+
required: false
467+
operations:
468+
monitor:
469+
interval:
470+
value: ["10", "10s"]
471+
required: false
472+
timeout:
473+
value: ["240", "240s"]
474+
required: false
475+
start:
476+
interval:
477+
value: ["0", "0s"]
478+
required: false
479+
timeout:
480+
value: ["10", "10s"]
481+
required: false
482+
stop:
483+
interval:
484+
value: ["0", "0s"]
485+
required: false
486+
timeout:
487+
value: ["10", "10s"]
488+
required: false
489+
454490
REDHAT:
455491
fence_agent:
456492
required: false
@@ -708,6 +744,38 @@ RESOURCE_DEFAULTS:
708744
value: ["20", "20s"]
709745
required: false
710746

747+
azureevents:
748+
required: true
749+
meta_attributes:
750+
allow-unhealthy-nodes:
751+
value: "true"
752+
required: false
753+
failure-timeout:
754+
value: "120s"
755+
required: false
756+
operations:
757+
monitor:
758+
interval:
759+
value: ["10", "10s"]
760+
required: false
761+
timeout:
762+
value: ["240", "240s"]
763+
required: false
764+
start:
765+
interval:
766+
value: ["0", "0s"]
767+
required: false
768+
timeout:
769+
value: ["10", "10s"]
770+
required: false
771+
stop:
772+
interval:
773+
value: ["0", "0s"]
774+
required: false
775+
timeout:
776+
value: ["10", "10s"]
777+
required: false
778+
711779
# === OS Parameters ===
712780
# Run command as root. Format of command is: "parent_key child_key"
713781
# Example: sysctl net.ipv4.tcp_timestamps

0 commit comments

Comments
 (0)