Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/module_utils/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,21 @@
}

CIB_ADMIN = lambda scope: ["cibadmin", "--query", "--scope", scope]

RECOMMENDATION_MESSAGES = {
"priority-fencing-delay": (
"The 'priority-fencing-delay' setting is not configured. "
"In a two-node cluster, configure priority-fencing-delay to enhance the "
"highest-priority node's survival odds during a fence race condition. "
"For more details on the setup, check official cluster pacemaker configuration "
"documentation in learn.microsoft.com"
),
"azureevents": (
"The Azure scheduled events resource is not configured. "
"It is advised to setup this agent in your cluster to monitor the Instance Metadata "
"Service (IMDS) for platform maintenance events, allowing it to proactively drain "
"resources or initiate a clean failover before Azure maintenance impacts the node. "
"For more details on the setup, check official cluster pacemaker configuration "
"documentation in learn.microsoft.com"
),
}
129 changes: 126 additions & 3 deletions src/module_utils/get_pcmk_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@
BaseHAClusterValidator: Base validator class for cluster configurations.
"""

import logging
from abc import ABC

try:
from ansible.module_utils.sap_automation_qa import SapAutomationQA
from ansible.module_utils.enums import OperatingSystemFamily, Parameters, TestStatus
from ansible.module_utils.commands import CIB_ADMIN
from ansible.module_utils.commands import CIB_ADMIN, RECOMMENDATION_MESSAGES
except ImportError:
from src.module_utils.sap_automation_qa import SapAutomationQA
from src.module_utils.enums import OperatingSystemFamily, Parameters, TestStatus
from src.module_utils.commands import CIB_ADMIN
from src.module_utils.commands import CIB_ADMIN, RECOMMENDATION_MESSAGES


class BaseHAClusterValidator(SapAutomationQA, ABC):
Expand Down Expand Up @@ -79,6 +80,7 @@ def __init__(
self.fencing_mechanism = fencing_mechanism
self.constants = constants
self.cib_output = cib_output
self.missing_required_items = []

def _get_expected_value(self, category, name):
"""
Expand Down Expand Up @@ -190,6 +192,15 @@ def _create_parameter(

status = self._determine_parameter_status(value, expected_config)

if status == TestStatus.WARNING.value and not value:
self._handle_missing_required_parameter(
expected_config=expected_config,
name=name,
category=category,
subcategory=subcategory,
op_name=op_name,
)

display_expected_value = None
if expected_config is None:
display_expected_value = ""
Expand Down Expand Up @@ -289,6 +300,47 @@ def _determine_parameter_status(self, value, expected_config):
else TestStatus.ERROR.value
)

def _handle_missing_required_parameter(
self, expected_config, name, category, subcategory=None, op_name=None
):
"""
Handle warnings for missing required parameters.
Logs warning message and updates result when a required parameter has no value.

:param expected_config: The expected configuration (tuple or dict)
:type expected_config: tuple or dict
:param name: The parameter name
:type name: str
:param category: The parameter category
:type category: str
:param subcategory: The parameter subcategory, defaults to None
:type subcategory: str, optional
:param op_name: The operation name (if applicable), defaults to None
:type op_name: str, optional
"""
is_required = False
if isinstance(expected_config, tuple) and len(expected_config) == 2:
is_required = expected_config[1]
elif isinstance(expected_config, dict):
is_required = expected_config.get("required", False)

if is_required:
param_display_name = f"{op_name}_{name}" if op_name else name
category_display = f"{category}_{subcategory}" if subcategory else category
self.missing_required_items.append(
{
"type": "parameter",
"name": name,
"display_name": param_display_name,
"category": category_display,
}
)
warning_msg = (
f"Required parameter '{param_display_name}' in category '{category_display}' "
+ "has no value configured."
)
self.log(logging.WARNING, warning_msg)

def _parse_nvpair_elements(self, elements, category, subcategory=None, op_name=None):
"""
Parse nvpair elements and create parameter dictionaries.
Expand Down Expand Up @@ -511,6 +563,8 @@ def validate_from_constants(self):
overall_status = TestStatus.ERROR.value
elif warning_parameters:
overall_status = TestStatus.WARNING.value
elif self.result.get("status") == TestStatus.WARNING.value:
overall_status = TestStatus.WARNING.value
else:
overall_status = TestStatus.SUCCESS.value

Expand All @@ -520,7 +574,10 @@ def validate_from_constants(self):
"status": overall_status,
}
)
self.result["message"] += "HA Parameter Validation completed successfully. "
self.result["message"] += "HA parameter validation completed successfully. "
recommendation_message = self._generate_recommendation_message()
if recommendation_message:
self.result["message"] += recommendation_message

def _validate_basic_constants(self, category):
"""
Expand Down Expand Up @@ -618,6 +675,72 @@ def _validate_resource_constants(self):
"""
return []

def _check_required_resources(self):
"""
Check if required resources are present in the cluster.
Adds warnings to result message for missing required resources.
"""
if "RESOURCE_DEFAULTS" not in self.constants:
return

try:
if self.cib_output:
resource_scope = self._get_scope_from_cib("resources")
else:
resource_scope = self.parse_xml_output(
self.execute_command_subprocess(CIB_ADMIN(scope="resources"))
)
if resource_scope is None:
return

for resource_type, resource_config in (
self.constants["RESOURCE_DEFAULTS"].get(self.os_type, {}).items()
):
if not isinstance(resource_config, dict):
continue
if resource_config.get("required", False):
if resource_type in self.RESOURCE_CATEGORIES:
xpath = self.RESOURCE_CATEGORIES[resource_type]
elements = resource_scope.findall(xpath)
if not elements:
self.missing_required_items.append(
{"type": "resource", "name": resource_type, "xpath": xpath}
)
self.result["status"] = TestStatus.WARNING.value
except Exception as ex:
self.result["message"] += f"Error checking required resources: {str(ex)} "

def _generate_recommendation_message(self):
"""
Generate recommendation message based on missing required items.
Uses centralized RECOMMENDATION_MESSAGES dictionary for consistent messaging.

