Refactoring

Add new keywords to support the software
deploy precheck testing.
Precheck timeout created

Jira: CGTS-74904

Tested On:
WRCP-SAPPHIRE-RAPIDS-8
YOW-WRCP-STD-017

Automation Logs:
/sharedsan/AUTOMATION_LOGS/WRCP/precheck/WRCP-SAPPHIRE-RAPIDS-8/202511261708/full_logs.txt
/sharedsan/AUTOMATION_LOGS/WRCP/precheck/YOW-WRCP-STD-017/202511261704/full_logs.txt

Change-Id: Ib2b54e5feda122617830fbaad2c083923a5825e5
Signed-off-by: Elson Oliveira <eclaudio@windriver.com>
This commit is contained in:
Elson Oliveira
2025-11-27 10:52:32 -03:00
parent 4086ab4b0f
commit c9a80c23b0
12 changed files with 790 additions and 151 deletions

View File

@@ -30,6 +30,7 @@
"upload_poll_interval_sec": 30, "upload_poll_interval_sec": 30,
"deployment_timeout_sec": 7200, "deployment_timeout_sec": 7200,
"activation_timeout_sec": 3600, "activation_timeout_sec": 3600,
"precheck_timeout_sec": 300,
// Remote copy settings // Remote copy settings
"copy_from_remote": true, "copy_from_remote": true,

View File

@@ -46,6 +46,7 @@ class USMConfig:
self.upload_poll_interval_sec = usm_dict.get("upload_poll_interval_sec", 30) self.upload_poll_interval_sec = usm_dict.get("upload_poll_interval_sec", 30)
self.upload_patch_timeout_sec = usm_dict.get("upload_patch_timeout_sec", 1800) self.upload_patch_timeout_sec = usm_dict.get("upload_patch_timeout_sec", 1800)
self.upload_release_timeout_sec = usm_dict.get("upload_release_timeout_sec", 1800) self.upload_release_timeout_sec = usm_dict.get("upload_release_timeout_sec", 1800)
self.precheck_timeout_sec = usm_dict.get("precheck_timeout_sec", 300)
def validate_config(self) -> None: def validate_config(self) -> None:
""" """
@@ -426,3 +427,19 @@ class USMConfig:
value (int): Maximum seconds to wait for release upload to complete. value (int): Maximum seconds to wait for release upload to complete.
""" """
self.upload_release_timeout_sec = value self.upload_release_timeout_sec = value
def get_precheck_timeout_sec(self) -> int:
"""Get timeout duration for deploy precheck completion.
Returns:
int: Maximum seconds to wait for deploy precheck to complete.
"""
return self.precheck_timeout_sec
def set_precheck_timeout_sec(self, value: int) -> None:
"""Set timeout duration for release upload completion.
Args:
value (int): Maximum seconds to wait for release upload to complete.
"""
self.precheck_timeout_sec = value

View File

