From 567bb957d7696eb6d9d2b88a6c19d370a8d2e032 Mon Sep 17 00:00:00 2001 From: Sandeep Kapur Date: Mon, 28 Apr 2025 16:22:17 -0400 Subject: [PATCH] Finalize Install, Uninstall, Version retrieval of openstack application Change-Id: Iccdc2128b48ed752b0ca12ea9887b81cfbbc1509 Signed-off-by: Sandeep Kapur --- .../configuration_file_locations_manager.py | 36 ++++++++++ config/configuration_manager.py | 22 ++++++- config/openstack/files/default.json5 | 19 ++++++ config/openstack/objects/custom_installer.py | 41 ++++++++++++ config/openstack/objects/openstack_config.py | 61 +++++++++++++++++ config/openstack/objects/remote_installer.py | 50 ++++++++++++++ framework/validation/validation.py | 49 ++++++++++++++ .../object/system_application_status_enum.py | 3 + .../system_application_apply_keywords.py | 16 +++++ .../system_application_list_keywords.py | 52 ++++++++++++++- .../system_application_upload_keywords.py | 10 ++- testcases/conftest.py | 1 + testcases/openstack/__init__.py | 0 testcases/openstack/openstack_install.py | 66 +++++++++++++++++++ testcases/openstack/openstack_version.py | 30 +++++++++ 15 files changed, 446 insertions(+), 10 deletions(-) create mode 100644 config/openstack/files/default.json5 create mode 100644 config/openstack/objects/custom_installer.py create mode 100644 config/openstack/objects/openstack_config.py create mode 100644 config/openstack/objects/remote_installer.py create mode 100644 testcases/openstack/__init__.py create mode 100644 testcases/openstack/openstack_install.py create mode 100644 testcases/openstack/openstack_version.py diff --git a/config/configuration_file_locations_manager.py b/config/configuration_file_locations_manager.py index 9260e66a..8d5bede8 100644 --- a/config/configuration_file_locations_manager.py +++ b/config/configuration_file_locations_manager.py @@ -21,6 +21,7 @@ class ConfigurationFileLocationsManager: self.security_config_file = None self.usm_config_file = None self.app_config_file = None + self.openstack_config_file = None def set_configs_from_pytest_args(self, session: Session): """ @@ -80,6 +81,10 @@ class ConfigurationFileLocationsManager: if app_config_file: self.set_app_config_file(app_config_file) + openstack_config_file = session.config.getoption("--openstack_config_file") + if openstack_config_file: + self.set_openstack_config_file(openstack_config_file) + def set_configs_from_options_parser(self, parser: OptionParser = None): """ Sets the config files from options parser. @@ -142,6 +147,10 @@ class ConfigurationFileLocationsManager: if app_config_file: self.set_app_config_file(app_config_file) + openstack_config_file = options.openstack_config_file + if openstack_config_file: + self.set_openstack_config_file(openstack_config_file) + def _add_options(self, parser: OptionParser): """ Adds the command line options we can expect. @@ -248,6 +257,14 @@ class ConfigurationFileLocationsManager: help="The app config file", ) + parser.add_option( + "--openstack_config_file", + action="store", + type="str", + dest="openstack_config_file", + help="The openstack config file", + ) + options, args = parser.parse_args() return options @@ -499,3 +516,22 @@ class ConfigurationFileLocationsManager: """ self.app_config_file = app_config_file + + def get_openstack_config_file(self) -> str: + """ + Getter for openstack config file + + Returns: + str: the openstack config file + """ + return self.openstack_config_file + + def set_openstack_config_file(self, openstack_config_file: str): + """ + Setter for app config file + + Args: + openstack_config_file (str): the app config file + + """ + self.openstack_config_file = openstack_config_file \ No newline at end of file diff --git a/config/configuration_manager.py b/config/configuration_manager.py index aa8ebf26..2834d96f 100644 --- a/config/configuration_manager.py +++ b/config/configuration_manager.py @@ -6,6 +6,7 @@ from config.docker.objects.docker_config import DockerConfig from config.k8s.objects.k8s_config import K8sConfig from config.lab.objects.lab_config import LabConfig from config.logger.objects.logger_config import LoggerConfig +from config.openstack.objects.openstack_config import OpenstackConfig from config.ptp.objects.ptp_config import PTPConfig from config.rest_api.objects.rest_api_config import RestAPIConfig from config.security.objects.security_config import SecurityConfig @@ -34,6 +35,7 @@ class ConfigurationManagerClass: self.usm_config: USMConfig = None self.app_config: AppConfig = None self.configuration_locations_manager = None + self.openstack_config: OpenstackConfig = None def is_config_loaded(self) -> bool: """ @@ -108,6 +110,10 @@ class ConfigurationManagerClass: if not app_config_file: app_config_file = get_stx_resource_path("config/app/files/default.json5") + openstack_config_file = config_file_locations.get_openstack_config_file() + if not openstack_config_file: + openstack_config_file = get_stx_resource_path("config/openstack/files/default.json5") + if not self.loaded: try: self.lab_config = LabConfig(lab_config_file) @@ -122,6 +128,7 @@ class ConfigurationManagerClass: self.security_config = SecurityConfig(security_config_file) self.usm_config = USMConfig(usm_config_file) self.app_config = AppConfig(app_config_file) + self.openstack_config = OpenstackConfig(openstack_config_file) self.loaded = True except FileNotFoundError as e: print(f"Unable to load the config using file: {str(e.filename)} ") @@ -246,7 +253,18 @@ class ConfigurationManagerClass: """ return self.app_config - def get_config_pytest_args(self) -> list[str]: + + def get_openstack_config(self) -> OpenstackConfig: + """ + Getter for openstack config + + Returns: + OpenstackConfig: the openstack config + + """ + return self.openstack_config + + def get_config_pytest_args(self) -> [str]: """ Returns the configuration file locations as pytest args. @@ -279,6 +297,8 @@ class ConfigurationManagerClass: pytest_config_args.append(f"--usm_config_file={self.configuration_locations_manager.get_usm_config_file()}") if self.configuration_locations_manager.app_config_file: pytest_config_args.append(f"--app_config_file={self.configuration_locations_manager.get_app_config_file()}") + if self.configuration_locations_manager.openstack_config_file: + pytest_config_args.append(f"--openstack_config_file={self.configuration_locations_manager.get_openstack_config_file()}") return pytest_config_args diff --git a/config/openstack/files/default.json5 b/config/openstack/files/default.json5 new file mode 100644 index 00000000..99e761d3 --- /dev/null +++ b/config/openstack/files/default.json5 @@ -0,0 +1,19 @@ +{ + // openstack configuration + "app_name": "stx-openstack", + "version_cmd": "cat /opt/platform/fluxcd/*/stx-openstack/*/metadata.yaml", + + "remote": { + "enabled": false, + "file_server": "", + "app_version": "latest_build", + "app_build": "stx-openstack-master-debian", + + }, + + "custom": { + "enabled": true, + "file_location": "/home/sysadmin/", + "file_name": "helm-chart-manifest.tgz" + } +} \ No newline at end of file diff --git a/config/openstack/objects/custom_installer.py b/config/openstack/objects/custom_installer.py new file mode 100644 index 00000000..98103599 --- /dev/null +++ b/config/openstack/objects/custom_installer.py @@ -0,0 +1,41 @@ +class CustomInstaller: + """ + Class to handle custom configuration object + """ + + def __init__(self, custom_dict: []): + self.enabled_flag = custom_dict['enabled'] + self.file_location = custom_dict['file_location'] + self.file_name = custom_dict['file_location'] + + def get_enabled_flag(self) -> str: + """ + Getter for custom enabled flag + Returns: the enabled_flag + + """ + return self.enabled_flag + + def get_file_location(self) -> str: + """ + Getter for helm file location + Returns: the file_location + + """ + return self.file_location + + def get_file_name(self) -> str: + """ + Getter for helm file name + Returns: the file_name + + """ + return self.file_name + + def get_file_path(self) -> str: + """ + This function will return a single string representation of the file path of custom object + Returns: str + + """ + return f"{self.file_location}{self.file_name}" diff --git a/config/openstack/objects/openstack_config.py b/config/openstack/objects/openstack_config.py new file mode 100644 index 00000000..21517b15 --- /dev/null +++ b/config/openstack/objects/openstack_config.py @@ -0,0 +1,61 @@ +from typing import List + +import json5 + +from config.openstack.objects.custom_installer import CustomInstaller +from config.openstack.objects.remote_installer import RemoteInstaller + + +class OpenstackConfig: + """ + Class to hold configuration of the openstack + """ + + def __init__(self, config): + + try: + json_data = open(config) + except FileNotFoundError: + print(f"Could not find the openstack config file: {config}") + raise + + openstack_dict = json5.load(json_data) + + self.app_name = openstack_dict['app_name'] + self.version_cmd = openstack_dict['version_cmd'] + self.custom_config = CustomInstaller(openstack_dict["custom"]) + self.remote_config = RemoteInstaller(openstack_dict["remote"]) + + + def get_app_name(self) : + """ + Getter for the application name. + Returns: the application name. + + """ + return self.app_name + + def get_version_cmd(self) : + """ + Getter for the application version command with file path. + Returns: the command for retrieving application version. + + """ + return self.version_cmd + + def get_custom_config(self) -> CustomInstaller: + """ + Getter for the custom config object. + Returns: the custom config object. + + """ + + return self.custom_config + + def get_remote_config(self) -> RemoteInstaller: + """ + Getter for the remote config object. + Returns: the custom config object. + + """ + return self.remote_config diff --git a/config/openstack/objects/remote_installer.py b/config/openstack/objects/remote_installer.py new file mode 100644 index 00000000..b66d73be --- /dev/null +++ b/config/openstack/objects/remote_installer.py @@ -0,0 +1,50 @@ +class RemoteInstaller: + """ + Class to handle remote configuration object + """ + + def __init__(self, remote_dict: []): + self.enabled_flag = remote_dict['enabled'] + self.file_server = remote_dict['file_server'] + self.app_version = remote_dict['app_version'] + self.app_build = remote_dict['app_build'] + + def get_enabled_flag(self) -> str: + """ + Getter for remote enabled flag + Returns: the enabled_flag + + """ + return self.enabled_flag + + def get_file_server(self) -> str: + """ + Getter for file server + Returns: the file_server + + """ + return self.file_server + + def get_app_version(self) : + """ + Getter for application version + Returns: the app_version + + """ + return self.app_version + + def get_app_build(self) -> str: + """ + Getter for application build + Returns: the app_build + + """ + return self.app_build + + def get_file_path(self) -> str: + """ + This function will return a single string representation of the installer location remote object + Returns: str + + """ + return f"{self.file_server}/load/wrcp_rel/{self.app_version}/{self.app_build}/helm-charts/" diff --git a/framework/validation/validation.py b/framework/validation/validation.py index 292b79c7..5748a34e 100644 --- a/framework/validation/validation.py +++ b/framework/validation/validation.py @@ -193,3 +193,52 @@ def validate_list_contains(observed_value: Any, expected_values: Any, validation get_logger().log_error(f"Expected: {expected_values}") get_logger().log_error(f"Observed: {observed_value}") raise Exception("Validation Failed") + + +def validate_list_contains_with_retry( + function_to_execute: Callable[[], Any], + expected_values: Any, + validation_description: str, + timeout: int = 30, + polling_sleep_time: int = 5, +) -> str: + """ + This function will validate if the observed value contains the expected value. + + Args: + function_to_execute (Callable[[], Any]): The function to be executed repeatedly, taking no arguments and returning any value. + expected_values (Any): the list of expected values. + validation_description (str): Description of this validation for logging purposes. + timeout (int): The maximum time (in seconds) to wait for the match. + polling_sleep_time (int): The interval of time to wait between calls to function_to_execute. + + + Returns: str + + Raises: + Exception: when validate fails + + """ + get_logger().log_info(f"Attempting Validation - {validation_description}") + end_time = time.time() + timeout + + # Attempt the validation + while True: + + # Compute the actual value that we are trying to validate. + result = function_to_execute() + + if result in expected_values: + get_logger().log_info(f"Validation Successful - {validation_description}") + return result + else: + get_logger().log_info("Validation Failed") + get_logger().log_info(f"Expected: {expected_values}") + get_logger().log_info(f"Observed: {result}") + + if time.time() < end_time: + get_logger().log_info(f"Retrying in {polling_sleep_time}s") + sleep(polling_sleep_time) + # Move on to the next iteration + else: + raise TimeoutError(f"Timeout performing validation - {validation_description}") \ No newline at end of file diff --git a/keywords/cloud_platform/system/application/object/system_application_status_enum.py b/keywords/cloud_platform/system/application/object/system_application_status_enum.py index c4a74a79..eb272b79 100644 --- a/keywords/cloud_platform/system/application/object/system_application_status_enum.py +++ b/keywords/cloud_platform/system/application/object/system_application_status_enum.py @@ -8,3 +8,6 @@ class SystemApplicationStatusEnum(Enum): APPLYING = "applying" APPLY_FAILED = "apply-failed" APPLIED = "applied" + REMOVING = 'removing' + REMOVE_FAILED = 'remove-failed' + DELETING = 'deleting' diff --git a/keywords/cloud_platform/system/application/system_application_apply_keywords.py b/keywords/cloud_platform/system/application/system_application_apply_keywords.py index 01952da5..b4e4b8a1 100644 --- a/keywords/cloud_platform/system/application/system_application_apply_keywords.py +++ b/keywords/cloud_platform/system/application/system_application_apply_keywords.py @@ -94,3 +94,19 @@ class SystemApplicationApplyKeywords(BaseKeyword): cmd = f'system application-apply {app_name}' return cmd + + def is_applied_or_failed(self, app_name: str) -> bool: + """ + Verifies if the application has already been applied or apply-failed. + Args: + app_name (str): a string representing the name of the application. + + Returns: + bool: True if the application named 'app_name' has already been applied or apply-failed; False otherwise. + + """ + system_application_list_keywords = SystemApplicationListKeywords(self.ssh_connection) + if system_application_list_keywords.get_system_application_list().is_in_application_list(app_name): + application = system_application_list_keywords.get_system_application_list().get_application(app_name) + return application.get_status() == SystemApplicationStatusEnum.APPLIED.value or application.get_status() == SystemApplicationStatusEnum.APPLY_FAILED.value + return False diff --git a/keywords/cloud_platform/system/application/system_application_list_keywords.py b/keywords/cloud_platform/system/application/system_application_list_keywords.py index b92d9042..661b255d 100644 --- a/keywords/cloud_platform/system/application/system_application_list_keywords.py +++ b/keywords/cloud_platform/system/application/system_application_list_keywords.py @@ -1,7 +1,9 @@ import time +from typing import List +from framework.exceptions.keyword_exception import KeywordException from framework.logging.automation_logger import get_logger -from framework.validation.validation import validate_equals_with_retry +from framework.validation.validation import validate_equals_with_retry, validate_list_contains_with_retry from keywords.base_keyword import BaseKeyword from keywords.cloud_platform.command_wrappers import source_openrc from keywords.cloud_platform.system.application.object.system_application_list_output import SystemApplicationListOutput @@ -37,12 +39,14 @@ class SystemApplicationListKeywords(BaseKeyword): return system_application_list_output - def validate_app_status(self, application_name: str, status: str): + def validate_app_status(self, application_name: str, status: str, timeout=300, polling_sleep_time=5): """ This function will validate that the application specified reaches the desired status. Args: application_name: Name of the application that we are waiting for. status: Status in which we want to wait for the application to reach. + timeout: Timeout in seconds + polling_sleep_time: wait time in seconds before the next attempt when unsuccessful validation Returns: None @@ -54,5 +58,47 @@ class SystemApplicationListKeywords(BaseKeyword): return application_status message = f"Application {application_name}'s status is {status}" - validate_equals_with_retry(get_app_status, status, message, timeout=300) + validate_equals_with_retry(get_app_status, status, message, timeout, polling_sleep_time) + def validate_app_status_in_list(self, application_name: str, status: List[str], timeout=3600, polling_sleep_time=30) -> str: + """ + This function will validate that the application specified reaches the desired status. + Args: + application_name: Name of the application that we are waiting for. + status: Status in which we want to wait for the application to reach. + timeout: Timeout in seconds + polling_sleep_time: wait time in seconds before the next attempt when unsuccessful validation + + Returns: observed_status of the application + + """ + + def get_app_status(): + system_applications = self.get_system_application_list() + application_status = system_applications.get_application(application_name).get_status() + return application_status + + message = f"Application {application_name}'s status is in {status}" + observed_status=validate_list_contains_with_retry(get_app_status, status, message, timeout, polling_sleep_time) + return observed_status + + + def is_app_present(self, application_name: str) -> bool: + """ + This function will validate that the application is present in application list. + Args: + application_name: Name of the application that we want to check. + + Returns: boolean value True if application is found in list else False + + """ + + try: + system_applications = self.get_system_application_list() + application_status = system_applications.get_application(application_name).get_status() + get_logger().log_info(f'{application_name} is present. Status is {application_status}') + return True + + except KeywordException: + get_logger().log_info(f'{application_name} is not found.') + return False \ No newline at end of file diff --git a/keywords/cloud_platform/system/application/system_application_upload_keywords.py b/keywords/cloud_platform/system/application/system_application_upload_keywords.py index 324b6bc7..0bff8486 100644 --- a/keywords/cloud_platform/system/application/system_application_upload_keywords.py +++ b/keywords/cloud_platform/system/application/system_application_upload_keywords.py @@ -45,9 +45,9 @@ class SystemApplicationUploadKeywords(BaseKeyword): cmd = self.get_command(system_application_upload_input) app_name = system_application_upload_input.get_app_name() - # If the upload must be forced, checks if the applications is applied/uploaded and remove/delete it, if so. + # If the upload must be forced, checks if the applications is applied/apply-failed/uploaded/upload-failed and remove/delete it, if so. if system_application_upload_input.get_force(): - if SystemApplicationApplyKeywords(self.ssh_connection).is_already_applied(app_name): + if SystemApplicationApplyKeywords(self.ssh_connection).is_applied_or_failed(app_name): system_application_remove_input = SystemApplicationRemoveInput() system_application_remove_input.set_force_removal(True) system_application_remove_input.set_app_name(app_name) @@ -65,10 +65,10 @@ class SystemApplicationUploadKeywords(BaseKeyword): # Tracks the execution of the command 'system application-upload' until its completion or a timeout. system_application_list_keywords = SystemApplicationListKeywords(self.ssh_connection) - system_application_list_keywords.validate_app_status(app_name, 'uploaded') + system_application_list_keywords.validate_app_status(app_name, SystemApplicationStatusEnum.UPLOADED.value) # If the execution arrived here the status of the application is 'uploaded'. - system_application_output.get_system_application_object().set_status('uploaded') + system_application_output.get_system_application_object().set_status(SystemApplicationStatusEnum.UPLOADED.value) return system_application_output @@ -91,9 +91,7 @@ class SystemApplicationUploadKeywords(BaseKeyword): application = SystemApplicationListKeywords(self.ssh_connection).get_system_application_list().get_application(app_name) return ( application.get_status() == SystemApplicationStatusEnum.UPLOADED.value - or application.get_status() == SystemApplicationStatusEnum.APPLIED.value or application.get_status() == SystemApplicationStatusEnum.UPLOAD_FAILED.value - or application.get_status() == SystemApplicationStatusEnum.APPLY_FAILED.value ) return False except Exception as ex: diff --git a/testcases/conftest.py b/testcases/conftest.py index a0e93c63..332be47c 100644 --- a/testcases/conftest.py +++ b/testcases/conftest.py @@ -27,6 +27,7 @@ def pytest_addoption(parser: Any): parser.addoption("--security_config_file", action="store") parser.addoption("--usm_config_file", action="store") parser.addoption("--app_config_file", action="store") + parser.addoption("--openstack_config_file", action="store") def pytest_sessionstart(session: Any): diff --git a/testcases/openstack/__init__.py b/testcases/openstack/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/testcases/openstack/openstack_install.py b/testcases/openstack/openstack_install.py new file mode 100644 index 00000000..3e17ee0d --- /dev/null +++ b/testcases/openstack/openstack_install.py @@ -0,0 +1,66 @@ +from config.configuration_file_locations_manager import ConfigurationFileLocationsManager +from config.configuration_manager import ConfigurationManagerClass +from framework.logging.automation_logger import get_logger +from keywords.cloud_platform.ssh.lab_connection_keywords import LabConnectionKeywords +from keywords.cloud_platform.system.application.system_application_upload_keywords import SystemApplicationUploadKeywords +from keywords.cloud_platform.system.application.object.system_application_upload_input import SystemApplicationUploadInput +from keywords.cloud_platform.system.application.object.system_application_status_enum import SystemApplicationStatusEnum +from keywords.cloud_platform.system.application.system_application_apply_keywords import SystemApplicationApplyKeywords +from keywords.cloud_platform.system.application.system_application_list_keywords import SystemApplicationListKeywords + +def test_openstack_install(): + """ + Test to install and uninstall the OpenStack application + + Test Steps: + - connect to active controller + - check application status + - Uninstall the existing application + - Install the specified version of the application + + """ + get_logger().log_info('App Install Step') + # Setups the upload input object. + system_application_upload_input = SystemApplicationUploadInput() + configuration_manager = ConfigurationManagerClass() + config_file_locations = ConfigurationFileLocationsManager() + configuration_manager.load_configs(config_file_locations) + app_name = configuration_manager.get_openstack_config().get_app_name() + system_application_upload_input.set_app_name(app_name) + if configuration_manager.get_openstack_config().get_remote_config().get_enabled_flag(): + system_application_upload_input.set_app_version(configuration_manager.get_openstack_config().get_remote_config().get_app_version()) + system_application_upload_input.set_automatic_installation(False) + elif configuration_manager.get_openstack_config().get_custom_config().get_enabled_flag(): + system_application_upload_input.set_tar_file_path(configuration_manager.get_openstack_config().get_custom_config().get_file_path()) + system_application_upload_input.set_force(True) + # Setups app configs and lab connection + lab_connect_keywords = LabConnectionKeywords() + ssh_connection = lab_connect_keywords.get_active_controller_ssh() + status_list = [SystemApplicationStatusEnum.UPLOADED.value, SystemApplicationStatusEnum.APPLIED.value, SystemApplicationStatusEnum.UPLOAD_FAILED.value, SystemApplicationStatusEnum.APPLY_FAILED.value] + #if uploading or applying or removing or deleting, wait to complete + app_status="Not Uploaded or Applied" + if SystemApplicationListKeywords(ssh_connection).is_app_present(app_name): + app_status = SystemApplicationListKeywords(ssh_connection).validate_app_status_in_list(app_name, status_list) + get_logger().log_info(f'{app_name} Status is {app_status}') + + get_logger().log_info(f'{app_name} Status is {app_status}') + get_logger().log_info('Uploading & Installing Openstack application') + system_application_upload_output = SystemApplicationUploadKeywords(ssh_connection).system_application_upload(system_application_upload_input) + + # Asserts that the uploading process concluded successfully + system_application_object = system_application_upload_output.get_system_application_object() + assert system_application_object is not None, f"Expecting 'system_application_object' as not None, Observed: {system_application_object}." + assert system_application_object.get_name() == app_name, f"Expecting 'app_name' = {app_name}, Observed: {system_application_object.get_name()}." + assert ( + system_application_object.get_status() == SystemApplicationStatusEnum.UPLOADED.value + ), f"Expecting 'system_application_object.get_status()' = {SystemApplicationStatusEnum.UPLOADED.value}, Observed: {system_application_object.get_status()}." + + # Asserts that the applying process concluded successfully + # Executes the application installation + system_application_apply_output = SystemApplicationApplyKeywords(ssh_connection).system_application_apply(app_name) + system_application_object = system_application_apply_output.get_system_application_object() + assert system_application_object is not None, f"Expecting 'system_application_object' as not None, Observed: {system_application_object}." + assert system_application_object.get_name() == app_name, f"Expecting 'app_name' = {app_name}, Observed: {system_application_object.get_name()}." + assert ( + system_application_object.get_status() == SystemApplicationStatusEnum.APPLIED.value + ), f"Expecting 'system_application_object.get_status()' = {SystemApplicationStatusEnum.APPLIED.value}, Observed: {system_application_object.get_status()}." \ No newline at end of file diff --git a/testcases/openstack/openstack_version.py b/testcases/openstack/openstack_version.py new file mode 100644 index 00000000..3082de9e --- /dev/null +++ b/testcases/openstack/openstack_version.py @@ -0,0 +1,30 @@ +from config.configuration_file_locations_manager import ConfigurationFileLocationsManager +from config.configuration_manager import ConfigurationManagerClass +from framework.logging.automation_logger import get_logger +from keywords.cloud_platform.ssh.lab_connection_keywords import LabConnectionKeywords + + +def test_openstack_version(): + """ + Test to retrieve openstack version present on lab + + Test Steps: + - connect to active controller + - run system cmd - cat /opt/platform/fluxcd/*/stx-openstack/*/metadata.yaml + - retrieve the openstack version and log the important values like name, version & build date + + """ + configuration_manager = ConfigurationManagerClass() + config_file_locations = ConfigurationFileLocationsManager() + configuration_manager.load_configs(config_file_locations) + cmd = configuration_manager.get_openstack_config().get_version_cmd() + + get_logger().log_info('Display App Version Step') + lab_connect_keywords = LabConnectionKeywords() + ssh_connection = lab_connect_keywords.get_active_controller_ssh() + cmd_out = ssh_connection.send(cmd) + + if cmd_out: + get_logger().log_info(f"App Name: {cmd_out[0]}") + get_logger().log_info(f"App Version: {cmd_out[1]}") + get_logger().log_info(f"App Build Date: {cmd_out[-1]}")