:return: Formatted recommendation message
:rtype: str
"""
recommendations = []

for item in self.missing_required_items:
if item["name"] in RECOMMENDATION_MESSAGES:
recommendations.append(RECOMMENDATION_MESSAGES[item["name"]])
else:
if item["type"] == "parameter":
recommendations.append(
f"The '{item['display_name']}' parameter in category "
f"'{item['category']}' is not configured."
)
elif item["type"] == "resource":
recommendations.append(
f"The required resource '{item['name']}' is not found in cluster config."
)

if recommendations:
recommendation_header = "\n\nRecommendation for warnings:\n"
recommendation_body = "\n".join(recommendations)
return recommendation_header + recommendation_body

return ""

def _validate_constraint_constants(self):
"""
Validate constraint constants with offline validation support.
Expand Down
6 changes: 3 additions & 3 deletions src/modules/get_azure_lb.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,17 +247,17 @@ def get_load_balancers_details(self) -> None:
parameters = []

def check_parameters(entity, parameters_dict, entity_type):
for key, expected_value in parameters_dict.items():
for key, value_object in parameters_dict.items():
parameters.append(
Parameters(
category=entity_type,
id=entity["name"],
name=key,
value=str(entity[key]),
expected_value=str(expected_value),
expected_value=str(value_object.get("value", "")),
status=(
TestStatus.SUCCESS.value
if entity[key] == expected_value
if entity[key] == value_object.get("value", "")
else TestStatus.ERROR.value
),
).to_dict()
Expand Down
3 changes: 3 additions & 0 deletions src/modules/get_pcmk_properties_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ class HAClusterValidator(BaseHAClusterValidator):
"azurelb": ".//primitive[@type='azure-lb']",
"angi_filesystem": ".//primitive[@type='SAPHanaFilesystem']",
"angi_hana": ".//primitive[@type='SAPHanaController']",
"azureevents": ".//primitive[@type='azure-events-az']",
}

def __init__(
Expand Down Expand Up @@ -227,6 +228,7 @@ def _validate_resource_constants(self):
"""
Resource validation with HANA-specific logic and offline validation support.
Validates resource constants by iterating through expected parameters.
Also checks for required resources.

:return: A list of parameter dictionaries
:rtype: list
Expand All @@ -243,6 +245,7 @@ def _validate_resource_constants(self):
if resource_scope is not None:
parameters.extend(self._parse_resources_section(resource_scope))

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

Expand Down
2 changes: 2 additions & 0 deletions src/modules/get_pcmk_properties_scs.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ def _validate_resource_constants(self):
"""
Resource validation with SCS-specific logic and offline validation support.
Validates resource constants by iterating through expected parameters.
Also checks for required resources.

:return: A list of parameter dictionaries
:rtype: list
Expand All @@ -240,6 +241,7 @@ def _validate_resource_constants(self):

if resource_scope is not None:
parameters.extend(self._parse_resources_section(resource_scope))
self._check_required_resources()

except Exception as ex:
self.result["message"] += f"Error validating resource constants: {str(ex)} "
Expand Down
68 changes: 68 additions & 0 deletions src/roles/ha_db_hana/tasks/files/constants.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ CRM_CONFIG_DEFAULTS:
stonith-enabled:
value: "true"
required: false
stonith-timeout:
value: ["210", "210s"]
required: false
concurrent-fencing:
value: "true"
required: false
Expand Down Expand Up @@ -451,6 +454,39 @@ RESOURCE_DEFAULTS:
timeout:
value: ["20", "20s"]
required: false

azureevents:
required: true
meta_attributes:
allow-unhealthy-nodes:
value: "true"
required: false
failure-timeout:
value: "120s"
required: false
operations:
monitor:
interval:
value: ["10", "10s"]
required: false
timeout:
value: ["240", "240s"]
required: false
start:
interval:
value: ["0", "0s"]
required: false
timeout:
value: ["10", "10s"]
required: false
stop:
interval:
value: ["0", "0s"]
required: false
timeout:
value: ["10", "10s"]
required: false

REDHAT:
fence_agent:
required: false
Expand Down Expand Up @@ -708,6 +744,38 @@ RESOURCE_DEFAULTS:
value: ["20", "20s"]
required: false

azureevents:
required: true
meta_attributes:
allow-unhealthy-nodes:
value: "true"
required: false
failure-timeout:
value: "120s"
required: false
operations:
monitor:
interval:
value: ["10", "10s"]
required: false
timeout:
value: ["240", "240s"]
required: false
start:
interval:
value: ["0", "0s"]
required: false
timeout:
value: ["10", "10s"]
required: false
stop:
interval:
value: ["0", "0s"]
required: false
timeout:
value: ["10", "10s"]
required: false

# === OS Parameters ===
# Run command as root. Format of command is: "parent_key child_key"
# Example: sysctl net.ipv4.tcp_timestamps
Expand Down
Loading