@@ -1,6 +1,6 @@
import json5 import json5
from framework.exceptions.keyword_exception import KeywordException from framework.exceptions.keyword_exception import KeywordException
from framework.logging.automation_logger import get_logger
from framework.rest.rest_response import RestResponse from framework.rest.rest_response import RestResponse
from keywords.cloud_platform.system.host.objects.host_capabilities_object import HostCapabilities from keywords.cloud_platform.system.host.objects.host_capabilities_object import HostCapabilities
from keywords.cloud_platform.system.host.objects.system_host_show_object import SystemHostShowObject from keywords.cloud_platform.system.host.objects.system_host_show_object import SystemHostShowObject
@@ -13,203 +13,209 @@ class SystemHostShowOutput:
This class parses the output of 'system host-show' command into an object of type SystemHostShowObject. This class parses the output of 'system host-show' command into an object of type SystemHostShowObject.
""" """
def __init__(self, system_host_show_output): def __init__(self, system_host_show_output: str):
""" """
Constructor Constructor
Args: Args:
system_host_show_output: output of 'system host-show' command as a list of strings. system_host_show_output (str): output of 'system host-show' command as a list of strings.
""" """
if isinstance(system_host_show_output, RestResponse): # came from REST and is already in dict form if isinstance(system_host_show_output, RestResponse): # came from REST and is already in dict form
json_object = system_host_show_output.get_json_content() json_object = system_host_show_output.get_json_content()
if 'ihosts' in json_object: if "ihosts" in json_object:
hosts = json_object['ihosts'] hosts = json_object["ihosts"]
else: else:
hosts = [json_object] hosts = [json_object]
else: # this came from a system command and must be parsed else: # this came from a system command and must be parsed
system_vertical_table_parser = SystemVerticalTableParser(system_host_show_output) system_vertical_table_parser = SystemVerticalTableParser(system_host_show_output)
output_values = system_vertical_table_parser.get_output_values_dict() output_values = system_vertical_table_parser.get_output_values_dict()
output_values = system_host_show_output
hosts = [output_values] hosts = [output_values]
self.system_host_show_objects: list[SystemHostShowObject] = [] self.system_host_show_objects: list[SystemHostShowObject] = []
for host in hosts: for host in hosts:
system_host_show_object = SystemHostShowObject() system_host_show_object = SystemHostShowObject()
if 'action' in host: if "action" in host:
system_host_show_object.set_action(host['action']) system_host_show_object.set_action(host["action"])
if 'administrative' in host: if "administrative" in host:
system_host_show_object.set_administrative(host['administrative']) system_host_show_object.set_administrative(host["administrative"])
if 'apparmor' in host: if "apparmor" in host:
system_host_show_object.set_apparmor(host['apparmor']) system_host_show_object.set_apparmor(host["apparmor"])
if 'availability' in host: if "availability" in host:
system_host_show_object.set_availability(host['availability']) system_host_show_object.set_availability(host["availability"])
if 'bm_ip' in host: if "bm_ip" in host:
system_host_show_object.set_bm_ip(host['bm_ip']) system_host_show_object.set_bm_ip(host["bm_ip"])
if 'bm_type' in host: if "bm_type" in host:
system_host_show_object.set_bm_type(host['bm_type']) system_host_show_object.set_bm_type(host["bm_type"])
if 'bm_username' in host: if "bm_username" in host:
system_host_show_object.set_bm_username(host['bm_username']) system_host_show_object.set_bm_username(host["bm_username"])
if 'boot_device' in host: if "boot_device" in host:
system_host_show_object.set_boot_device(host['boot_device']) system_host_show_object.set_boot_device(host["boot_device"])
if 'capabilities' in host: if "capabilities" in host:
capabilities_dict = host['capabilities'] capabilities_dict = host["capabilities"]
# if this from system, we need to parse the string to a dict # if this from system, we need to parse the string to a dict
if not isinstance(system_host_show_output, RestResponse): if not isinstance(system_host_show_output, RestResponse):
capabilities_dict = json5.loads(host['capabilities']) capabilities_dict = json5.loads(host["capabilities"])
capabilities = HostCapabilities() capabilities = HostCapabilities()
if 'is_max_cpu_configurable' in capabilities_dict: if "is_max_cpu_configurable" in capabilities_dict:
capabilities.set_is_max_cpu_configurable(capabilities_dict['is_max_cpu_configurable']) capabilities.set_is_max_cpu_configurable(capabilities_dict["is_max_cpu_configurable"])
if 'mgmt_ipsec' in capabilities_dict: if "mgmt_ipsec" in capabilities_dict:
capabilities.set_mgmt_ipsec(capabilities_dict['mgmt_ipsec']) capabilities.set_mgmt_ipsec(capabilities_dict["mgmt_ipsec"])
if 'stor_function' in capabilities_dict: if "stor_function" in capabilities_dict:
capabilities.set_stor_function(capabilities_dict['stor_function']) capabilities.set_stor_function(capabilities_dict["stor_function"])
if 'Personality' in capabilities_dict: if "Personality" in capabilities_dict:
capabilities.set_personality(capabilities_dict['Personality']) capabilities.set_personality(capabilities_dict["Personality"])
system_host_show_object.set_capabilities(capabilities) system_host_show_object.set_capabilities(capabilities)
if 'clock_synchronization' in host: if "clock_synchronization" in host:
system_host_show_object.set_clock_synchronization(host['clock_synchronization']) system_host_show_object.set_clock_synchronization(host["clock_synchronization"])
if 'config_applied' in host: if "config_applied" in host:
system_host_show_object.set_config_applied(host['config_applied']) system_host_show_object.set_config_applied(host["config_applied"])
if 'config_status' in host: if "config_status" in host:
system_host_show_object.set_config_status(host['config_status']) system_host_show_object.set_config_status(host["config_status"])
if 'config_target' in host: if "config_target" in host:
system_host_show_object.set_config_target(host['config_target']) system_host_show_object.set_config_target(host["config_target"])
if 'console' in host: if "console" in host:
system_host_show_object.set_console(host['console']) system_host_show_object.set_console(host["console"])
if 'created_at' in host: if "created_at" in host:
system_host_show_object.set_created_at(host['created_at']) system_host_show_object.set_created_at(host["created_at"])
if 'cstates_available' in host: if "cstates_available" in host:
system_host_show_object.set_cstates_available(host['cstates_available']) system_host_show_object.set_cstates_available(host["cstates_available"])
if 'device_image_update' in host: if "device_image_update" in host:
system_host_show_object.set_device_image_update(host['device_image_update']) system_host_show_object.set_device_image_update(host["device_image_update"])
if 'hostname' in host: if "hostname" in host:
system_host_show_object.set_hostname(host['hostname']) system_host_show_object.set_hostname(host["hostname"])
if 'hw_settle' in host: if "hw_settle" in host:
system_host_show_object.set_hw_settle(int(host['hw_settle'])) system_host_show_object.set_hw_settle(int(host["hw_settle"]))
if 'id' in host: if "id" in host:
system_host_show_object.set_id(int(host['id'])) system_host_show_object.set_id(int(host["id"]))
if 'install_output' in host: if "install_output" in host:
system_host_show_object.set_install_output(host['install_output']) system_host_show_object.set_install_output(host["install_output"])
if 'install_state' in host: if "install_state" in host:
system_host_show_object.set_install_state(host['install_state']) system_host_show_object.set_install_state(host["install_state"])
if 'install_state_info' in host: if "install_state_info" in host:
system_host_show_object.set_install_state_info(host['install_state_info']) system_host_show_object.set_install_state_info(host["install_state_info"])
if 'inv_state' in host: if "inv_state" in host:
system_host_show_object.set_inv_state(host['inv_state']) system_host_show_object.set_inv_state(host["inv_state"])
if 'invprovision' in host: if "invprovision" in host:
system_host_show_object.set_invprovision(host['invprovision']) system_host_show_object.set_invprovision(host["invprovision"])
if 'iscsi_initiator_name' in host: if "iscsi_initiator_name" in host:
system_host_show_object.set_iscsi_initiator_name(host['iscsi_initiator_name']) system_host_show_object.set_iscsi_initiator_name(host["iscsi_initiator_name"])
if 'location' in host: if "location" in host:
system_host_show_object.set_location(host['location']) system_host_show_object.set_location(host["location"])
if 'max_cpu_mhz_allowed' in host: if "max_cpu_mhz_allowed" in host:
system_host_show_object.set_max_cpu_mhz_allowed(host['max_cpu_mhz_allowed']) system_host_show_object.set_max_cpu_mhz_allowed(host["max_cpu_mhz_allowed"])
if 'max_cpu_mhz_configured' in host: if "max_cpu_mhz_configured" in host:
system_host_show_object.set_max_cpu_mhz_configured(host['max_cpu_mhz_configured']) system_host_show_object.set_max_cpu_mhz_configured(host["max_cpu_mhz_configured"])
if 'mgmt_mac' in host: if "mgmt_mac" in host:
system_host_show_object.set_mgmt_mac(host['mgmt_mac']) system_host_show_object.set_mgmt_mac(host["mgmt_mac"])
if 'min_cpu_mhz_allowed' in host: if "min_cpu_mhz_allowed" in host:
system_host_show_object.set_min_cpu_mhz_allowed(host['min_cpu_mhz_allowed']) system_host_show_object.set_min_cpu_mhz_allowed(host["min_cpu_mhz_allowed"])
if 'nvme_host_id' in host: if "nvme_host_id" in host:
system_host_show_object.set_nvme_host_id(host['nvme_host_id']) system_host_show_object.set_nvme_host_id(host["nvme_host_id"])
if 'nvme_host_nqn' in host: if "nvme_host_nqn" in host:
system_host_show_object.set_nvme_host_nqn(host['nvme_host_nqn']) system_host_show_object.set_nvme_host_nqn(host["nvme_host_nqn"])
if 'operational' in host: if "operational" in host:
system_host_show_object.set_operational(host['operational']) system_host_show_object.set_operational(host["operational"])
if 'personality' in host: if "personality" in host:
system_host_show_object.set_personality(host['personality']) system_host_show_object.set_personality(host["personality"])
if 'reboot_needed' in host: if "reboot_needed" in host:
value = host['reboot_needed'] if isinstance(host['reboot_needed'], bool) else TypeConverter.str_to_bool(host['reboot_needed']) value = host["reboot_needed"] if isinstance(host["reboot_needed"], bool) else TypeConverter.str_to_bool(host["reboot_needed"])
system_host_show_object.set_reboot_needed(value) system_host_show_object.set_reboot_needed(value)
if 'reserved' in host: if "reserved" in host:
value = host['reserved'] if isinstance(host['reserved'], bool) else TypeConverter.str_to_bool(host['reserved']) value = host["reserved"] if isinstance(host["reserved"], bool) else TypeConverter.str_to_bool(host["reserved"])
system_host_show_object.set_reserved(value) system_host_show_object.set_reserved(value)
if 'rootfs_device' in host: if "rootfs_device" in host:
system_host_show_object.set_rootfs_device(host['rootfs_device']) system_host_show_object.set_rootfs_device(host["rootfs_device"])
if 'serialid' in host: if "serialid" in host:
system_host_show_object.set_serialid(host['serialid']) system_host_show_object.set_serialid(host["serialid"])
if 'software_load' in host: if "software_load" in host:
system_host_show_object.set_software_load(host['software_load']) system_host_show_object.set_software_load(host["software_load"])
if 'subfunction_avail' in host: if "subfunction_avail" in host:
system_host_show_object.set_subfunction_avail(host['subfunction_avail']) system_host_show_object.set_subfunction_avail(host["subfunction_avail"])
if 'subfunction_oper' in host: if "subfunction_oper" in host:
system_host_show_object.set_subfunction_oper(host['subfunction_oper']) system_host_show_object.set_subfunction_oper(host["subfunction_oper"])
if 'subfunctions' in host: if "subfunctions" in host:
system_host_show_object.set_subfunctions(TypeConverter.parse_string_to_list(host['subfunctions'])) system_host_show_object.set_subfunctions(TypeConverter.parse_string_to_list(host["subfunctions"]))
if 'sw_version' in host: if "sw_version" in host:
system_host_show_object.set_sw_version(host['sw_version']) system_host_show_object.set_sw_version(host["sw_version"])
if 'task' in host: if "task" in host:
system_host_show_object.set_task(host['task']) system_host_show_object.set_task(host["task"])
if 'tboot' in host: if "tboot" in host:
system_host_show_object.set_tboot(host['tboot']) system_host_show_object.set_tboot(host["tboot"])
if 'ttys_dcd' in host: if "ttys_dcd" in host:
value = host['ttys_dcd'] if isinstance(host['ttys_dcd'], bool) else TypeConverter.str_to_bool(host['ttys_dcd']) value = host["ttys_dcd"] if isinstance(host["ttys_dcd"], bool) else TypeConverter.str_to_bool(host["ttys_dcd"])
system_host_show_object.set_ttys_dcd(value) system_host_show_object.set_ttys_dcd(value)
if 'updated_at' in host: if "updated_at" in host:
system_host_show_object.set_updated_at(host['updated_at']) system_host_show_object.set_updated_at(host["updated_at"])
if 'uptime' in host: if "uptime" in host:
system_host_show_object.set_uptime(host['uptime']) system_host_show_object.set_uptime(host["uptime"])
if 'uuid' in host: if "uuid" in host:
system_host_show_object.set_uuid(host['uuid']) system_host_show_object.set_uuid(host["uuid"])
if 'vim_progress_status' in host: if "vim_progress_status" in host:
system_host_show_object.set_vim_progress_status(host['vim_progress_status']) system_host_show_object.set_vim_progress_status(host["vim_progress_status"])
self.system_host_show_objects.append(system_host_show_object) self.system_host_show_objects.append(system_host_show_object)
def _get_host_value(self, hostname: str=None): def _get_host_value(self, hostname: str = None) -> SystemHostShowObject:
"""
This function will return a SystemHostShowObject of a specific hostname
Args:
hostname (str): Host name to get its bmc type.
Returns:
SystemHostShowObject: SystemHostShowObject Object of a hostname if specified
"""
if hostname: if hostname:
hosts = list(filter(lambda system_show_object: system_show_object.get_hostname() == hostname, self.system_host_show_objects)) hosts = list(filter(lambda system_show_object: system_show_object.get_hostname() == hostname, self.system_host_show_objects))
if hosts: if hosts:
@@ -222,69 +228,78 @@ class SystemHostShowOutput:
raise KeywordException("There was not exactly 1 host") raise KeywordException("There was not exactly 1 host")
else: else:
# return the first one # return the first one
return self.system_host_show_objects[0] return self.system_host_show_objects[0]
def has_host_bmc_ipmi(self, hostname: str = None) -> bool:
def has_host_bmc_ipmi(self, hostname: str=None) -> bool:
""" """
This function will return True if bm_type of this host is 'ipmi'. This function will return True if bm_type of this host is 'ipmi'.
Returns: True if bm_type of this host is 'ipmi', False otherwise. Args:
hostname (str): Host name to get its bmc type.
Returns:
bool: True if bm_type of this host is 'ipmi', False otherwise.
""" """
system_host_show_object = self._get_host_value(hostname) system_host_show_object = self._get_host_value(hostname)
return system_host_show_object.get_bm_type() == "ipmi" return system_host_show_object.get_bm_type() == "ipmi"
def has_host_bmc_redfish(self, hostname: str=None) -> bool: def has_host_bmc_redfish(self, hostname: str = None) -> bool:
""" """
This function will return True if bm_type of this host is 'redfish', False otherwise. This function will return True if bm_type of this host is 'redfish', False otherwise.
Returns: True if bm_type of this host is 'redfish'. Args:
hostname (str): Host name to get its bmc type.
Returns:
bool: True if bm_type of this host is 'redfish'.
""" """
system_host_show_object = self._get_host_value(hostname) system_host_show_object = self._get_host_value(hostname)
return system_host_show_object.get_bm_type() == "redfish" return system_host_show_object.get_bm_type() == "redfish"
def has_host_bmc_dynamic(self, hostname: str=None) -> bool: def has_host_bmc_dynamic(self, hostname: str = None) -> bool:
""" """
This function will return True if bm_type of this host is 'dynamic', False otherwise. This function will return True if bm_type of this host is 'dynamic', False otherwise.
Returns: True if bm_type of this host is 'dynamic'. Args:
hostname (str): Host name to get its bmc type.
Returns:
bool: True if bm_type of this host is 'dynamic'.
""" """
system_host_show_object = self._get_host_value(hostname) system_host_show_object = self._get_host_value(hostname)
return system_host_show_object.get_bm_type() == "dynamic" return system_host_show_object.get_bm_type() == "dynamic"
def get_host_id(self, hostname: str=None)-> int: def get_host_id(self, hostname: str = None) -> int:
""" """
Gets the host id Gets the host id
Args: Args:
hostname (): the name of the host hostname (str): the name of the host
Returns: the host id
Returns:
int: the host id
""" """
system_host_show_object = self._get_host_value(hostname) system_host_show_object = self._get_host_value(hostname)
return system_host_show_object.get_id() return system_host_show_object.get_id()
def get_system_host_show_object(self, hostname: str=None) -> SystemHostShowObject: def get_system_host_show_object(self, hostname: str = None) -> SystemHostShowObject:
""" """
Gets the system host show object Gets the system host show object
Args: Args:
hostname (): the name of the host hostname (str): the name of the host
Returns: the system host show object Returns:
SystemHostShowObject: the system host show object
""" """
return self._get_host_value(hostname) return self._get_host_value(hostname)
def get_all_system_host_show_objects(self) -> list[SystemHostShowObject]: def get_all_system_host_show_objects(self) -> list[SystemHostShowObject]:
""" """
Gets all system host show objects Gets all system host show objects
Returns: list of system host show objects Returns:
list[SystemHostShowObject]: List of system host show objects
""" """
return self.system_host_show_objects return self.system_host_show_objects

