diff --git a/.gitignore b/.gitignore index 82d7c8f1..1bd7e322 100644 --- a/.gitignore +++ b/.gitignore @@ -409,3 +409,6 @@ VMWPASSWORD .coverage.* *.ini .ansible/ + +# GitHub Copilot configuration files +copilot-instructions.md diff --git a/requirements.txt b/requirements.txt index 3a5f1874..bd3a4766 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,14 +4,14 @@ # # pip-compile requirements.in # -ansible-compat==25.6.0 +ansible-compat==25.8.1 # via ansible-lint -ansible-core==2.17.13 +ansible-core==2.17.14 # via # -r requirements.in # ansible-compat # ansible-lint -ansible-lint==25.6.1 +ansible-lint==25.8.2 # via -r requirements.in ansible-runner==2.4.1 # via -r requirements.in @@ -30,7 +30,7 @@ azure-core==1.35.0 # azure-mgmt-core # azure-storage-blob # azure-storage-queue -azure-identity==1.23.1 +azure-identity==1.24.0 # via # -r requirements.in # azure-kusto-data @@ -58,21 +58,21 @@ black==25.1.0 # ansible-lint bracex==2.6 # via wcmatch -certifi==2025.7.14 +certifi==2025.8.3 # via requests -cffi==1.17.1 +cffi==2.0.0 # via cryptography -charset-normalizer==3.4.2 +charset-normalizer==3.4.3 # via requests click==8.2.1 # via # -r requirements.in # black -coverage[toml]==7.10.0 +coverage[toml]==7.10.6 # via # -r requirements.in # pytest-cov -cryptography==45.0.5 +cryptography==45.0.7 # via # ansible-core # azure-identity @@ -84,7 +84,7 @@ dill==0.4.0 # via pylint exceptiongroup==1.3.0 # via pytest -filelock==3.18.0 +filelock==3.19.1 # via ansible-lint idna==3.10 # via requests @@ -107,15 +107,15 @@ jinja2==3.1.6 # ansible-core jmespath==1.0.1 # via -r requirements.in -jsonschema==4.25.0 +jsonschema==4.25.1 # via # ansible-compat # ansible-lint -jsonschema-specifications==2025.4.1 +jsonschema-specifications==2025.9.1 # via jsonschema lockfile==0.12.2 # via python-daemon -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via rich markupsafe==3.0.2 # via jinja2 @@ -144,7 +144,7 @@ packaging==25.0 # ansible-runner # black # pytest -pandas==2.3.1 +pandas==2.3.2 # via -r requirements.in pathspec==0.12.1 # via @@ -153,7 +153,7 @@ pathspec==0.12.1 # yamllint pexpect==4.9.0 # via ansible-runner -platformdirs==4.3.8 +platformdirs==4.4.0 # via # black # pylint @@ -163,7 +163,7 @@ pluggy==1.6.0 # pytest-cov ptyprocess==0.7.0 # via pexpect -pycparser==2.22 +pycparser==2.23 # via cffi pygments==2.19.2 # via @@ -173,16 +173,16 @@ pyjwt[crypto]==2.10.1 # via # msal # pyjwt -pylint==3.3.7 +pylint==3.3.8 # via -r requirements.in -pytest==8.4.1 +pytest==8.4.2 # via # -r requirements.in # pytest-cov # pytest-mock -pytest-cov==6.2.1 +pytest-cov==7.0.0 # via -r requirements.in -pytest-mock==3.14.1 +pytest-mock==3.15.0 # via -r requirements.in python-daemon==3.1.2 # via ansible-runner @@ -205,7 +205,7 @@ referencing==0.36.2 # ansible-lint # jsonschema # jsonschema-specifications -requests==2.32.4 +requests==2.32.5 # via # -r requirements.in # azure-core @@ -213,13 +213,13 @@ requests==2.32.4 # msal resolvelib==1.0.1 # via ansible-core -rich==14.0.0 +rich==14.1.0 # via -r requirements.in -rpds-py==0.26.0 +rpds-py==0.27.1 # via # jsonschema # referencing -ruamel-yaml==0.18.14 +ruamel-yaml==0.18.15 # via ansible-lint ruamel-yaml-clib==0.2.12 # via ruamel-yaml @@ -241,7 +241,7 @@ tomli==2.2.1 # pytest tomlkit==0.13.3 # via pylint -typing-extensions==4.14.1 +typing-extensions==4.15.0 # via # astroid # azure-core @@ -252,7 +252,6 @@ typing-extensions==4.14.1 # black # exceptiongroup # referencing - # rich tzdata==2025.2 # via pandas urllib3==2.5.0 diff --git a/scripts/setup.sh b/scripts/setup.sh index 47b6a249..4abc0d49 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -13,9 +13,21 @@ set_output_context # Ensure we're in the project root directory cd "$(dirname "$script_dir")" -packages=("python3-pip" "ansible" "sshpass" "python3-venv") +packages=("python3-pip" "sshpass" "python3-venv") install_packages "${packages[@]}" +# Install az cli if not present +if ! command_exists az; then + log "INFO" "Azure CLI not found. Installing Azure CLI..." + curl -L https://aka.ms/InstallAzureCli | bash + if command_exists az; then + log "INFO" "Azure CLI installed successfully." + else + log "ERROR" "Failed to install Azure CLI. Please install it manually." + exit 1 + fi +fi + # Verify Python3 is available if ! command_exists python3; then log "ERROR" "Python3 is not available after installation. Please install Python3 manually." @@ -47,7 +59,7 @@ log "INFO" "Installing Python packages..." if ! pip install --upgrade pip; then log "ERROR" "Failed to upgrade pip." fi -if pip install pyyaml requests azure-identity azure-kusto-data azure-kusto-ingest azure-mgmt-network azure-storage-blob azure-storage-queue; then +if pip install -r requirements.in; then log "INFO" "Python packages installed successfully." else log "ERROR" "Failed to install Python packages." diff --git a/src/ansible.cfg b/src/ansible.cfg index 28b92a00..5c7636f6 100644 --- a/src/ansible.cfg +++ b/src/ansible.cfg @@ -9,7 +9,7 @@ display_skipped_hosts = False conditional_bare_variables = False interpreter_python = auto_silent callbacks_enabled = profile_tasks -stdout_callback = yaml +stdout_callback = default bin_ansible_callbacks = True host_key_checking = False error_on_undefined_vars = True diff --git a/src/module_utils/get_pcmk_properties.py b/src/module_utils/get_pcmk_properties.py index dc796165..322bc12d 100644 --- a/src/module_utils/get_pcmk_properties.py +++ b/src/module_utils/get_pcmk_properties.py @@ -89,14 +89,35 @@ def _get_expected_value(self, category, name): :param name: The name of the configuration parameter. :type name: str :return: The expected value for the configuration parameter. - :rtype: str + :rtype: tuple(str, bool) """ _, defaults_key = self.BASIC_CATEGORIES[category] fence_config = self.constants["VALID_CONFIGS"].get(self.fencing_mechanism, {}) os_config = self.constants["VALID_CONFIGS"].get(self.os_type, {}) - return fence_config.get(name) or os_config.get(name, self.constants[defaults_key].get(name)) + fence_param = fence_config.get(name, {}) + if fence_param: + if isinstance(fence_param, dict) and fence_param.get("value"): + return (fence_param.get("value", ""), fence_param.get("required", False)) + elif isinstance(fence_param, (str, list)): + return (fence_param, False) + + os_param = os_config.get(name, {}) + if os_param: + if isinstance(os_param, dict) and os_param.get("value"): + return (os_param.get("value", ""), os_param.get("required", False)) + elif isinstance(os_param, (str, list)): + return (os_param, False) + + default_param = self.constants[defaults_key].get(name, {}) + if default_param: + if isinstance(default_param, dict) and default_param.get("value"): + return (default_param.get("value", ""), default_param.get("required", False)) + elif isinstance(default_param, (str, list)): + return (default_param, False) + + return None def _get_resource_expected_value(self, resource_type, section, param_name, op_name=None): """ @@ -111,20 +132,21 @@ def _get_resource_expected_value(self, resource_type, section, param_name, op_na :param op_name: The name of the operation (if applicable), defaults to None :type op_name: str, optional :return: The expected value for the resource configuration parameter. - :rtype: str + :rtype: tuple(str, bool) """ resource_defaults = ( self.constants["RESOURCE_DEFAULTS"].get(self.os_type, {}).get(resource_type, {}) ) - + attr = None if section == "meta_attributes": - return resource_defaults.get("meta_attributes", {}).get(param_name) + attr = resource_defaults.get("meta_attributes", {}).get(param_name) elif section == "operations": ops = resource_defaults.get("operations", {}).get(op_name, {}) - return ops.get(param_name) + attr = ops.get(param_name) elif section == "instance_attributes": - return resource_defaults.get("instance_attributes", {}).get(param_name) - return None + attr = resource_defaults.get("instance_attributes", {}).get(param_name) + + return (attr.get("value"), attr.get("required", False)) if attr else None def _create_parameter( self, @@ -157,22 +179,36 @@ def _create_parameter( :rtype: dict """ if expected_value is None: - expected_value = self._get_expected_value_for_category( + expected_config = self._get_expected_value_for_category( category, subcategory, name, op_name ) + else: + if isinstance(expected_value, tuple) and len(expected_value) == 2: + expected_config = expected_value # Already in correct format + else: + expected_config = (expected_value, False) - status = self._determine_parameter_status(value, expected_value) + status = self._determine_parameter_status(value, expected_config) - if isinstance(expected_value, list): - expected_value = expected_value[0] if expected_value else "" - elif isinstance(expected_value, dict): - expected_value = ( + display_expected_value = None + if expected_config is None: + display_expected_value = "" + else: + if isinstance(expected_config, tuple): + display_expected_value = expected_config[0] + else: + display_expected_value = expected_config + + if isinstance(display_expected_value, list): + display_expected_value = display_expected_value[0] if display_expected_value else "" + elif isinstance(display_expected_value, dict): + display_expected_value = ( [ item - for val in expected_value.values() + for val in display_expected_value.values() for item in (val if isinstance(val, list) else [val]) ] - if expected_value + if display_expected_value else "" ) @@ -181,7 +217,7 @@ def _create_parameter( id=id if id else "", name=name if not op_name else f"{op_name}_{name}", value=value, - expected_value=expected_value if expected_value is not None else "", + expected_value=display_expected_value if display_expected_value is not None else "", status=status if status else TestStatus.ERROR.value, ).to_dict() @@ -211,34 +247,47 @@ def _get_expected_value_for_category(self, category, subcategory, name, op_name) else: return self._get_expected_value(category, name) - def _determine_parameter_status(self, value, expected_value): + def _determine_parameter_status(self, value, expected_config): """ Determine the status of a parameter based on its value and expected value. :param value: The actual value of the parameter. :type value: str - :param expected_value: The expected value of the parameter. - :type expected_value: str or list or dict + :param expected_config: The expected value of the parameter and bool indicating if required. + :type expected_config: tuple(str, bool) :return: The status of the parameter. :rtype: str """ - if expected_value is None or value == "": + if expected_config is None: return TestStatus.INFO.value - elif isinstance(expected_value, (str, list)): - if isinstance(expected_value, list): - return ( - TestStatus.SUCCESS.value - if str(value) in expected_value - else TestStatus.ERROR.value - ) + + if isinstance(expected_config, tuple): + expected_value, is_required = expected_config + elif isinstance(expected_config, dict): + expected_value = expected_config.get("value") + is_required = expected_config.get("required", False) + else: + expected_value = expected_config + is_required = False + + if not value or value == "": + if is_required: + return TestStatus.WARNING.value else: - return ( - TestStatus.SUCCESS.value - if str(value) == str(expected_value) - else TestStatus.ERROR.value - ) + return TestStatus.INFO.value + + if expected_value is None or expected_value == "": + return TestStatus.INFO.value + elif isinstance(expected_value, list): + return ( + TestStatus.SUCCESS.value if str(value) in expected_value else TestStatus.ERROR.value + ) else: - return TestStatus.ERROR.value + return ( + TestStatus.SUCCESS.value + if str(value) == str(expected_value) + else TestStatus.ERROR.value + ) def _parse_nvpair_elements(self, elements, category, subcategory=None, op_name=None): """ @@ -297,38 +346,12 @@ def _parse_os_parameters(self): id=section, name=param_name, value=value, - expected_value=expected_value, + expected_value=expected_value.get("value", "") if expected_value else None, ) ) return parameters - def _parse_basic_config(self, element, category, subcategory=None): - """ - Parse basic configuration parameters - - :param element: The XML element to parse. - :type element: xml.etree.ElementTree.Element - :param category: The category of the configuration parameter. - :type category: str - :param subcategory: The subcategory of the configuration parameter, defaults to None - :type subcategory: str, optional - :return: A list of parameter dictionaries. - :rtype: list - """ - parameters = [] - for nvpair in element.findall(".//nvpair"): - parameters.append( - self._create_parameter( - category=category, - subcategory=subcategory, - name=nvpair.get("name", ""), - value=nvpair.get("value", ""), - id=nvpair.get("id", ""), - ) - ) - return parameters - def _parse_resource(self, element, category): """ Parse resource-specific configuration parameters @@ -376,37 +399,6 @@ def _parse_resource(self, element, category): ) return parameters - def _parse_constraints(self, root): - """ - Parse constraints configuration parameters - - :param root: The XML root element to parse. - :type root: xml.etree.ElementTree.Element - :return: A list of parameter dictionaries. - :rtype: list - """ - parameters = [] - for element in root: - tag = element.tag - if tag in self.constants["CONSTRAINTS"]: - for attr, expected in self.constants["CONSTRAINTS"][tag].items(): - if element.get(attr) is not None: - parameters.append( - self._create_parameter( - category="constraints", - subcategory=tag, - id=element.get("id", ""), - name=attr, - value=element.get(attr), - expected_value=expected, - ) - ) - else: - continue - else: - continue - return parameters - def _parse_resources_section(self, root): """ Parse resources section - can be overridden by subclasses for custom resource parsing. @@ -476,57 +468,26 @@ def _get_scope_from_cib(self, scope): return self.cib_output.find(xpath) return None - def parse_ha_cluster_config(self): + def validate_from_constants(self): """ - Parse HA cluster configuration XML and return a list of properties. - This is the main orchestration method that coordinates all parsing activities. + Constants-first validation approach: iterate through constants and validate against CIB. + This ensures all expected parameters are checked, with offline validation support. """ parameters = [] - scopes = [ - "rsc_defaults", - "crm_config", - "op_defaults", - "constraints", - "resources", - ] - - for scope in scopes: - if self._should_skip_scope(scope): - continue - - self.category = scope - if self.cib_output: - root = self._get_scope_from_cib(scope) - else: - root = self.parse_xml_output( - self.execute_command_subprocess(CIB_ADMIN(scope=scope)) - ) - if not root: - continue - - try: - if self.category in self.BASIC_CATEGORIES: - xpath = self.BASIC_CATEGORIES[self.category][0] - for element in root.findall(xpath): - parameters.extend(self._parse_basic_config(element, self.category)) - - elif self.category == "resources": - parameters.extend(self._parse_resources_section(root)) - - elif self.category == "constraints": - parameters.extend(self._parse_constraints(root)) - - except Exception as ex: - self.result["message"] += f"Failed to get {self.category} configuration: {str(ex)}" - continue + for category in ["crm_config", "rsc_defaults", "op_defaults"]: + if not self._should_skip_scope(category): + parameters.extend(self._validate_basic_constants(category)) + parameters.extend(self._validate_resource_constants()) + parameters.extend(self._validate_constraint_constants()) try: if not self.cib_output: parameters.extend(self._parse_os_parameters()) else: self.result["message"] += "CIB output provided, skipping OS parameters parsing. " except Exception as ex: - self.result["message"] += f"Failed to get OS parameters: {str(ex)} \n" + self.result["message"] += f"Failed to get OS parameters: {str(ex)} " + try: if not self.cib_output: parameters.extend(self._get_additional_parameters()) @@ -535,18 +496,174 @@ def parse_ha_cluster_config(self): "message" ] += "CIB output provided, skipping additional parameters parsing. " except Exception as ex: - self.result["message"] += f"Failed to get additional parameters: {str(ex)} \n" + self.result["message"] += f"Failed to get additional parameters: {str(ex)} " + failed_parameters = [ param for param in parameters if param.get("status", TestStatus.ERROR.value) == TestStatus.ERROR.value ] + warning_parameters = [ + param for param in parameters if param.get("status", "") == TestStatus.WARNING.value + ] + + if failed_parameters: + overall_status = TestStatus.ERROR.value + elif warning_parameters: + overall_status = TestStatus.WARNING.value + else: + overall_status = TestStatus.SUCCESS.value + self.result.update( { "details": {"parameters": parameters}, - "status": ( - TestStatus.ERROR.value if failed_parameters else TestStatus.SUCCESS.value - ), + "status": overall_status, } ) self.result["message"] += "HA Parameter Validation completed successfully. " + + def _validate_basic_constants(self, category): + """ + Validate basic configuration constants with offline validation support. + Uses existing CIB parsing logic but focuses on constants-first approach. + Creates dynamic subcategories based on element IDs found in CIB. + + :param category: The category to validate (crm_config, rsc_defaults, op_defaults) + :type category: str + :return: A list of parameter dictionaries + :rtype: list + """ + parameters = [] + + if category not in self.BASIC_CATEGORIES: + return parameters + + _, constants_key = self.BASIC_CATEGORIES[category] + category_constants = self.constants.get(constants_key, {}) + + for param_name, expected_config in category_constants.items(): + param_value, param_id = self._find_param_with_element_info(category, param_name) + expected_result = self._get_expected_value(category, param_name) + if expected_result: + expected_value, is_required = expected_result + expected_config_tuple = (expected_value, is_required) + else: + if isinstance(expected_config, dict): + expected_value = expected_config.get("value", "") + is_required = expected_config.get("required", False) + expected_config_tuple = (expected_value, is_required) + else: + expected_value = str(expected_config) + expected_config_tuple = (expected_value, False) + + parameters.append( + self._create_parameter( + category=category, + name=param_name, + value=param_value, + expected_value=expected_config_tuple, + subcategory=param_id if param_id else "", + id=param_id, + ) + ) + + return parameters + + def _find_param_with_element_info(self, category, param_name): + """ + Find a parameter value and its own unique ID in CIB XML. + Returns both the parameter value and the parameter's own ID (not container ID). + + :param category: The category scope to search in (crm_config, rsc_defaults, op_defaults) + :type category: str + :param param_name: The parameter name to find + :type param_name: str + :return: Tuple of (parameter_value, parameter_id) or ("", "") if not found + :rtype: tuple(str, str) + """ + param_value, param_id = "", "" + try: + if self.cib_output: + root = self._get_scope_from_cib(category) + else: + root = self.parse_xml_output( + self.execute_command_subprocess(CIB_ADMIN(scope=category)) + ) + + if not root: + return param_value, param_id + + if category in self.BASIC_CATEGORIES: + for element in root.findall(self.BASIC_CATEGORIES[category][0]): + for nvpair in element.findall(".//nvpair"): + if nvpair.get("name") == param_name: + param_id = nvpair.get("id", "") + param_value = nvpair.get("value", "") + return param_value, param_id + + except Exception as ex: + self.result[ + "message" + ] += f"Error finding parameter {param_name} in {category}: {str(ex)} " + + return param_value, param_id + + def _validate_resource_constants(self): + """ + Resource validation - to be overridden by subclasses. + Base implementation returns empty list. + + :return: A list of parameter dictionaries + :rtype: list + """ + return [] + + def _validate_constraint_constants(self): + """ + Validate constraint constants with offline validation support. + Uses constants-first approach to validate constraints against CIB. + + :return: A list of parameter dictionaries + :rtype: list + """ + parameters = [] + + if "CONSTRAINTS" not in self.constants: + return parameters + + try: + if self.cib_output: + constraints_scope = self._get_scope_from_cib("constraints") + else: + constraints_scope = self.parse_xml_output( + self.execute_command_subprocess(CIB_ADMIN(scope="constraints")) + ) + + if constraints_scope is not None: + for constraint_type, constraint_config in self.constants["CONSTRAINTS"].items(): + elements = constraints_scope.findall(f".//{constraint_type}") + + for element in elements: + for attr_name, expected_config in constraint_config.items(): + actual_value = element.get(attr_name, "") + expected_value = ( + expected_config.get("value") + if isinstance(expected_config, dict) + else expected_config + ) + + parameters.append( + self._create_parameter( + category="constraints", + subcategory=constraint_type, + id=element.get("id", ""), + name=attr_name, + value=actual_value, + expected_value=expected_value, + ) + ) + + except Exception as ex: + self.result["message"] += f"Error validating constraint constants: {str(ex)} " + + return parameters diff --git a/src/module_utils/sap_automation_qa.py b/src/module_utils/sap_automation_qa.py index 4dc6dae2..bd94b369 100644 --- a/src/module_utils/sap_automation_qa.py +++ b/src/module_utils/sap_automation_qa.py @@ -7,7 +7,7 @@ import sys import logging import subprocess -from typing import Optional, Dict, Any +from typing import Dict, Any import xml.etree.ElementTree as ET try: diff --git a/src/modules/get_pcmk_properties_db.py b/src/modules/get_pcmk_properties_db.py index 2eeedb15..843da940 100644 --- a/src/modules/get_pcmk_properties_db.py +++ b/src/modules/get_pcmk_properties_db.py @@ -18,9 +18,11 @@ try: from ansible.module_utils.get_pcmk_properties import BaseHAClusterValidator from ansible.module_utils.enums import OperatingSystemFamily, HanaSRProvider + from ansible.module_utils.commands import CIB_ADMIN except ImportError: from src.module_utils.get_pcmk_properties import BaseHAClusterValidator from src.module_utils.enums import OperatingSystemFamily, HanaSRProvider + from src.module_utils.commands import CIB_ADMIN DOCUMENTATION = r""" --- @@ -194,7 +196,7 @@ def __init__( ) self.instance_number = instance_number self.saphanasr_provider = saphanasr_provider - self.parse_ha_cluster_config() + self.validate_from_constants() def _parse_resources_section(self, root): """ @@ -211,6 +213,8 @@ def _parse_resources_section(self, root): resource_categories.pop("topology", None) else: resource_categories.pop("angi_topology", None) + resource_categories.pop("angi_filesystem", None) + resource_categories.pop("angi_hana", None) for sub_category, xpath in resource_categories.items(): elements = root.findall(xpath) @@ -219,6 +223,31 @@ def _parse_resources_section(self, root): return parameters + def _validate_resource_constants(self): + """ + Resource validation with HANA-specific logic and offline validation support. + Validates resource constants by iterating through expected parameters. + + :return: A list of parameter dictionaries + :rtype: list + """ + parameters = [] + + 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 not None: + parameters.extend(self._parse_resources_section(resource_scope)) + + except Exception as ex: + self.result["message"] += f"Error validating resource constants: {str(ex)} " + + return parameters + def _parse_global_ini_parameters(self): """ Parse global.ini parameters specific to SAP HANA. @@ -255,22 +284,29 @@ def _parse_global_ini_parameters(self): if sep } - for param_name, expected_value in global_ini_defaults.items(): + for param_name, expected_config in global_ini_defaults.items(): value = global_ini_properties.get(param_name, "") - if isinstance(expected_value, list): - if value in expected_value: - expected_value = value + if isinstance(expected_config, dict): + expected_value = expected_config.get("value") + is_required = expected_config.get("required", False) + else: + expected_value = expected_config + is_required = False self.log( logging.INFO, - f"param_name: {param_name}, value: {value}, expected_value: {expected_value}", + f"param_name: {param_name}, value: {value}, expected_value: {expected_config}", ) parameters.append( self._create_parameter( category="global_ini", name=param_name, value=value, - expected_value=expected_value, + expected_value=( + expected_config.get("value") + if isinstance(expected_config, dict) + else expected_value + ), ) ) except Exception as ex: diff --git a/src/modules/get_pcmk_properties_scs.py b/src/modules/get_pcmk_properties_scs.py index 8b3d95d5..59680f1c 100644 --- a/src/modules/get_pcmk_properties_scs.py +++ b/src/modules/get_pcmk_properties_scs.py @@ -17,9 +17,11 @@ try: from ansible.module_utils.get_pcmk_properties import BaseHAClusterValidator from ansible.module_utils.enums import OperatingSystemFamily, TestStatus + from ansible.module_utils.commands import CIB_ADMIN except ImportError: from src.module_utils.get_pcmk_properties import BaseHAClusterValidator from src.module_utils.enums import OperatingSystemFamily, TestStatus + from src.module_utils.commands import CIB_ADMIN DOCUMENTATION = r""" @@ -191,7 +193,7 @@ def __init__( self.scs_instance_number = scs_instance_number self.ers_instance_number = ers_instance_number self.nfs_provider = nfs_provider - self.parse_ha_cluster_config() + self.validate_from_constants() def _get_expected_value_for_category(self, category, subcategory, name, op_name): """ @@ -218,17 +220,49 @@ def _get_expected_value_for_category(self, category, subcategory, name, op_name) else: return self._get_expected_value(category, name) + def _validate_resource_constants(self): + """ + Resource validation with SCS-specific logic and offline validation support. + Validates resource constants by iterating through expected parameters. + + :return: A list of parameter dictionaries + :rtype: list + """ + parameters = [] + + 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 not None: + parameters.extend(self._parse_resources_section(resource_scope)) + + except Exception as ex: + self.result["message"] += f"Error validating resource constants: {str(ex)} " + + return parameters + def _determine_parameter_status(self, value, expected_value): """ Determine the status of a parameter with SCS-specific logic for NFS provider. :param value: The actual value of the parameter. :type value: str - :param expected_value: The expected value of the parameter. - :type expected_value: str or list or dict + :param expected_value: The expected value tuple (value, required) or legacy format. + :type expected_value: tuple or str or list or dict :return: The status of the parameter. :rtype: str """ + if isinstance(expected_value, tuple): + expected_val, required = expected_value + if not required and (expected_val is None or value == ""): + return TestStatus.INFO.value + expected_value = expected_val + if expected_value is None or value == "": return TestStatus.INFO.value elif isinstance(expected_value, (str, list)): @@ -245,12 +279,38 @@ def _determine_parameter_status(self, value, expected_value): else TestStatus.ERROR.value ) elif isinstance(expected_value, dict): - provider_values = expected_value.get(self.nfs_provider, expected_value.get("AFS", [])) - return ( - TestStatus.SUCCESS.value - if str(value) in provider_values - else TestStatus.ERROR.value - ) + provider_values = [] + if self.nfs_provider and self.nfs_provider in expected_value: + provider_config = expected_value[self.nfs_provider] + if isinstance(provider_config, dict) and "value" in provider_config: + provider_values = provider_config["value"] + else: + provider_values = provider_config + else: + # If provider is unknown/not set, collect all provider values + for provider_key, provider_config in expected_value.items(): + if isinstance(provider_config, dict) and "value" in provider_config: + if isinstance(provider_config["value"], list): + provider_values.extend(provider_config["value"]) + else: + provider_values.append(provider_config["value"]) + elif isinstance(provider_config, list): + provider_values.extend(provider_config) + else: + provider_values.append(provider_config) + + if isinstance(provider_values, list): + return ( + TestStatus.SUCCESS.value + if str(value) in provider_values + else TestStatus.ERROR.value + ) + else: + return ( + TestStatus.SUCCESS.value + if str(value) == str(provider_values) + else TestStatus.ERROR.value + ) else: return TestStatus.ERROR.value diff --git a/src/roles/ha_db_hana/tasks/files/constants.yaml b/src/roles/ha_db_hana/tasks/files/constants.yaml index fa103cdf..f2865b5d 100644 --- a/src/roles/ha_db_hana/tasks/files/constants.yaml +++ b/src/roles/ha_db_hana/tasks/files/constants.yaml @@ -7,330 +7,706 @@ # === CRM Configuration Defaults === # cibadmin --query --scope crm_config CRM_CONFIG_DEFAULTS: - cluster-infrastructure: corosync - priority-fencing-delay: ['30', '30s'] - stonith-action: reboot - stonith-enabled: 'true' - concurrent-fencing: 'true' - maintenance-mode: 'false' - node-health-strategy: 'custom' - azure-events-az_globalPullState: 'IDLE' + cluster-infrastructure: + value: corosync + required: false + priority-fencing-delay: + value: ["30", "30s"] + required: true + stonith-action: + value: reboot + required: false + stonith-enabled: + value: "true" + required: false + concurrent-fencing: + value: "true" + required: false + maintenance-mode: + value: "false" + required: false + node-health-strategy: + value: "custom" + required: false + azure-events-az_globalPullState: + value: "IDLE" + required: false # === Operation Defaults === # cibadmin --query --scope op_defaults OP_DEFAULTS: - record-pending: 'true' - timeout: ['600', '600s'] + record-pending: + value: "true" + required: false + timeout: + value: ["600", "600s"] + required: false # === Resource Defaults === # cibadmin --query --scope rsc_defaults RSC_DEFAULTS: - migration-threshold: '5000' - priority: '1' - resource-stickiness: '1000' + migration-threshold: + value: "5000" + required: false + priority: + value: "1" + required: false + resource-stickiness: + value: "1000" + required: false # === Constraints === # cibadmin --query --scope constraints CONSTRAINTS: rsc_colocation: - score: "4000" - rsc-role: "Started" + score: + value: "4000" + required: false + rsc-role: + value: "Started" + required: false rsc_order: - kind: "Optional" + kind: + value: "Optional" + required: false # === Valid Configurations for different OS versions === # Specify the properties that are different for different OS versions VALID_CONFIGS: REDHAT: - priority-fencing-delay: ['15', '15s'] + priority-fencing-delay: + value: ["15", "15s"] + required: true SUSE: {} AFA: - have-watchdog: "false" - stonith-timeout: ["900s", "900"] + have-watchdog: + value: "false" + required: true + stonith-timeout: + value: ["900s", "900"] + required: true ISCSI: - have-watchdog: "true" - stonith-timeout: ["210", "210s"] - + have-watchdog: + value: "true" + required: true + stonith-timeout: + value: ["210", "210s"] + required: true + ASD: + have-watchdog: + value: "true" + required: true + stonith-timeout: + value: ["210", "210s"] + required: true # === Resource Defaults === # cibadmin --query --scope resources RESOURCE_DEFAULTS: SUSE: fence_agent: + required: false instance_attributes: - pcmk_delay_max: "15" - pcmk_monitor_retries: "4" - pcmk_action_limit: "3" - pcmk_reboot_timeout: ["900", "900s"] - power_timeout: ["240", "240s"] - pcmk_monitor_timeout: ["120", "120s"] + pcmk_delay_max: + value: "15" + required: false + pcmk_monitor_retries: + value: "4" + required: false + pcmk_action_limit: + value: "3" + required: false + pcmk_reboot_timeout: + value: ["900", "900s"] + required: false + power_timeout: + value: ["240", "240s"] + required: false + pcmk_monitor_timeout: + value: ["120", "120s"] + required: false operations: monitor: - interval: ["3600", "3600s"] - timeout: ["120", "120s"] + interval: + value: ["3600", "3600s"] + required: false + timeout: + value: ["120", "120s"] + required: false sbd_stonith: + required: false instance_attributes: - pcmk_delay_max: "15" - pcmk_monitor_retries: "4" - pcmk_action_limit: "3" - pcmk_reboot_timeout: ["900", "900s"] - power_timeout: ["240", "240s"] - pcmk_monitor_timeout: ["120", "120s"] + pcmk_delay_max: + value: "15" + required: false + pcmk_monitor_retries: + value: "4" + required: false + pcmk_action_limit: + value: "3" + required: false + pcmk_reboot_timeout: + value: ["900", "900s"] + required: false + power_timeout: + value: ["240", "240s"] + required: false + pcmk_monitor_timeout: + value: ["120", "120s"] + required: false operations: monitor: - interval: ["600", "600s"] - timeout: ["15", "15s"] + interval: + value: ["600", "600s"] + required: false + timeout: + value: ["15", "15s"] + required: false topology: + required: false meta_attributes: - clone-node-max: "1" - target-role: "Started" - interleave: "true" + clone-node-max: + value: "1" + required: false + target-role: + value: "Started" + required: false + interleave: + value: "true" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["600", "600s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["600", "600s"] + required: false start: - interval: ["0", "0s"] - timeout: ["600", "600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["600", "600s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["300", "300s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["300", "300s"] + required: false angi_topology: + required: false meta_attributes: - clone-node-max: "1" - target-role: "Started" - interleave: "true" + clone-node-max: + value: "1" + required: false + target-role: + value: "Started" + required: false + interleave: + value: "true" + required: false operations: monitor: - interval: ["50", "50s"] - timeout: ["600", "600s"] + interval: + value: ["50", "50s"] + required: false + timeout: + value: ["600", "600s"] + required: false start: - interval: ["0", "0s"] - timeout: ["600", "600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["600", "600s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["300", "300s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["300", "300s"] + required: false hana: + required: false meta_attributes: - notify: "true" - clone-max: "2" - clone-node-max: "1" - target-role: "Started" - interleave: "true" - priority: "100" + notify: + value: "true" + required: false + clone-max: + value: "2" + required: false + clone-node-max: + value: "1" + required: false + target-role: + value: "Started" + required: false + interleave: + value: "true" + required: false + priority: + value: "100" + required: false instance_attributes: - PREFER_SITE_TAKEOVER: "true" - DUPLICATE_PRIMARY_TIMEOUT: "7200" - AUTOMATED_REGISTER: "true" + PREFER_SITE_TAKEOVER: + value: "true" + required: false + DUPLICATE_PRIMARY_TIMEOUT: + value: "7200" + required: false + AUTOMATED_REGISTER: + value: "true" + required: false operations: start: - interval: ["0", "0s"] - timeout: ["3600", "3600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["3600", "3600s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["3600", "3600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["3600", "3600s"] + required: false promote: - interval: ["0", "0s"] - timeout: ["3600", "3600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["3600", "3600s"] + required: false monitor: - timeout: ["700", "700s"] + timeout: + value: ["700", "700s"] + required: false angi_hana: + required: false meta_attributes: - notify: "true" - clone-max: "2" - clone-node-max: "1" - target-role: "Started" - interleave: "true" - priority: "100" + notify: + value: "true" + required: false + clone-max: + value: "2" + required: false + clone-node-max: + value: "1" + required: false + target-role: + value: "Started" + required: false + interleave: + value: "true" + required: false + priority: + value: "100" + required: false instance_attributes: - PREFER_SITE_TAKEOVER: "true" - DUPLICATE_PRIMARY_TIMEOUT: "7200" - AUTOMATED_REGISTER: "true" + PREFER_SITE_TAKEOVER: + value: "true" + required: false + DUPLICATE_PRIMARY_TIMEOUT: + value: "7200" + required: false + AUTOMATED_REGISTER: + value: "true" + required: false operations: start: - interval: ["0", "0s"] - timeout: ["3600", "3600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["3600", "3600s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["3600", "3600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["3600", "3600s"] + required: false promote: - interval: ["0", "0s"] - timeout: ["3600", "3600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["3600", "3600s"] + required: false demote: - interval: ["0", "0s"] - timeout: ["320", "320s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["320", "320s"] + required: false monitor: - timeout: ["700", "700s"] + timeout: + value: ["700", "700s"] + required: false ipaddr: + required: false meta_attributes: - target-role: "Started" + target-role: + value: "Started" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["20", "20s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["20", "20s"] + required: false filesystem: + required: false meta_attributes: - clone-node-max: "1" - interleave: "true" + clone-node-max: + value: "1" + required: false + interleave: + value: "true" + required: false operations: monitor: - interval: ["120", "120s"] - timeout: ["120", "120s"] + interval: + value: ["120", "120s"] + required: false + timeout: + value: ["120", "120s"] + required: false start: - interval: ["0", "0s"] - timeout: ["120", "120s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["120", "120s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["120", "120s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["120", "120s"] + required: false angi_filesystem: + required: false meta_attributes: - clone-node-max: "1" - interleave: "true" + clone-node-max: + value: "1" + required: true + interleave: + value: "true" + required: false operations: monitor: - interval: ["120", "120s"] - timeout: ["120", "120s"] + interval: + value: ["120", "120s"] + required: false + timeout: + value: ["120", "120s"] + required: false start: - interval: ["0", "0s"] - timeout: ["10", "10s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["10", "10s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false azurelb: + required: false meta_attributes: - resource-stickiness: "0" + resource-stickiness: + value: "0" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["20", "20s"] - + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["20", "20s"] + required: false REDHAT: fence_agent: + required: false instance_attributes: - pcmk_delay_max: "15" - pcmk_monitor_retries: "4" - pcmk_action_limit: "3" - pcmk_reboot_timeout: "900" - power_timeout: "240" - pcmk_monitor_timeout: "120" + pcmk_delay_max: + value: "15" + required: false + pcmk_monitor_retries: + value: "4" + required: false + pcmk_action_limit: + value: "3" + required: false + pcmk_reboot_timeout: + value: "900" + required: false + power_timeout: + value: "240" + required: false + pcmk_monitor_timeout: + value: "120" + required: false operations: monitor: - interval: ["3600", "3600s"] + interval: + value: ["3600", "3600s"] + required: false sbd_stonith: + required: false instance_attributes: - pcmk_monitor_retries: "4" - pcmk_action_limit: "3" - pcmk_reboot_timeout: ["900", "900s"] - power_timeout: ["240", "240s"] - pcmk_monitor_timeout: ["120", "120s"] + pcmk_monitor_retries: + value: "4" + required: false + pcmk_action_limit: + value: "3" + required: false + pcmk_reboot_timeout: + value: ["900", "900s"] + required: false + power_timeout: + value: ["240", "240s"] + required: false + pcmk_monitor_timeout: + value: ["120", "120s"] + required: false operations: monitor: - interval: ["600", "600s"] - timeout: ["15", "15s"] + interval: + value: ["600", "600s"] + required: false + timeout: + value: ["15", "15s"] + required: false topology: + required: false meta_attributes: - clone-node-max: "1" - clone-max: "2" - target-role: "Started" - interleave: "true" - failure-timeout: "120s" + clone-node-max: + value: "1" + required: false + clone-max: + value: "2" + required: false + target-role: + value: "Started" + required: false + interleave: + value: "true" + required: false + failure-timeout: + value: "120s" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["600", "600s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["600", "600s"] + required: false start: - interval: ["0", "0s"] - timeout: ["600", "600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["600", "600s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["300", "300s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["300", "300s"] + required: false methods: - timeout: ["5", "5s"] - interval: ["0", "0s"] + timeout: + value: ["5", "5s"] + required: false + interval: + value: ["0", "0s"] + required: false reload: - timeout: ["5", "5s"] - interval: ["0", "0s"] + timeout: + value: ["5", "5s"] + required: false + interval: + value: ["0", "0s"] + required: false hana: + required: false meta_attributes: - notify: "true" - clone-max: "2" - clone-node-max: "1" - target-role: "Started" - interleave: "true" - priority: "100" + notify: + value: "true" + required: false + clone-max: + value: "2" + required: false + clone-node-max: + value: "1" + required: false + target-role: + value: "Started" + required: false + interleave: + value: "true" + required: false + priority: + value: "100" + required: true instance_attributes: - PREFER_SITE_TAKEOVER: "true" - DUPLICATE_PRIMARY_TIMEOUT: "7200" - AUTOMATED_REGISTER: "true" + PREFER_SITE_TAKEOVER: + value: "true" + required: false + DUPLICATE_PRIMARY_TIMEOUT: + value: "7200" + required: false + AUTOMATED_REGISTER: + value: "true" + required: false operations: start: - interval: ["0", "0s"] - timeout: ["3600", "3600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["3600", "3600s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["3600", "3600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["3600", "3600s"] + required: false promote: - interval: ["0", "0s"] - timeout: ["3600", "3600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["3600", "3600s"] + required: false monitor: - timeout: ["700", "700s"] + timeout: + value: ["700", "700s"] + required: false ipaddr: meta_attributes: - target-role: "Started" + target-role: + value: "Started" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["20", "20s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["20", "20s"] + required: false start: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false filesystem: + required: false meta_attributes: - clone-node-max: "1" - interleave: "true" + clone-node-max: + value: "1" + required: false + interleave: + value: "true" + required: false operations: monitor: - interval: ["20", "20s"] - timeout: ["120", "120s"] + interval: + value: ["20", "20s"] + required: false + timeout: + value: ["120", "120s"] + required: false start: - interval: ["0", "0s"] - timeout: ["60", "60s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["60", "60s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["60", "60s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["60", "60s"] + required: false azurelb: + required: false meta_attributes: - resource-stickiness: "0" + resource-stickiness: + value: "0" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["20", "20s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["20", "20s"] + required: false start: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["20", "20s"] - + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false # === OS Parameters === # Run command as root. Format of command is: "parent_key child_key" @@ -338,39 +714,74 @@ RESOURCE_DEFAULTS: OS_PARAMETERS: DEFAULTS: sysctl: - net.ipv4.tcp_timestamps: "net.ipv4.tcp_timestamps = 0" - vm.swappiness: "vm.swappiness = 10" + net.ipv4.tcp_timestamps: + value: "net.ipv4.tcp_timestamps = 0" + required: true + vm.swappiness: + value: "vm.swappiness = 10" + required: true corosync-cmapctl: - runtime.config.totem.token: "runtime.config.totem.token (u32) = 30000" - runtime.config.totem.consensus: "runtime.config.totem.consensus (u32) = 36000" + runtime.config.totem.token: + value: "runtime.config.totem.token (u32) = 30000" + required: true + runtime.config.totem.consensus: + value: "runtime.config.totem.consensus (u32) = 36000" + required: true # === Global INI === # Reading the global.ini file to get the provider and path for the SAPHanaSR resource agent GLOBAL_INI: + GLOBAL_INI: SUSE: SAPHanaSR: - provider: "SAPHanaSR" - path: ["/usr/share/SAPHanaSR", "/hana/shared/myHooks"] - execution_order: "1" + provider: + value: "SAPHanaSR" + required: true + path: + value: ["/usr/share/SAPHanaSR", "/hana/shared/myHooks"] + required: true + execution_order: + value: "1" + required: true SAPHanaSR-angi: - provider: "susHanaSR" - path: ["/usr/share/SAPHanaSR", "/hana/shared/myHooks"] - execution_order: "1" + provider: + value: "susHanaSR" + required: true + path: + value: ["/usr/share/SAPHanaSR", "/hana/shared/myHooks"] + required: true + execution_order: + value: "1" + required: true REDHAT: SAPHanaSR: - provider: "SAPHanaSR" - path: ["/usr/share/SAPHanaSR/srHook", "/hana/shared/myHooks"] - execution_order: "1" - + provider: + value: "SAPHanaSR" + required: true + path: + value: ["/usr/share/SAPHanaSR/srHook", "/hana/shared/myHooks"] + required: true + execution_order: + value: "1" + required: true # === Azure Load Balancer === # Azure Load Balancer configuration AZURE_LOADBALANCER: PROBES: - probe_threshold: 2 - interval_in_seconds: 5 - + probe_threshold: + value: 2 + required: true + interval_in_seconds: + value: 5 + required: true RULES: - idle_timeout_in_minutes: 30 - enable_floating_ip: true - enable_tcp_reset: false + idle_timeout_in_minutes: + value: 30 + required: true + enable_floating_ip: + value: true + required: true + enable_tcp_reset: + value: false + required: false diff --git a/src/roles/ha_scs/tasks/files/constants.yaml b/src/roles/ha_scs/tasks/files/constants.yaml index 8fec70fc..3348fccf 100644 --- a/src/roles/ha_scs/tasks/files/constants.yaml +++ b/src/roles/ha_scs/tasks/files/constants.yaml @@ -7,324 +7,675 @@ # === CRM Configuration Defaults === # cibadmin --query --scope crm_config CRM_CONFIG_DEFAULTS: - cluster-infrastructure: corosync - priority-fencing-delay: ["30", "30s"] - stonith-action: reboot - stonith-enabled: "true" - concurrent-fencing: "true" - maintenance-mode: "false" - node-health-strategy: "custom" - azure-events-az_globalPullState: "IDLE" + cluster-infrastructure: + value: corosync + required: false + priority-fencing-delay: + value: ["30", "30s"] + required: true + stonith-action: + value: reboot + required: false + stonith-enabled: + value: "true" + required: false + concurrent-fencing: + value: "true" + required: false + maintenance-mode: + value: "false" + required: false + node-health-strategy: + value: "custom" + required: false + azure-events-az_globalPullState: + value: "IDLE" + required: false # === Operation Defaults === # cibadmin --query --scope op_defaults OP_DEFAULTS: - record-pending: "true" - timeout: ["600", "600s"] + record-pending: + value: "true" + required: false + timeout: + value: ["600", "600s"] + required: false # === Resource Defaults === # cibadmin --query --scope rsc_defaults RSC_DEFAULTS: - migration-threshold: "3" - priority: "1" - resource-stickiness: "1" + migration-threshold: + value: "3" + required: false + priority: + value: "1" + required: false + resource-stickiness: + value: "1" + required: false # === Constraints === # cibadmin --query --scope constraints CONSTRAINTS: rsc_colocation: - score: "-5000" - rsc-role: "Started" - with-rsc-role: "Started" + score: + value: "-5000" + required: false + rsc-role: + value: "Started" + required: false + with-rsc-role: + value: "Started" + required: false rsc_order: - first-action: "start" - then-action: "stop" - symmetrical: "false" + first-action: + value: "start" + required: false + then-action: + value: "stop" + required: false + symmetrical: + value: "false" + required: false rsc_location: - score-attribute: "#health-azure" - operation: "defined" - attribute: "#uname" + score-attribute: + value: "#health-azure" + required: false + operation: + value: "defined" + required: false + attribute: + value: "#uname" + required: false # === Valid Configurations for different OS versions === # Specify the properties that are different for different OS versions VALID_CONFIGS: REDHAT: - priority-fencing-delay: "15s" + priority-fencing-delay: + value: ["15", "15s"] + required: false SUSE: {} AFA: - have-watchdog: "false" - stonith-timeout: ["900", "900s"] + have-watchdog: + value: "false" + required: false + stonith-timeout: + value: ["900", "900s"] + required: false ISCSI: - have-watchdog: "true" - stonith-timeout: ["210", "210s"] + have-watchdog: + value: "true" + required: false + stonith-timeout: + value: ["210", "210s"] + required: false + ASD: + have-watchdog: + value: "true" + required: false + stonith-timeout: + value: ["210", "210s"] + required: false # === Resource Defaults === # cibadmin --query --scope resources RESOURCE_DEFAULTS: SUSE: fence_agent: + required: false instance_attributes: - pcmk_delay_max: "15" - pcmk_monitor_retries: "4" - pcmk_action_limit: "3" - pcmk_reboot_timeout: ["900", "900s"] - power_timeout: ["240", "240s"] + pcmk_delay_max: + value: "15" + required: false + pcmk_monitor_retries: + value: "4" + required: false + pcmk_action_limit: + value: "3" + required: false + pcmk_reboot_timeout: + value: ["900", "900s"] + required: false + power_timeout: + value: ["240", "240s"] + required: false operations: monitor: - interval: ["3600", "3600s"] - timeout: ["120", "120s"] + interval: + value: ["3600", "3600s"] + required: false + timeout: + value: ["120", "120s"] + required: false start: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false sbd_stonith: instance_attributes: - pcmk_delay_max: "15" - pcmk_monitor_retries: "4" - pcmk_action_limit: "3" - pcmk_reboot_timeout: ["900", "900s"] - power_timeout: ["240", "240s"] - pcmk_monitor_timeout: ["120", "120s"] + pcmk_delay_max: + value: "15" + required: false + pcmk_monitor_retries: + value: "4" + required: false + pcmk_action_limit: + value: "3" + required: false + pcmk_reboot_timeout: + value: ["900", "900s"] + required: false + power_timeout: + value: ["240", "240s"] + required: false + pcmk_monitor_timeout: + value: ["120", "120s"] + required: false operations: monitor: - interval: ["600", "600s"] - timeout: ["15", "15s"] + interval: + value: ["600", "600s"] + required: false + timeout: + value: ["15", "15s"] + required: false ascs: instance_attributes: - AUTOMATIC_RECOVER: "false" - MINIMAL_PROBE: "true" + AUTOMATIC_RECOVER: + value: "false" + required: false + MINIMAL_PROBE: + value: "true" + required: false meta_attributes: - resource-stickiness: "5000" - priority: "100" + resource-stickiness: + value: "5000" + required: false + priority: + value: "100" + required: true operations: monitor: - interval: ["11", "11s"] + interval: + value: ["11", "11s"] + required: false timeout: - ANF: ["105", "105s"] - AFS: ["60", "60s"] + ANF: + value: ["105", "105s"] + required: false + AFS: + value: ["60", "60s"] + required: false start: - interval: ["0", "0s"] - timeout: ["180", "180s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["180", "180s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["240", "240s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["240", "240s"] + required: false promote: - interval: ["0", "0s"] - timeout: ["320", "320s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["320", "320s"] + required: false demote: - interval: ["0", "0s"] - timeout: ["320", "320s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["320", "320s"] + required: false ers: instance_attributes: - AUTOMATIC_RECOVER: "false" - MINIMAL_PROBE: "true" - IS_ERS: "true" + AUTOMATIC_RECOVER: + value: "false" + required: false + MINIMAL_PROBE: + value: "true" + required: false + IS_ERS: + value: "true" + required: false meta_attributes: - resource-stickiness: "5000" - priority: "100" + resource-stickiness: + value: "5000" + required: false + priority: + value: "100" + required: false operations: monitor: - interval: ["11", "11s"] + interval: + value: ["11", "11s"] + required: false timeout: - ANF: ["105", "105s"] - AFS: ["60", "60s"] + ANF: + value: ["105", "105s"] + required: false + AFS: + value: ["60", "60s"] + required: false start: - interval: ["0", "0s"] - timeout: ["180", "180s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["180", "180s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["240", "240s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["240", "240s"] + required: false promote: - interval: ["0", "0s"] - timeout: ["320", "320s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["320", "320s"] + required: false demote: - interval: ["0", "0s"] - timeout: ["320", "320s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["320", "320s"] + required: false ipaddr: meta_attributes: - target-role: "Started" + target-role: + value: "Started" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["20", "20s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["20", "20s"] + required: false start: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false azurelb: meta_attributes: - resource-stickiness: "0" + resource-stickiness: + value: "0" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["20", "20s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["20", "20s"] + required: false start: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false azureevents: meta_attributes: - allow-unhealthy-nodes: "true" - failure-timeout: "120s" + allow-unhealthy-nodes: + value: "true" + required: false + failure-timeout: + value: "120s" + required: false operations: monitor: - interval: ["10", "10s"] + interval: + value: ["10", "10s"] + required: false start: - interval: ["0", "0s"] + interval: + value: ["0", "0s"] + required: false REDHAT: fence_agent: instance_attributes: - pcmk_delay_max: "15" - pcmk_monitor_retries: "4" - pcmk_action_limit: "3" - pcmk_reboot_timeout: ["900", "900s"] - power_timeout: ["240", "240s"] + pcmk_delay_max: + value: "15" + required: false + pcmk_monitor_retries: + value: "4" + required: false + pcmk_action_limit: + value: "3" + required: false + pcmk_reboot_timeout: + value: ["900", "900s"] + required: false + power_timeout: + value: ["240", "240s"] + required: false operations: monitor: - interval: "3600" - timeout: ["120", "120s"] + interval: + value: "3600" + required: false + timeout: + value: ["120", "120s"] + required: false start: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false sbd_stonith: instance_attributes: - pcmk_delay_max: "15" - pcmk_monitor_retries: "4" - pcmk_action_limit: "3" - pcmk_reboot_timeout: ["900", "900s"] - power_timeout: ["240", "240s"] - pcmk_monitor_timeout: ["120", "120s"] + pcmk_delay_max: + value: "15" + required: false + pcmk_monitor_retries: + value: "4" + required: false + pcmk_action_limit: + value: "3" + required: false + pcmk_reboot_timeout: + value: ["900", "900s"] + required: false + power_timeout: + value: ["240", "240s"] + required: false + pcmk_monitor_timeout: + value: ["120", "120s"] + required: false operations: monitor: - interval: "600" - timeout: ["15", "15s"] + interval: + value: "600" + required: false + timeout: + value: ["15", "15s"] + required: false ascs: instance_attributes: - AUTOMATIC_RECOVER: "false" - MINIMAL_PROBE: "true" - meta_attributes: - resource-stickiness: "5000" - priority: "10" + AUTOMATIC_RECOVER: + value: "false" + required: false + MINIMAL_PROBE: + value: "true" + required: false operations: - monitor: - interval: ["20", "20s"] - timeout: - ANF: ["105", "105s"] - AFS: ["60", "60s"] start: - interval: ["0", "0s"] - timeout: ["600", "600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["600", "600s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["600", "600s"] - promote: - interval: ["0", "0s"] - timeout: ["320", "320s"] - demote: - interval: ["0", "0s"] - timeout: ["320", "320s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["600", "600s"] + required: false + monitor: + interval: + value: ["20", "20s"] + required: false + timeout: + ANF: + value: ["105", "105s"] + required: false + AFS: + value: ["60", "60s"] + required: false methods: - timeout: ["5", "5s"] - interval: ["0", "0s"] + timeout: + value: ["5", "5s"] + required: false + interval: + value: ["0", "0s"] + required: false reload: - timeout: ["320", "320s"] - interval: ["0", "0s"] + timeout: + value: ["320", "320s"] + required: false + interval: + value: ["0", "0s"] + required: false + promote: + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["320", "320s"] + required: false + demote: + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["320", "320s"] + required: false ers: instance_attributes: - AUTOMATIC_RECOVER: "false" - MINIMAL_PROBE: "true" - IS_ERS: "true" + AUTOMATIC_RECOVER: + value: "false" + required: false + MINIMAL_PROBE: + value: "true" + required: false + IS_ERS: + value: "true" + required: false meta_attributes: - resource-stickiness: "3000" - priority: "100" + resource-stickiness: + value: "3000" + required: false + priority: + value: "100" + required: false operations: monitor: - interval: ["20", "20s"] + interval: + value: ["20", "20s"] + required: false timeout: - ANF: ["105", "105s"] - AFS: ["60", "60s"] + ANF: + value: ["105", "105s"] + required: false + AFS: + value: ["60", "60s"] + required: false start: - interval: ["0", "0s"] - timeout: ["600", "600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["600", "600s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["600", "600s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["600", "600s"] + required: false promote: - interval: ["0", "0s"] - timeout: ["320", "320s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["320", "320s"] + required: false demote: - interval: ["0", "0s"] - timeout: ["320", "320s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["320", "320s"] + required: false methods: - timeout: ["5", "5s"] - interval: ["0", "0s"] + timeout: + value: ["5", "5s"] + required: false + interval: + value: ["0", "0s"] + required: false reload: - timeout: ["320", "320s"] - interval: ["0", "0s"] + timeout: + value: ["320", "320s"] + required: false + interval: + value: ["0", "0s"] + required: false ipaddr: meta_attributes: - target-role: "Started" + target-role: + value: "Started" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["20", "20s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["20", "20s"] + required: false start: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false azurelb: meta_attributes: - resource-stickiness: "0" + resource-stickiness: + value: "0" + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["20", "20s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["20", "20s"] + required: false start: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["20", "20s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["20", "20s"] + required: false azureevents: meta_attributes: - allow-unhealthy-nodes: "true" - failure-timeout: ["120", "120s"] + allow-unhealthy-nodes: + value: "true" + required: false + failure-timeout: + value: ["120", "120s"] + required: false operations: monitor: - interval: ["10", "10s"] - timeout: ["240", "240s"] + interval: + value: ["10", "10s"] + required: false + timeout: + value: ["240", "240s"] + required: false start: - interval: ["0", "0s"] - timeout: ["10", "10s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["10", "10s"] + required: false stop: - interval: ["0", "0s"] - timeout: ["10", "10s"] + interval: + value: ["0", "0s"] + required: false + timeout: + value: ["10", "10s"] + required: false # === OS Parameters === @@ -333,20 +684,38 @@ RESOURCE_DEFAULTS: OS_PARAMETERS: DEFAULTS: sysctl: - net.ipv4.tcp_timestamps: "net.ipv4.tcp_timestamps = 0" - vm.swappiness: "vm.swappiness = 10" + net.ipv4.tcp_timestamps: + value: "net.ipv4.tcp_timestamps = 0" + required: false + vm.swappiness: + value: "vm.swappiness = 10" + required: false corosync-cmapctl: - runtime.config.totem.token: "runtime.config.totem.token (u32) = 30000" - runtime.config.totem.consensus: "runtime.config.totem.consensus (u32) = 36000" + runtime.config.totem.token: + value: "runtime.config.totem.token (u32) = 30000" + required: false + runtime.config.totem.consensus: + value: "runtime.config.totem.consensus (u32) = 36000" + required: false # === Azure Load Balancer === # Azure Load Balancer configuration AZURE_LOADBALANCER: PROBES: - probe_threshold: 2 - interval_in_seconds: 5 + probe_threshold: + value: 2 + required: false + interval_in_seconds: + value: 5 + required: false RULES: - idle_timeout_in_minutes: 30 - enable_floating_ip: true - enable_tcp_reset: false + idle_timeout_in_minutes: + value: 30 + required: false + enable_floating_ip: + value: true + required: false + enable_tcp_reset: + value: false + required: false diff --git a/src/templates/report.html b/src/templates/report.html index b4a77595..c5410a96 100644 --- a/src/templates/report.html +++ b/src/templates/report.html @@ -111,7 +111,7 @@ .pie { --passed: #2a882e; /* Adjusted green to be brighter */ --failed: #df3d3d; /* Kept red but slightly adjusted */ - --warning: #fcd116; + --warning: #d36325; --size: 150px; --thickness: 12px; --progress: 0; @@ -193,7 +193,7 @@ } .test-case.warning .title { - background-color: #fcd116; + background-color: #d36325; color: white; } diff --git a/src/vars/input-api.yaml b/src/vars/input-api.yaml index 41e0d6b0..58e7a73c 100644 --- a/src/vars/input-api.yaml +++ b/src/vars/input-api.yaml @@ -29,7 +29,7 @@ test_groups: including Corosync settings, Pacemaker resources, SBD device configuration, and HANA system replication setup. This test is run in an offline mode where the CIB files are already available in the offline_validation directory. - enabled: false + enabled: true - name: Azure Load Balancer Validation task_name: azure-lb @@ -156,7 +156,7 @@ test_groups: including Corosync settings, Pacemaker resources, SBD device configuration, and SCS system replication setup. This test is run in an offline mode where the CIB files are already available in the offline_validation directory. - enabled: false + enabled: true - name: Azure Load Balancer Validation task_name: azure-lb diff --git a/tests/module_utils/get_pcmk_properties_test.py b/tests/module_utils/get_pcmk_properties_test.py index ea96dcbe..02b28315 100644 --- a/tests/module_utils/get_pcmk_properties_test.py +++ b/tests/module_utils/get_pcmk_properties_test.py @@ -88,53 +88,74 @@ DUMMY_CONSTANTS = { "VALID_CONFIGS": { - "REDHAT": {"stonith-enabled": "true", "cluster-name": "hdb_HDB"}, - "azure-fence-agent": {"priority": "10"}, - "sbd": {"pcmk_delay_max": "30"}, + "REDHAT": { + "stonith-enabled": {"value": "true", "required": False}, + "cluster-name": {"value": "hdb_HDB", "required": False}, + }, + "azure-fence-agent": {"priority": {"value": "10", "required": False}}, + "sbd": {"pcmk_delay_max": {"value": "30", "required": False}}, }, "RSC_DEFAULTS": { - "resource-stickiness": "1000", - "migration-threshold": "5000", + "resource-stickiness": {"value": "1000", "required": False}, + "migration-threshold": {"value": "5000", "required": False}, }, "OP_DEFAULTS": { - "timeout": "600", - "record-pending": "true", + "timeout": {"value": "600", "required": False}, + "record-pending": {"value": "true", "required": False}, }, "CRM_CONFIG_DEFAULTS": { - "stonith-enabled": "true", - "maintenance-mode": "false", + "stonith-enabled": {"value": "true", "required": False}, + "maintenance-mode": {"value": "false", "required": False}, }, "RESOURCE_DEFAULTS": { "REDHAT": { "fence_agent": { - "meta_attributes": {"pcmk_delay_max": "15", "target-role": "Started"}, + "meta_attributes": { + "pcmk_delay_max": {"value": "15", "required": False}, + "target-role": {"value": "Started", "required": False}, + }, "operations": { - "monitor": {"timeout": ["700", "700s"], "interval": "10"}, - "start": {"timeout": "20"}, + "monitor": { + "timeout": {"value": ["700", "700s"], "required": False}, + "interval": {"value": "10", "required": False}, + }, + "start": {"timeout": {"value": "20", "required": False}}, }, - "instance_attributes": {"login": "testuser"}, + "instance_attributes": {"login": {"value": "testuser", "required": False}}, }, "sbd_stonith": { - "meta_attributes": {"pcmk_delay_max": "30", "target-role": "Started"}, + "meta_attributes": { + "pcmk_delay_max": {"value": "30", "required": False}, + "target-role": {"value": "Started", "required": False}, + }, "operations": { - "monitor": {"timeout": ["30", "30s"], "interval": "10"}, - "start": {"timeout": "20"}, + "monitor": { + "timeout": {"value": ["30", "30s"], "required": False}, + "interval": {"value": "10", "required": False}, + }, + "start": {"timeout": {"value": "20", "required": False}}, }, }, "test_resource": { - "meta_attributes": {"clone-max": "2"}, - "operations": {"monitor": {"timeout": ["600", "600s"]}}, - "instance_attributes": {"SID": "HDB"}, + "meta_attributes": {"clone-max": {"value": "2", "required": False}}, + "operations": { + "monitor": {"timeout": {"value": ["600", "600s"], "required": False}} + }, + "instance_attributes": {"SID": {"value": "HDB", "required": False}}, }, } }, "OS_PARAMETERS": { - "DEFAULTS": {"sysctl": {"kernel.numa_balancing": "kernel.numa_balancing = 0"}} + "DEFAULTS": { + "sysctl": { + "kernel.numa_balancing": {"value": "kernel.numa_balancing = 0", "required": False} + } + } }, "CONSTRAINTS": { - "rsc_location": {"score": "INFINITY"}, - "rsc_colocation": {"score": "4000"}, - "rsc_order": {"kind": "Optional"}, + "rsc_location": {"score": {"value": "INFINITY", "required": False}}, + "rsc_colocation": {"score": {"value": "4000", "required": False}}, + "rsc_order": {"kind": {"value": "Optional", "required": False}}, }, } @@ -254,7 +275,7 @@ def test_get_expected_value_fence_config(self, validator): """ validator.fencing_mechanism = "azure-fence-agent" expected = validator._get_expected_value("crm_config", "priority") - assert expected == "10" + assert expected == ("10", False) def test_get_resource_expected_value_instance_attributes(self, validator): """ @@ -263,7 +284,7 @@ def test_get_resource_expected_value_instance_attributes(self, validator): expected = validator._get_resource_expected_value( "fence_agent", "instance_attributes", "login" ) - assert expected == "testuser" + assert expected == ("testuser", False) def test_get_resource_expected_value_invalid_section(self, validator): """ @@ -307,30 +328,16 @@ def test_determine_parameter_status_success_string(self, validator): """ Test _determine_parameter_status method with matching string values. """ - status = validator._determine_parameter_status("true", "true") + status = validator._determine_parameter_status("true", ("true", False)) assert status == TestStatus.SUCCESS.value def test_determine_parameter_status_error_string(self, validator): """ Test _determine_parameter_status method with non-matching string values. """ - status = validator._determine_parameter_status("true", "false") + status = validator._determine_parameter_status("true", ("false", False)) assert status == TestStatus.ERROR.value - def test_parse_basic_config(self, validator): - """ - Test _parse_basic_config method. - """ - xml_str = """ - - - """ - params = validator._parse_basic_config( - ET.fromstring(xml_str), "crm_config", "test_subcategory" - ) - assert len(params) == 2 - assert params[0]["category"] == "crm_config_test_subcategory" - def test_parse_resource_with_operations(self, validator): """ Test _parse_resource method with operations. @@ -347,25 +354,6 @@ def test_parse_resource_with_operations(self, validator): assert len(timeout_params) == 2 assert len(interval_params) == 2 - def test_parse_constraints(self, validator): - """ - Test _parse_constraints method. - """ - xml_str = """ - - - - - """ - root = ET.fromstring(xml_str) - params = validator._parse_constraints(root) - location_params = [p for p in params if "rsc_location" in p["category"]] - colocation_params = [p for p in params if "rsc_colocation" in p["category"]] - order_params = [p for p in params if "rsc_order" in p["category"]] - assert len(location_params) >= 1 - assert len(colocation_params) >= 1 - assert len(order_params) >= 1 - def test_parse_resources_section(self, validator): """ Test _parse_resources_section method. @@ -410,15 +398,6 @@ def test_get_scope_from_cib_without_cib_output(self, validator): scope_element = validator._get_scope_from_cib("resources") assert scope_element is None - def test_parse_ha_cluster_config_with_cib(self, validator_with_cib): - """ - Test parse_ha_cluster_config method with CIB output. - """ - validator_with_cib.parse_ha_cluster_config() - result = validator_with_cib.get_result() - assert result["status"] in [TestStatus.SUCCESS.value, TestStatus.ERROR.value] - assert "parameters" in result["details"] - def test_get_expected_value_for_category_resource(self, validator): """ Test _get_expected_value_for_category method for resource category. @@ -426,7 +405,7 @@ def test_get_expected_value_for_category_resource(self, validator): expected = validator._get_expected_value_for_category( "fence_agent", "meta_attributes", "pcmk_delay_max", None ) - assert expected == "15" + assert expected == ("15", False) def test_get_expected_value_for_category_basic(self, validator): """ @@ -435,26 +414,14 @@ def test_get_expected_value_for_category_basic(self, validator): expected = validator._get_expected_value_for_category( "crm_config", None, "stonith-enabled", None ) - assert expected == "true" + assert expected == ("true", False) - def test_determine_parameter_status_error_invalid_expected(self, validator): + def test_determine_parameter_status_with_required_parameter(self, validator): """ - Test _determine_parameter_status method with invalid expected value type. + Test _determine_parameter_status method with required parameter. """ - status = validator._determine_parameter_status("value", {"invalid": "dict"}) - assert status == TestStatus.ERROR.value - - def test_parse_constraints_skip_missing_attributes(self, validator): - """ - Test _parse_constraints method skips elements with missing attributes. - """ - xml_str = """ - - """ - root = ET.fromstring(xml_str) - params = validator._parse_constraints(root) - score_params = [p for p in params if p["name"] == "score"] - assert len(score_params) == 0 + status = validator._determine_parameter_status("", ("expected_value", True)) + assert status == TestStatus.WARNING.value def test_get_scope_from_cib_invalid_scope(self, validator_with_cib): """ diff --git a/tests/modules/get_pcmk_properties_db_test.py b/tests/modules/get_pcmk_properties_db_test.py index 135b7b7b..bdd75f7e 100644 --- a/tests/modules/get_pcmk_properties_db_test.py +++ b/tests/modules/get_pcmk_properties_db_test.py @@ -146,47 +146,90 @@ DUMMY_CONSTANTS = { "VALID_CONFIGS": { - "REDHAT": {"stonith-enabled": "true"}, - "azure-fence-agent": {"priority": "10"}, + "REDHAT": { + "stonith-enabled": {"value": "true", "required": False}, + "cluster-name": {"value": "hdb_HDB", "required": False}, + }, + "azure-fence-agent": {"priority": {"value": "10", "required": False}}, + "sbd": {"pcmk_delay_max": {"value": "30", "required": False}}, }, "RSC_DEFAULTS": { - "resource-stickiness": "1000", - "migration-threshold": "5000", + "resource-stickiness": {"value": "1000", "required": False}, + "migration-threshold": {"value": "5000", "required": False}, }, "OP_DEFAULTS": { - "timeout": "600", - "record-pending": "true", + "timeout": {"value": "600", "required": False}, + "record-pending": {"value": "true", "required": False}, + }, + "CRM_CONFIG_DEFAULTS": { + "stonith-enabled": {"value": "true", "required": False}, + "maintenance-mode": {"value": "false", "required": False}, }, - "CRM_CONFIG_DEFAULTS": {"stonith-enabled": "true"}, "RESOURCE_DEFAULTS": { "REDHAT": { "fence_agent": { - "meta_attributes": {"pcmk_delay_max": "15"}, - "operations": {"monitor": {"timeout": ["700", "700s"]}}, + "meta_attributes": { + "pcmk_delay_max": {"value": "15", "required": False}, + "target-role": {"value": "Started", "required": False}, + }, + "operations": { + "monitor": { + "timeout": {"value": ["700", "700s"], "required": False}, + "interval": {"value": "10", "required": False}, + }, + "start": {"timeout": {"value": "20", "required": False}}, + }, + "instance_attributes": {"login": {"value": "testuser", "required": False}}, }, "sbd_stonith": { - "meta_attributes": {"pcmk_delay_max": "15"}, - "operations": {"monitor": {"timeout": ["30", "30s"]}}, + "meta_attributes": { + "pcmk_delay_max": {"value": "30", "required": False}, + "target-role": {"value": "Started", "required": False}, + }, + "operations": { + "monitor": { + "timeout": {"value": ["30", "30s"], "required": False}, + "interval": {"value": "10", "required": False}, + }, + "start": {"timeout": {"value": "20", "required": False}}, + }, + }, + "hana": { + "meta_attributes": {"clone-max": {"value": "2", "required": False}}, + "operations": { + "monitor": {"timeout": {"value": ["600", "600s"], "required": False}} + }, + "instance_attributes": {"SID": {"value": "HDB", "required": False}}, }, - "hana": {"meta_attributes": {"clone-max": "2"}}, } }, "OS_PARAMETERS": { - "DEFAULTS": {"sysctl": {"kernel.numa_balancing": "kernel.numa_balancing = 0"}} + "DEFAULTS": { + "sysctl": { + "kernel.numa_balancing": {"value": "kernel.numa_balancing = 0", "required": False} + } + } }, "GLOBAL_INI": { "REDHAT": { "SAPHanaSR": { - "provider": "SAPHanaSR", - "path": "/usr/share/SAPHanaSR", - "execution_order": ["1", "2"], + "provider": {"value": "SAPHanaSR", "required": False}, + "path": {"value": "/usr/share/SAPHanaSR", "required": False}, + "execution_order": {"value": ["1", "2"], "required": False}, } }, "SUSE": { - "SAPHanaSR-angi": {"provider": "SAPHanaSR-angi", "path": "/usr/share/SAPHanaSR-angi"} + "SAPHanaSR-angi": { + "provider": {"value": "SAPHanaSR-angi", "required": False}, + "path": {"value": "/usr/share/SAPHanaSR-angi", "required": False}, + } }, }, - "CONSTRAINTS": {"rsc_location": {"score": "INFINITY"}}, + "CONSTRAINTS": { + "rsc_location": {"score": {"value": "INFINITY", "required": False}}, + "rsc_colocation": {"score": {"value": "4000", "required": False}}, + "rsc_order": {"kind": {"value": "Optional", "required": False}}, + }, } @@ -552,26 +595,13 @@ def test_get_expected_value_methods(self, validator): """ validator.fencing_mechanism = "azure-fence-agent" expected = validator._get_expected_value("crm_config", "priority") - assert expected == "10" + assert expected == ("10", False) expected = validator._get_expected_value("crm_config", "stonith-enabled") - assert expected == "true" + assert expected == ("true", False) expected = validator._get_resource_expected_value( "fence_agent", "meta_attributes", "pcmk_delay_max" ) - assert expected == "15" - - def test_parse_constraints_with_valid_constraints(self, validator): - """ - Test _parse_constraints method with valid constraints. - """ - xml_str = """ - - - - """ - root = ET.fromstring(xml_str) - params = validator._parse_constraints(root) - assert len(params) > 0 + assert expected == ("15", False) def test_successful_validation_result(self, validator): """ diff --git a/tests/modules/get_pcmk_properties_scs_test.py b/tests/modules/get_pcmk_properties_scs_test.py index a2f542c9..51bac43e 100644 --- a/tests/modules/get_pcmk_properties_scs_test.py +++ b/tests/modules/get_pcmk_properties_scs_test.py @@ -125,61 +125,96 @@ DUMMY_CONSTANTS = { "VALID_CONFIGS": { - "REDHAT": {"stonith-enabled": "true", "cluster-name": "scs_S4D"}, - "azure-fence-agent": {"priority": "10"}, - "sbd": {"pcmk_delay_max": "30"}, + "REDHAT": { + "stonith-enabled": {"value": "true", "required": False}, + "cluster-name": {"value": "scs_S4D", "required": False}, + }, + "azure-fence-agent": {"priority": {"value": "10", "required": False}}, + "sbd": {"pcmk_delay_max": {"value": "30", "required": False}}, }, "RSC_DEFAULTS": { - "resource-stickiness": "1000", - "migration-threshold": "5000", + "resource-stickiness": {"value": "1000", "required": False}, + "migration-threshold": {"value": "5000", "required": False}, }, "OP_DEFAULTS": { - "timeout": "600", - "record-pending": "true", + "timeout": {"value": "600", "required": False}, + "record-pending": {"value": "true", "required": False}, }, "CRM_CONFIG_DEFAULTS": { - "stonith-enabled": "true", - "maintenance-mode": "false", + "stonith-enabled": {"value": "true", "required": False}, + "maintenance-mode": {"value": "false", "required": False}, }, "RESOURCE_DEFAULTS": { "REDHAT": { "fence_agent": { - "meta_attributes": {"pcmk_delay_max": "15", "target-role": "Started"}, + "meta_attributes": { + "pcmk_delay_max": {"value": "15", "required": False}, + "target-role": {"value": "Started", "required": False}, + }, "operations": { - "monitor": {"timeout": ["700", "700s"], "interval": "10"}, - "start": {"timeout": "20"}, + "monitor": { + "timeout": {"value": ["700", "700s"], "required": False}, + "interval": {"value": "10", "required": False}, + }, + "start": {"timeout": {"value": "20", "required": False}}, + }, + "instance_attributes": { + "login": {"value": "testuser", "required": False}, + "resourceGroup": {"value": "test-rg", "required": False}, }, - "instance_attributes": {"login": "testuser", "resourceGroup": "test-rg"}, }, "sbd_stonith": { - "meta_attributes": {"pcmk_delay_max": "30", "target-role": "Started"}, + "meta_attributes": { + "pcmk_delay_max": {"value": "30", "required": False}, + "target-role": {"value": "Started", "required": False}, + }, "operations": { - "monitor": {"timeout": ["30", "30s"], "interval": "10"}, - "start": {"timeout": "20"}, + "monitor": { + "timeout": {"value": ["30", "30s"], "required": False}, + "interval": {"value": "10", "required": False}, + }, + "start": {"timeout": {"value": "20", "required": False}}, }, }, "ascs": { - "meta_attributes": {"target-role": "Started"}, - "operations": {"monitor": {"timeout": ["600", "600s"]}}, - "instance_attributes": {"InstanceName": "S4D_ASCS00_sapascs"}, + "meta_attributes": {"target-role": {"value": "Started", "required": False}}, + "operations": { + "monitor": {"timeout": {"value": ["600", "600s"], "required": False}} + }, + "instance_attributes": { + "InstanceName": {"value": "S4D_ASCS00_sapascs", "required": False} + }, }, "ers": { - "meta_attributes": {"target-role": "Started"}, - "operations": {"monitor": {"timeout": ["600", "600s"]}}, - "instance_attributes": {"InstanceName": "S4D_ERS10_sapers"}, + "meta_attributes": {"target-role": {"value": "Started", "required": False}}, + "operations": { + "monitor": {"timeout": {"value": ["600", "600s"], "required": False}} + }, + "instance_attributes": { + "InstanceName": {"value": "S4D_ERS10_sapers", "required": False} + }, }, "ipaddr": { - "instance_attributes": {"ip": {"AFS": ["10.0.1.100"], "ANF": ["10.0.1.101"]}} + "instance_attributes": { + "ip": { + "value": {"AFS": ["10.0.1.100"], "ANF": ["10.0.1.101"]}, + "required": False, + } + } }, } }, "OS_PARAMETERS": { - "DEFAULTS": {"sysctl": {"kernel.numa_balancing": "kernel.numa_balancing = 0"}} + "DEFAULTS": { + "sysctl": { + "kernel.numa_balancing": {"value": "kernel.numa_balancing = 0", "required": False} + } + } }, "CONSTRAINTS": { - "rsc_location": {"score": "INFINITY"}, - "rsc_colocation": {"score": "4000"}, - "rsc_order": {"kind": "Optional"}, + "rsc_location": {"score": {"value": "INFINITY", "required": False}}, + "rsc_colocation": {"score": {"value": "4000", "required": False}}, + "rsc_order": {"kind": {"value": "Optional", "required": False}}, }, } @@ -303,7 +338,7 @@ def test_get_expected_value_for_category_resource(self, validator): expected = validator._get_expected_value_for_category( "fence_agent", "meta_attributes", "pcmk_delay_max", None ) - assert expected == "15" + assert expected == ("15", False) def test_get_expected_value_for_category_ascs_ers(self, validator): """ @@ -312,11 +347,11 @@ def test_get_expected_value_for_category_ascs_ers(self, validator): expected = validator._get_expected_value_for_category( "ascs", "meta_attributes", "target-role", None ) - assert expected == "Started" + assert expected == ("Started", False) expected = validator._get_expected_value_for_category( "ers", "meta_attributes", "target-role", None ) - assert expected == "Started" + assert expected == ("Started", False) def test_get_expected_value_for_category_basic(self, validator): """ @@ -325,15 +360,16 @@ def test_get_expected_value_for_category_basic(self, validator): expected = validator._get_expected_value_for_category( "crm_config", None, "stonith-enabled", None ) - assert expected == "true" + assert expected == ("true", False) - def test_determine_parameter_status_with_dict_expected_value_anf(self, validator_anf): + def test_determine_parameter_status_with_list_expected_value(self, validator): """ - Test _determine_parameter_status method with dict expected value and ANF provider. + Test _determine_parameter_status method with list expected value. """ - status = validator_anf._determine_parameter_status( - "10.0.1.101", {"AFS": ["10.0.1.100"], "ANF": ["10.0.1.101"]} + status = validator._determine_parameter_status( + "10.0.1.101", (["10.0.1.100", "10.0.1.101"], False) ) + print(f"Actual status: {status}, Expected: {TestStatus.SUCCESS.value}") assert status == TestStatus.SUCCESS.value def test_determine_parameter_status_info_cases(self, validator): @@ -465,25 +501,6 @@ def test_resource_categories_defined(self, validator): assert category in HAClusterValidator.RESOURCE_CATEGORIES assert HAClusterValidator.RESOURCE_CATEGORIES[category].startswith(".//") - def test_parse_constraints_with_location_constraints(self, validator): - """ - Test _parse_constraints method with location constraints. - """ - xml_str = """ - - - - - """ - root = ET.fromstring(xml_str) - params = validator._parse_constraints(root) - location_params = [p for p in params if "rsc_location" in p["category"]] - colocation_params = [p for p in params if "rsc_colocation" in p["category"]] - order_params = [p for p in params if "rsc_order" in p["category"]] - assert len(location_params) >= 1 - assert len(colocation_params) >= 1 - assert len(order_params) >= 1 - def test_successful_validation_result(self, validator): """ Test that validator returns proper result structure. @@ -518,20 +535,20 @@ def test_get_expected_value_methods_coverage(self, validator): """ validator.fencing_mechanism = "azure-fence-agent" expected = validator._get_expected_value("crm_config", "priority") - assert expected == "10" + assert expected == ("10", False) expected = validator._get_expected_value("crm_config", "stonith-enabled") - assert expected == "true" + assert expected == ("true", False) expected = validator._get_resource_expected_value( "fence_agent", "meta_attributes", "pcmk_delay_max" ) - assert expected == "15" + assert expected == ("15", False) expected = validator._get_resource_expected_value( "fence_agent", "operations", "timeout", "monitor" ) - assert expected == ["700", "700s"] + assert expected == (["700", "700s"], False) expected = validator._get_resource_expected_value( "fence_agent", "instance_attributes", "login" ) - assert expected == "testuser" + assert expected == ("testuser", False) expected = validator._get_resource_expected_value("fence_agent", "unknown_section", "param") assert expected is None