diff --git a/keywords/cloud_platform/health/health_keywords.py b/keywords/cloud_platform/health/health_keywords.py index 6f588402..d33e7d77 100644 --- a/keywords/cloud_platform/health/health_keywords.py +++ b/keywords/cloud_platform/health/health_keywords.py @@ -34,10 +34,17 @@ class HealthKeywords(BaseKeyword): # Validate all apps are healthy and applied self.validate_apps_health_and_applied() - def validate_pods_health(self): - """Function to validate the health of all pods in the cluster""" + def validate_pods_health(self, namespace: str | None = None) -> bool: + """Function to validate the health of all pods in the cluster + + Args: + namespace (str | None): Namespace to check the pods in. If None, the default namespace will be used. + + Returns: + bool: True if pod is in expected status else False. + """ healthy_status = ["Running", "Succeeded", "Completed"] - KubectlGetPodsKeywords(self.ssh_connection).wait_for_all_pods_status(healthy_status, timeout=300) + return KubectlGetPodsKeywords(self.ssh_connection).wait_for_pods_to_reach_status(expected_status=healthy_status, namespace=namespace, timeout=300) def validate_no_alarms(self): """Function to validate no alarms are present in the cluster""" diff --git a/keywords/cloud_platform/system/helm/objects/system_helm_override_object.py b/keywords/cloud_platform/system/helm/objects/system_helm_override_object.py index 5d253c14..d71c4810 100644 --- a/keywords/cloud_platform/system/helm/objects/system_helm_override_object.py +++ b/keywords/cloud_platform/system/helm/objects/system_helm_override_object.py @@ -11,7 +11,6 @@ class HelmOverrideObject: """ self.name: str = None self.namespace: str = None - self.user_overrides: str = None def set_name(self, name: str): """ @@ -48,21 +47,3 @@ class HelmOverrideObject: str: The Kubernetes namespace. """ return self.namespace - - def set_user_overrides(self, user_overrides: str): - """ - Set the user-defined overrides. - - Args: - user_overrides (str): string of user overrides. - """ - self.user_overrides = user_overrides - - def get_user_overrides(self) -> str: - """ - Get the user-defined overrides. - - Returns: - str: string of user overrides. - """ - return self.user_overrides diff --git a/keywords/cloud_platform/system/helm/objects/system_helm_override_output.py b/keywords/cloud_platform/system/helm/objects/system_helm_override_output.py index 94031fc7..2713aec7 100644 --- a/keywords/cloud_platform/system/helm/objects/system_helm_override_output.py +++ b/keywords/cloud_platform/system/helm/objects/system_helm_override_output.py @@ -26,7 +26,6 @@ class SystemHelmOverrideOutput: self.helm_override = HelmOverrideObject() self.helm_override.set_name(output_values["name"]) self.helm_override.set_namespace(output_values["namespace"]) - self.helm_override.set_user_overrides(output_values["user_overrides"]) else: raise KeywordException(f"The output line {output_values} was not valid") @@ -50,7 +49,7 @@ class SystemHelmOverrideOutput: Returns: bool: True if the output contains all required fields, False otherwise. """ - required_fields = ["name", "namespace", "user_overrides"] + required_fields = ["name", "namespace"] valid = True for field in required_fields: if field not in value: diff --git a/keywords/cloud_platform/system/helm/objects/system_helm_override_show_object.py b/keywords/cloud_platform/system/helm/objects/system_helm_override_show_object.py index cb366a17..09d86dfd 100644 --- a/keywords/cloud_platform/system/helm/objects/system_helm_override_show_object.py +++ b/keywords/cloud_platform/system/helm/objects/system_helm_override_show_object.py @@ -12,7 +12,7 @@ class HelmOverrideShowObject: self.attributes: dict = None self.combined_overrides: dict = None self.system_overrides: dict = None - self.user_overrides: dict = None + self.user_overrides: str = None def set_name(self, name: str): """ @@ -113,11 +113,11 @@ class HelmOverrideShowObject: """ self.user_overrides = user_overrides - def get_user_overrides(self) -> dict: + def get_user_overrides(self) -> str: """ Gets the user-defined overrides for the chart. Returns: - dict: User override values. + str: User override values. """ return self.user_overrides diff --git a/keywords/cloud_platform/system/helm/system_helm_override_keywords.py b/keywords/cloud_platform/system/helm/system_helm_override_keywords.py index 844bc211..663e8c9d 100644 --- a/keywords/cloud_platform/system/helm/system_helm_override_keywords.py +++ b/keywords/cloud_platform/system/helm/system_helm_override_keywords.py @@ -39,6 +39,25 @@ class SystemHelmOverrideKeywords(BaseKeyword): system_helm_override_output = SystemHelmOverrideOutput(output) return system_helm_override_output + def update_helm_override_via_set(self, override_values: str, app_name: str, chart_name: str, namespace: str) -> SystemHelmOverrideOutput: + """ + Update the helm-override values via --set parameter + + Args: + override_values (str): override values + app_name (str): the app name + chart_name (str): the chart name + namespace (str): the namespace + + Returns: + SystemHelmOverrideOutput: object with the list of helm overrides. + """ + command = source_openrc(f"system helm-override-update --set {override_values} {app_name} {chart_name} {namespace}") + output = self.ssh_connection.send(command) + self.validate_success_return_code(self.ssh_connection) + system_helm_override_output = SystemHelmOverrideOutput(output) + return system_helm_override_output + def get_system_helm_override_show(self, app_name: str, chart_name: str, namespace: str) -> SystemHelmOverrideShowOutput: """ Gets the system helm-override show @@ -70,3 +89,31 @@ class SystemHelmOverrideKeywords(BaseKeyword): user_override = self.get_system_helm_override_show(app_name, chart_name, namespace).get_helm_override_show().get_user_overrides() validate_str_contains(user_override, label, "User Override") + + def get_system_helm_override_list(self, app_name: str) -> SystemHelmOverrideOutput: + """ + Gets the system helm-override show + + Args: + app_name (str): the app name + + Returns: + SystemHelmOverrideOutput: The parsed helm override list object. + """ + command = source_openrc(f"system helm-override-list {app_name} ") + output = self.ssh_connection.send(command) + self.validate_success_return_code(self.ssh_connection) + system_helm_override_show_output = SystemHelmOverrideOutput(output) + return system_helm_override_show_output + + def delete_system_helm_override(self, app_name: str, chart_name: str, namespace: str) -> None: + """ + Gets the system helm-override show + + Args: + app_name (str): the app name + chart_name (str): the chart name + namespace (str): the namespace + """ + self.ssh_connection.send(source_openrc(f"system helm-override-delete {app_name} {chart_name} {namespace}")) + self.validate_success_return_code(self.ssh_connection) diff --git a/keywords/cloud_platform/system/system_vertical_table_parser.py b/keywords/cloud_platform/system/system_vertical_table_parser.py index 0874e4a0..6ef99e57 100644 --- a/keywords/cloud_platform/system/system_vertical_table_parser.py +++ b/keywords/cloud_platform/system/system_vertical_table_parser.py @@ -5,7 +5,8 @@ from framework.exceptions.keyword_exception import KeywordException class SystemVerticalTableParser: """ - Class for System vertical table parsing + Class for System vertical table parsing. + In a vertical table, attributes and their corresponding values are arranged in adjacent columns, with each attribute listed alongside its value. @@ -60,11 +61,12 @@ class SystemVerticalTableParser: _MIN_ROWS = 5 - def __init__(self, system_output): + def __init__(self, system_output: list[str] | str): """ - Constructor + Constructor. + Args: - system_output (list[str]): a list of strings representing the output of a 'system <*>' command. + system_output (list[str] | str): a list of strings or a string representing the output of a 'system <*>' command. """ self.system_output = system_output @@ -72,12 +74,13 @@ class SystemVerticalTableParser: self, ): """ - Getter for output values dict + Getter for output values dict. + Returns: the output values dict """ # Regex to validate the pattern "+---+---+", with varying dashes - pattern = r'^\+\-+\+\-+\+$' + pattern = r"^\+\-+\+\-+\+$" # Get the total number of rows. total_rows = len(self.system_output) @@ -99,7 +102,7 @@ class SystemVerticalTableParser: second_last_row_valid = re.match(pattern, self.system_output[total_rows - 2]) # Check if there is some text in the last different from the pattern. - has_text_in_last_row = True if not last_row_valid and self.system_output[total_rows - 1].strip() != '' else False + has_text_in_last_row = True if not last_row_valid and self.system_output[total_rows - 1].strip() != "" else False # There are cases in which the last row is a message that is not part of the table. # Refer to the header comment of this class for an example. @@ -125,19 +128,19 @@ class SystemVerticalTableParser: data_row_number = 0 previous_key = None for row in self.system_output[3:last_row]: # Ignore the first three rows and the last row. + # Detect regex syntax patterns - regex_indicators = [ - 'regexp:', 'regex:', # Explicit regex keywords - r'\^/', # Start anchor with path - r'\$\|/', # End anchor with pipe - r'\([^)]*\|[^)]*\)' # Parentheses containing pipes - ] + regex_indicators = ["regexp:", "regex:", r"\^/", r"\$\|/", r"\([^)]*\|[^)]*\)"] # Explicit regex keywords # Start anchor with path # End anchor with pipe # Parentheses containing pipes + + if "!!binary" in row: + row = row.replace("!!binary |", "") has_regex = any(re.search(pattern, str(row)) for pattern in regex_indicators) - if not has_regex and str(row).count('|') != 3: + if not has_regex and str(row).count("|") != 3: raise KeywordException("It is expected that a table have exactly two columns.") - parts = row.split('|') + parts = row.split("|") + if len(parts) > 2: key = parts[1].strip() value = parts[2].strip() @@ -148,7 +151,7 @@ class SystemVerticalTableParser: # This 'else' block handles cases where the property name spans multiple lines, ensuring proper parsing. if data_row_number == 0: raise KeywordException("The property name in the first data row cannot be empty.") - output_values_dict[previous_key] = output_values_dict[previous_key] + ' ' + value + output_values_dict[previous_key] = output_values_dict[previous_key] + " " + value data_row_number += 1 return output_values_dict diff --git a/keywords/k8s/pods/kubectl_get_pods_keywords.py b/keywords/k8s/pods/kubectl_get_pods_keywords.py index 3c9821b7..d5a4e8d0 100644 --- a/keywords/k8s/pods/kubectl_get_pods_keywords.py +++ b/keywords/k8s/pods/kubectl_get_pods_keywords.py @@ -152,12 +152,13 @@ class KubectlGetPodsKeywords(BaseKeyword): return False - def wait_for_all_pods_status(self, expected_statuses: [str], timeout: int = 600) -> bool: + def wait_for_all_pods_status(self, expected_statuses: list[str], timeout: int = 600) -> bool: """ Wait for all pods to be in the given status(s) Args: - expected_statuses ([str]): list of expected statuses ex. ['Completed', 'Running'] + + expected_statuses (list[str]): list of expected statuses ex. ['Completed' , 'Running'] timeout (int): the amount of time in seconds to wait Returns: @@ -174,12 +175,12 @@ class KubectlGetPodsKeywords(BaseKeyword): raise KeywordException("All pods are not in the expected status") - def wait_for_pods_to_reach_status(self, expected_status: str, pod_names: list = None, namespace: str = None, poll_interval: int = 5, timeout: int = 180) -> bool: + def wait_for_pods_to_reach_status(self, expected_status: str | list, pod_names: list = None, namespace: str = None, poll_interval: int = 5, timeout: int = 180) -> bool: """ Wait timeout amount of time for the given pod in a namespace to be in the given status. Args: - expected_status (str): the expected status. + expected_status (str | list): the expected status. pod_names (list): the pod names to look for. If left as None, we will check for all the pods. namespace (str): the namespace. poll_interval (int): the interval in seconds to poll for status. @@ -190,6 +191,11 @@ class KubectlGetPodsKeywords(BaseKeyword): """ pod_status_timeout = time.time() + timeout + if isinstance(expected_status, str): + expected_statuses = [expected_status] + else: + expected_statuses = expected_status + while time.time() < pod_status_timeout: pods = self.get_pods(namespace).get_pods() @@ -198,11 +204,12 @@ class KubectlGetPodsKeywords(BaseKeyword): if pod_names: pods = [pod for pod in pods if pod.get_name() in pod_names] - pods_in_incorrect_status = [pod for pod in pods if pod.get_status() != expected_status] - + pods_in_incorrect_status = list(filter(lambda pod: pod.get_status() not in expected_statuses, pods)) if len(pods_in_incorrect_status) == 0: return True + time.sleep(poll_interval) + pods_in_incorrect_status_names = [pod.get_name() for pod in pods_in_incorrect_status] pods_in_incorrect_status_names = ", ".join(pods_in_incorrect_status_names) raise KeywordException(f"Pods {pods_in_incorrect_status_names} in namespace {namespace} did not reach status {expected_status} within {timeout} seconds") diff --git a/testcases/cloud_platform/regression/storage/test_ceph_rook_operations.py b/testcases/cloud_platform/regression/storage/test_ceph_rook_operations.py index 511f1b2a..50bd1a35 100644 --- a/testcases/cloud_platform/regression/storage/test_ceph_rook_operations.py +++ b/testcases/cloud_platform/regression/storage/test_ceph_rook_operations.py @@ -5,9 +5,11 @@ from framework.logging.automation_logger import get_logger from framework.validation.validation import validate_equals, validate_equals_with_retry, validate_str_contains from keywords.ceph.ceph_osd_pool_ls_detail_keywords import CephOsdPoolLsDetailKeywords from keywords.ceph.ceph_status_keywords import CephStatusKeywords +from keywords.cloud_platform.health.health_keywords import HealthKeywords from keywords.cloud_platform.ssh.lab_connection_keywords import LabConnectionKeywords from keywords.cloud_platform.system.application.system_application_apply_keywords import SystemApplicationApplyKeywords from keywords.cloud_platform.system.application.system_application_list_keywords import SystemApplicationListKeywords +from keywords.cloud_platform.system.helm.system_helm_override_keywords import SystemHelmOverrideKeywords from keywords.cloud_platform.system.host.system_host_fs_keywords import SystemHostFSKeywords from keywords.cloud_platform.system.host.system_host_list_keywords import SystemHostListKeywords from keywords.cloud_platform.system.host.system_host_lock_keywords import SystemHostLockKeywords @@ -762,3 +764,57 @@ def test_monitor_operations_rook_ceph(): get_logger().log_test_case_step("Verify final rook-ceph health.") ceph_status_keywords.wait_for_ceph_health_status(expect_health_status=True) + + +@mark.p2 +@mark.lab_rook_ceph +def test_helm_overrides_operations(): + """ + Test case: test helm overrides operations + + Test Steps: + - Check the storage backend is rook-ceph + - Check the healthy before start the update + - Update the helm overrides + - Validate the update of helm override + - Delete the helm overrides + - Check the healthy after delete the update + + Args: None + """ + + ssh_connection = LabConnectionKeywords().get_active_controller_ssh() + system_storage_backend_keywords = SystemStorageBackendKeywords(ssh_connection) + ceph_status_keywords = CephStatusKeywords(ssh_connection) + health_keywords = HealthKeywords(ssh_connection) + system_helm_override_keywords = SystemHelmOverrideKeywords(ssh_connection) + + storage_backend = "ceph-rook" + chart_name = "rook-ceph-cluster" + namespace = "rook-ceph" + app_name = namespace + + ceph_block_pools = "cephBlockPools.0.spec.replicated.size=3" + + get_logger().log_test_case_step("Check the storage backend is rook-ceph") + validate_equals(system_storage_backend_keywords.get_system_storage_backend_list().is_backend_configured(storage_backend), True, "Checking storage backend") + + get_logger().log_test_case_step("Check the healthy before start the update") + health_keywords.validate_pods_health(namespace) + ceph_status_keywords.wait_for_ceph_health_status(expect_health_status=True) + + get_logger().log_test_case_step("Update the helm overrides") + system_helm_override_keywords.update_helm_override_via_set(ceph_block_pools, app_name, chart_name, namespace) + + get_logger().log_test_case_step("Validate the update of helm override") + user_override = system_helm_override_keywords.get_system_helm_override_show(app_name, chart_name, namespace).get_helm_override_show().get_user_overrides() + validate_str_contains(user_override, 'size: "3"', "Validate if the helm override was updated") + + get_logger().log_test_case_step("Delete the helm overrides") + system_helm_override_keywords.delete_system_helm_override(app_name, chart_name, namespace) + user_override = system_helm_override_keywords.get_system_helm_override_show(app_name, chart_name, namespace).get_helm_override_show().get_user_overrides() + validate_equals(user_override, "None", "Validate if the helm override was deleted") + + get_logger().log_test_case_step("Check the healthy after delete the update") + health_keywords.validate_pods_health(namespace) + ceph_status_keywords.wait_for_ceph_health_status(expect_health_status=True) diff --git a/unit_tests/parser/system_parser_test.py b/unit_tests/parser/system_parser_test.py index d7452d1a..0c2e91f3 100644 --- a/unit_tests/parser/system_parser_test.py +++ b/unit_tests/parser/system_parser_test.py @@ -196,6 +196,19 @@ system_host_if_ptp_remove_wrapped_output = [ "| | | | |\n", "+--------------------------------------+---------+-----------+---------------+\n", ] +system_helm_override_show_output = [ + "+--------------------+--------------------------------------------------------------+\n", + "| Property | Value |\n", + "+--------------------+--------------------------------------------------------------+\n", + "| attributes | enabled: true |\n", + "| | mon_hosts: |\n", + "| | - !!binary | |\n", + "| | Y29udHJvbGxlci0w |\n", + "| | - !!binary | |\n", + "| | Y29udHJvbGxlci0x |\n", + "+--------------------+--------------------------------------------------------------+\n", +] + system_helm_overrides_with_regex = [ "+----------------+------------------------------------------------------------------------------------+\n", @@ -607,7 +620,6 @@ def test_system_vertical_table_parser_with_valid_table_with_a_text_in_the_end(): assert output_dict.get("status") == "removing" assert output_dict.get("updated_at") == "2024-10-16T15:50:59.902779+00:00" - def test_system_helm_overrides_with_regex(): """ Tests the system vertical parser with regex values. @@ -621,3 +633,13 @@ def test_system_helm_overrides_with_regex(): assert "^/(sys" in output_dict["user_overrides"] assert "system.network.name" in output_dict["user_overrides"] assert "^(docker0" in output_dict["user_overrides"] + +def test_system_vertical_table_parser_handles_binary_lines(): + """ + Tests the system vertical table parser with lines containing '!!binary'. + """ + system_vertical_table_parser = SystemVerticalTableParser(system_helm_override_show_output) + output_dict = system_vertical_table_parser.get_output_values_dict() + + assert "attributes" in output_dict + assert "!!binary" not in output_dict["attributes"]