View File

@@ -0,0 +1,63 @@
class SoftwareDeployPrecheckItemObject:
"""
Represents a single line/check from the 'software deploy precheck' output.
Example line:
'Ceph Storage Healthy: [OK]'
"""
def __init__(self, name: str, status: str):
"""
Constructor
Args:
name (str): Check name (e.g., 'Ceph Storage Healthy').
status (str): Status string (e.g., '[OK]', '[FAIL] ...').
"""
self._name = name
self._status = status
def get_name(self) -> str:
"""
Get the check name.
Returns:
str: Check name.
"""
return self._name
def get_status(self) -> str:
"""
Get the raw status.
Returns:
str: Status string as returned by the command.
"""
return self._status
def is_ok(self) -> bool:
"""
Check if this item is considered OK based on its status string.
Returns:
bool: True if the status contains "[OK]", False otherwise.
"""
return "[OK]" in self._status
def __str__(self) -> str:
"""
Return a readable string representation.
Returns:
str: Formatted string with name and status.
"""
return f"{self._name}: {self._status}"
def __repr__(self) -> str:
"""
Return the developer-facing representation.
Returns:
str: Class name and field values.
"""
return f"{self.__class__.__name__}(name={self._name}, status={self._status})"

View File

@@ -0,0 +1,115 @@
from typing import Dict, List
from framework.logging.automation_logger import get_logger
from keywords.cloud_platform.upgrade.objects.software_deploy_precheck_object import SoftwareDeployPrecheckItemObject
class SoftwareDeployPrecheckOutput:
"""
Parses the output of the 'software deploy precheck' command into structured objects.
The raw output is expected to contain lines in the format:
"<check name>: <status>"
Example:
"Ceph Storage Healthy: [OK]"
"No alarms: [OK]"
"System Health: [OK]"
"""
def __init__(self, raw_output: List[str]):
"""
Initialize and parse the precheck output.
Args:
raw_output (List[str]): Raw output lines from 'software deploy precheck'.
"""
self._raw_output = raw_output
self._items: List[SoftwareDeployPrecheckItemObject] = []
self._status_by_name: Dict[str, str] = {}
self._parse_output()
def _parse_output(self) -> None:
"""
Internal parsing of the raw output into objects and a name->status mapping.
"""
lines_to_parse = self._raw_output[:-1]
for line in lines_to_parse:
if ":" not in line:
get_logger().log_warning(f"There is unexpected output {line}")
continue
key, value = line.split(":", 1)
key = key.strip()
value = value.strip()
self._status_by_name[key] = value
self._items.append(SoftwareDeployPrecheckItemObject(name=key, status=value))
def get_items(self) -> List[SoftwareDeployPrecheckItemObject]:
"""
Get all parsed precheck items.
Returns:
List[SoftwareDeployPrecheckItemObject]: Parsed items.
"""
return self._items
def get_status_dict(self) -> Dict[str, str]:
"""
Get a mapping of check name -> status string.
Returns:
Dict[str, str]: Status by check name.
"""
return self._status_by_name
def get_status_by_name(self, name: str) -> str:
"""
Get the status string for a specific check.
Args:
name (str): Check name.
Returns:
str: Status string or empty string if not found.
"""
return self._status_by_name.get(name, "")
def get_failed_items(self) -> List[SoftwareDeployPrecheckItemObject]:
"""
Get all items that are not marked as OK.
Returns:
List[SoftwareDeployPrecheckItemObject]: Items where status does not contain "[OK]".
"""
return [item for item in self._items if not item.is_ok()]
def get_raw_output(self) -> List[str]:
"""
Get the raw output lines.
Returns:
List[str]: Raw command output.
"""
return self._raw_output
def __str__(self) -> str:
"""
Return a human-readable string representation of the precheck.
Returns:
str: Formatted precheck entries as strings.
"""
return "\n".join([str(item) for item in self._items])
def __repr__(self) -> str:
"""
Return the developer-facing representation of the object.
Returns:
str: Class name and row count.
"""
return f"{self.__class__.__name__}(items={len(self._items)})"

View File

@@ -0,0 +1,173 @@
"""Software deploy precheck keywords."""
from config.configuration_manager import ConfigurationManager
from framework.exceptions.keyword_exception import KeywordException
from framework.logging.automation_logger import get_logger
from framework.ssh.ssh_connection import SSHConnection
from keywords.base_keyword import BaseKeyword
from keywords.ceph.ceph_status_keywords import CephStatusKeywords
from keywords.cloud_platform.command_wrappers import source_openrc
from keywords.cloud_platform.fault_management.alarms.alarm_list_keywords import AlarmListKeywords
from keywords.cloud_platform.system.host.system_host_list_keywords import SystemHostListKeywords
from keywords.cloud_platform.system.host.system_host_show_keywords import SystemHostShowKeywords
from keywords.cloud_platform.upgrade.objects.software_deploy_precheck_output import SoftwareDeployPrecheckOutput
from keywords.k8s.node.kubectl_nodes_keywords import KubectlNodesKeywords
from keywords.k8s.pods.kubectl_get_pods_keywords import KubectlGetPodsKeywords
class SoftwareDeployPrecheckKeywords(BaseKeyword):
"""
Keywords for 'software deploy precheck' using the ACE object-output model.
This class:
- runs the 'software deploy precheck' command
- wraps the CLI output into SoftwareDeployPrecheckOutput
- performs additional cross-checks against system state
"""
def __init__(self, ssh_connection: SSHConnection):
"""
Instance of the class.
Args:
ssh_connection (SSHConnection): An instance of SSH connection.
"""
self.ssh_connection = ssh_connection
self.usm_config = ConfigurationManager.get_usm_config()
def _run_deploy_precheck(self, release_id: str, sudo: bool = False) -> SoftwareDeployPrecheckOutput:
"""
Run the 'software deploy precheck' command and return its parsed output.
Args:
release_id (str): Release to be prechecked.
sudo (bool): Option to pass the command with sudo.
Returns:
SoftwareDeployPrecheckOutput: Parsed precheck output.
Raises:
KeywordException: If the CLI command fails.
"""
if not release_id:
raise KeywordException("Missing release ID for software deploy precheck")
get_logger().log_info(f"Prechecking deploy software release: {release_id}")
base_cmd = f"software deploy precheck {release_id}"
cmd = source_openrc(base_cmd)
timeout = self.usm_config.get_precheck_timeout_sec()
if sudo:
output = self.ssh_connection.send_as_sudo(cmd, reconnect_timeout=timeout)
else:
output = self.ssh_connection.send(cmd, reconnect_timeout=timeout, get_pty=True)
# Validate the return code using the base keyword helper.
self.validate_success_return_code(self.ssh_connection)
# Wrap the output into the object-output model.
precheck_output = SoftwareDeployPrecheckOutput(output)
return precheck_output
def _validate_precheck_output(self, precheck_output: SoftwareDeployPrecheckOutput) -> bool:
"""
Validate the precheck output by cross-checking with the actual system state.
Args:
precheck_output (SoftwareDeployPrecheckOutput): Parsed precheck output.
Returns:
bool: True if validation passes, False otherwise.
"""
ceph_status = CephStatusKeywords(self.ssh_connection).ceph_status()
alarm_list = AlarmListKeywords(self.ssh_connection).alarm_list()
system_hosts = SystemHostListKeywords(self.ssh_connection)
system_host_show = SystemHostShowKeywords(self.ssh_connection)
hosts = system_hosts.get_system_host_list().get_hosts()
status_dict = precheck_output.get_status_dict()
for key, value in status_dict.items():
if "[OK]" in value:
get_logger().log_info(f"'{key}' is OK")
if key == "Ceph Storage Healthy" and not ceph_status.is_ceph_healthy():
get_logger().log_warning(f"Ceph is not healthy but '{key}' value is OK")
return False
if key == "No alarms" and [] != alarm_list:
get_logger().log_warning(f"There are one or more alarms but '{key}' value is OK")
return False
if key == "All hosts are provisioned":
for host in hosts:
system_host_show_object = system_host_show.get_system_host_show_output(host.get_host_name()).get_system_host_show_object()
provisioned = True if system_host_show_object.get_invprovision() == "provisioned" else False
# Don't remove this validation, to avoid bool failures.
if not provisioned:
get_logger().log_warning(f"The host {host} is not provisioned but {key} value is Ok")
return False
if key == "All hosts are unlocked/enabled":
for host in hosts:
if host.get_administrative == "locked":
get_logger().log_warning(f"There are one or more locked hosts but '{key}' value is OK")
return False
if host.get_operational() == "disabled":
get_logger().log_warning(f"There are one or more disabled hosts but '{key}' value is OK")
return False
if key == "All hosts have current configurations":
for host in hosts:
system_host_show_object = system_host_show.get_system_host_show_output(host.get_host_name()).get_system_host_show_object()
config_applied = system_host_show_object.get_config_applied()
config_target = system_host_show_object.get_config_target()
if config_applied != config_target:
get_logger().log_warning("There are one or host with failed " f"configuration but '{key}' value is OK")
return False
if key == "All kubernetes nodes are ready":
nodes = KubectlNodesKeywords(self.ssh_connection).get_kubectl_nodes().get_nodes()
for node in nodes:
if node.get_status() != "Ready":
get_logger().log_warning("There are one or more kubernetes nodes not ready " f"but '{key}' value is OK")
return False
if key == "All kubernetes control plane pods are ready":
kube_get_pods = KubectlGetPodsKeywords(self.ssh_connection)
if kube_get_pods.get_unhealthy_pods().get_pods():
get_logger().log_warning(f"There are one or more failed pods but '{key}' value is OK")
return False
if key == "Active controller is controller-0" and system_hosts.get_active_controller().get_host_name() != "controller-0":
get_logger().log_warning(f"controller-0 is not active but '{key}' value is OK")
return False
else:
if key != "System Health":
get_logger().log_warning(f"The following check '{key}' is not OK")
return False
return True
def deploy_precheck(self, release_id: str, sudo: bool = False) -> SoftwareDeployPrecheckOutput:
"""
Run the deploy precheck for a software release and validate its result.
Args:
release_id (str): Release to be prechecked.
sudo (bool): Option to pass the command with sudo.
Returns:
SoftwareDeployPrecheckOutput: Parsed and validated precheck output.
Raises:
AssertionError: If any of the checks fail.
"""
precheck_output = self._run_deploy_precheck(release_id, sudo=sudo)
is_valid = self._validate_precheck_output(precheck_output)
assert is_valid, f"There is failed resource in the deploy precheck. Output: {precheck_output.get_raw_output()}"
get_logger().log_info("Deploy precheck completed:\n" + "\n".join(precheck_output.get_raw_output()))
return precheck_output

View File

@@ -1,12 +1,12 @@
from config.configuration_manager import ConfigurationManager
from framework.exceptions.keyword_exception import KeywordException from framework.exceptions.keyword_exception import KeywordException
from framework.logging.automation_logger import get_logger from framework.logging.automation_logger import get_logger
from framework.ssh.ssh_connection import SSHConnection from framework.ssh.ssh_connection import SSHConnection
from framework.validation.validation import validate_equals_with_retry from framework.validation.validation import validate_equals_with_retry
from keywords.base_keyword import BaseKeyword from keywords.base_keyword import BaseKeyword
from keywords.cloud_platform.upgrade.objects.software_upload_output import SoftwareUploadOutput
from keywords.cloud_platform.command_wrappers import source_openrc from keywords.cloud_platform.command_wrappers import source_openrc
from keywords.cloud_platform.upgrade.objects.software_upload_output import SoftwareUploadOutput
from keywords.cloud_platform.upgrade.software_show_keywords import SoftwareShowKeywords from keywords.cloud_platform.upgrade.software_show_keywords import SoftwareShowKeywords
from config.configuration_manager import ConfigurationManager
class USMKeywords(BaseKeyword): class USMKeywords(BaseKeyword):
@@ -20,14 +20,14 @@ class USMKeywords(BaseKeyword):
self.ssh_connection = ssh_connection self.ssh_connection = ssh_connection
self.usm_config = ConfigurationManager.get_usm_config() self.usm_config = ConfigurationManager.get_usm_config()
def upload_patch_file(self, patch_file_path: str, sudo: bool = False, os_region_name: str = "" ) -> SoftwareUploadOutput: def upload_patch_file(self, patch_file_path: str, sudo: bool = False, os_region_name: str = "") -> SoftwareUploadOutput:
""" """
Upload a single patch file using 'software upload'. Upload a single patch file using 'software upload'.
Args: Args:
patch_file_path (str): Absolute path to a .patch file. patch_file_path (str): Absolute path to a .patch file.
sudo (bool): Option to pass the command with sudo. sudo (bool): Option to pass the command with sudo.
os_region_name: Use Os region name option for upload if it is specified os_region_name (str): Use Os region name option for upload if it is specified
Raises: Raises:
KeywordException: On failure to upload. KeywordException: On failure to upload.
@@ -57,7 +57,7 @@ class USMKeywords(BaseKeyword):
Args: Args:
patch_dir_path (str): Absolute path to a directory of .patch files. patch_dir_path (str): Absolute path to a directory of .patch files.
sudo (bool): Option to pass the command with sudo. sudo (bool): Option to pass the command with sudo.
os_region_name: OS region name option for upload if it is specified os_region_name (str): OS region name option for upload if it is specified
Raises: Raises:
KeywordException: On failure to upload. KeywordException: On failure to upload.
@@ -105,7 +105,7 @@ class USMKeywords(BaseKeyword):
iso_path (str): Absolute path to the .iso file. iso_path (str): Absolute path to the .iso file.
sig_path (str): Absolute path to the corresponding .sig file. sig_path (str): Absolute path to the corresponding .sig file.
sudo (bool): Option to pass the command with sudo. sudo (bool): Option to pass the command with sudo.
os_region_name: Use Os region name option for upload if it is specified os_region_name (str): Use Os region name option for upload if it is specified
Raises: Raises:
KeywordException: On failure to upload. KeywordException: On failure to upload.
@@ -124,7 +124,7 @@ class USMKeywords(BaseKeyword):
self.validate_success_return_code(self.ssh_connection) self.validate_success_return_code(self.ssh_connection)
get_logger().log_info("Release upload completed:\n" + "\n".join(output)) get_logger().log_info("Release upload completed:\n" + "\n".join(output))
def upload_and_verify_patch_file(self, patch_file_path: str, expected_release_id: str, timeout: int, poll_interval: int, sudo: bool = False, os_region_name: str="") -> None: def upload_and_verify_patch_file(self, patch_file_path: str, expected_release_id: str, timeout: int, poll_interval: int, sudo: bool = False, os_region_name: str = "") -> None:
"""Upload a patch and verify that it becomes available. """Upload a patch and verify that it becomes available.
This method is used for USM patching operations. It uploads a `.patch` file This method is used for USM patching operations. It uploads a `.patch` file
@@ -137,7 +137,8 @@ class USMKeywords(BaseKeyword):
timeout (int): Maximum number of seconds to wait for the release to appear. timeout (int): Maximum number of seconds to wait for the release to appear.
poll_interval (int): Interval (in seconds) between poll attempts. poll_interval (int): Interval (in seconds) between poll attempts.
sudo (bool): Option to pass the command with sudo. sudo (bool): Option to pass the command with sudo.
os_region_name: Use Os region name option for upload if it is specified os_region_name (str): Use Os region name option for upload if it is specified
Raises: Raises:
KeywordException: If upload fails or release does not become available in time. KeywordException: If upload fails or release does not become available in time.
@@ -166,7 +167,7 @@ class USMKeywords(BaseKeyword):
timeout (int): Maximum number of seconds to wait for the release to appear. timeout (int): Maximum number of seconds to wait for the release to appear.
poll_interval (int): Interval (in seconds) between poll attempts. poll_interval (int): Interval (in seconds) between poll attempts.
sudo (bool): Option to pass the command with sudo. sudo (bool): Option to pass the command with sudo.
os_region_name (str): Region name used when upload to the DC Systems os_region_name (str): Use Os region name option for upload if it is specified
Raises: Raises:
KeywordException: If upload fails or release does not become available in time. KeywordException: If upload fails or release does not become available in time.

View File

@@ -0,0 +1,34 @@
from framework.ssh.ssh_connection import SSHConnection
from keywords.base_keyword import BaseKeyword
from keywords.cloud_platform.command_wrappers import source_openrc
from keywords.k8s.k8s_command_wrapper import export_k8s_config
from keywords.k8s.node.object.kubectl_nodes_output import KubectlNodesOutput
class KubectlNodesKeywords(BaseKeyword):
"""
Class for Kubectl "kubectl get nodes" keywords
"""
def __init__(self, ssh_connection: SSHConnection):
"""
Constructor
Args:
ssh_connection(SSHConnection): SSH Connection object
"""
self.ssh_connection = ssh_connection
def get_kubectl_nodes(self) -> KubectlNodesOutput:
"""
Gets the kubectl get nodes
Returns:
KubectlNodesOutput: KubectlNodesOutput object
"""
output = self.ssh_connection.send(source_openrc(export_k8s_config("kubectl get nodes")))
self.validate_success_return_code(self.ssh_connection)
kubectl_nodes_output = KubectlNodesOutput(output)
return kubectl_nodes_output

View File

@@ -0,0 +1,102 @@
class KubectlNodesObject:
"""
Class to hold attributes of a 'kubectl get nodes' entry.
"""
def __init__(self):
"""
Constructor
"""
self.name: str = None
self.status: str = None
self.roles: str = None
self.age: str = None
self.version: str = None
def set_name(self, name: str):
"""
Setter for the name
Args:
name(str): Name of the Node
"""
self.name = name
def get_name(self) -> str:
"""
Getter for the name of the node.
Returns: (str) name of the node.
"""
return self.name
def set_status(self, status: str):
"""
Setter for the status
Args:
status (str): Status of the Node
"""
self.status = status
def get_status(self) -> str:
"""
Getter for the status of the node.
Returns: (str) status of the node.
"""
return self.status
def set_roles(self, roles: str):
"""
Setter for the roles
Args:
roles(str): Roles of the Node
"""
self.roles = roles
def get_roles(self) -> str:
"""
Getter for the roles of the node.
Returns: (str) roles of the node.
"""
return self.roles
def set_age(self, age: str):
"""
Setter for the age of the node
Args:
age(str): Age of the Node
"""
self.age = age
def get_age(self) -> str:
"""
Getter for the age of the node.
Returns: (str) Age of the node.
"""
return self.age
def set_version(self, version: str):
"""
Setter for the version of the node
Args:
version(str): Version of the Node
"""
self.version = version
def get_version(self) -> str:
"""
Getter for the version of the node.
Returns:
str: Version of the node.
"""
return self.version

View File

@@ -0,0 +1,87 @@
from framework.exceptions.keyword_exception import KeywordException
from framework.logging.automation_logger import get_logger
from keywords.k8s.node.object.kubectl_nodes_object import KubectlNodesObject
from keywords.k8s.node.object.kubectl_nodes_table_parser import KubectlNodesTableParser
class KubectlNodesOutput:
"""
Class for 'kubectl get nodes' output.
"""
def __init__(self, kubectl_nodes_output: str):
"""
Constructor
Args:
kubectl_nodes_output(str): Raw string output from running a "kubectl get nodes" command.
"""
self.kubectl_nodes: list[KubectlNodesObject] = []
k8s_table_parser = KubectlNodesTableParser(kubectl_nodes_output)
output_values = k8s_table_parser.get_output_values_list()
for value in output_values:
if self.is_valid_output(value):
kubectl_nodes_object = KubectlNodesObject()
kubectl_nodes_object.set_name(value["NAME"])
kubectl_nodes_object.set_status(value["STATUS"])
kubectl_nodes_object.set_age(value["AGE"])
kubectl_nodes_object.set_version(value["VERSION"])
self.kubectl_nodes.append(kubectl_nodes_object)
else:
raise KeywordException(f"The output line {value} was not valid")
def get_nodes(self) -> list[KubectlNodesObject]:
"""
Returns the list of kubectl nodes objects.
Returns:
list[KubectlNodesObject]: List of KubectlNodesObject
"""
return self.kubectl_nodes
def get_node(self, node_name: str) -> KubectlNodesObject:
"""
Returns the node with the given name
Args:
node_name (str): the name of the node
Returns:
KubectlNodesObject: kubectl node object
"""
nodes = list(filter(lambda node: node.get_name() == node_name, self.kubectl_nodes))
if len(nodes) == 0:
raise KeywordException(f"No Node with name {node_name} was found.")
return nodes[0]
@staticmethod
def is_valid_output(value: dict) -> bool:
"""
Checks to ensure the output has the correct keys.
Args:
value (dict): The value to check.
Returns:
bool: True if valid, False otherwise.
"""
valid = True
if "NAME" not in value:
get_logger().log_error(f"NAME is not in the output value: {value}")
valid = False
if "STATUS" not in value:
get_logger().log_error(f"STATUS is not in the output value: {value}")
valid = False
if "ROLES" not in value:
get_logger().log_error(f"ROLES is not in the output value: {value}")
valid = False
if "AGE" not in value:
get_logger().log_error(f"AGE is not in the output value: {value}")
valid = False
if "VERSION" not in value:
get_logger().log_error(f"VERSION is not in the output value: {value}")
valid = False
return valid

View File

@@ -0,0 +1,17 @@
from keywords.k8s.k8s_table_parser_base import K8sTableParserBase
class KubectlNodesTableParser(K8sTableParserBase):
"""
Class for parsing the output of "kubectl get nodes" command.
"""
def __init__(self, k8s_output: str):
"""
Constructor
Args:
k8s_output(str): The raw String output of a kubernetes command that returns a table.
"""
super().__init__(k8s_output)
self.possible_headers = ["NAME", "STATUS", "ROLES", "AGE", "VERSION"]

View File

@@ -86,6 +86,20 @@ class KubectlGetPodsKeywords(BaseKeyword):
return pods_list_output return pods_list_output
def get_unhealthy_pods(self) -> KubectlGetPodsOutput:
"""
Get the k8s pods that are unhealthy
Returns:
KubectlGetPodsOutput: An object containing the parsed output of the command.
"""
field_selector = "status.phase!=Running,status.phase!=Succeeded"
kubectl_get_pods_output = self.ssh_connection.send(export_k8s_config(f"kubectl get pods --all-namespaces --field-selector={field_selector}"))
self.validate_success_return_code(self.ssh_connection)
pods_list_output = KubectlGetPodsOutput(kubectl_get_pods_output)
return pods_list_output
def wait_for_pod_max_age(self, pod_name: str, max_age: int, namespace: str = None, timeout: int = 600, check_interval: int = 20) -> bool: def wait_for_pod_max_age(self, pod_name: str, max_age: int, namespace: str = None, timeout: int = 600, check_interval: int = 20) -> bool:
""" """
Wait for the pod to be in a certain max_age. Wait for the pod to be in a certain max_age.