From 2c5ad3f123d1fbffb76143846ee476e8a3ef4117 Mon Sep 17 00:00:00 2001 From: Rajshekar P <31881341+rajshekarp87@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:35:04 +0530 Subject: [PATCH] [idrac_system_erase] - Erase of system and storage components (#729) * Initial code of system component erase * Addressed doc review comments --------- Co-authored-by: Sachin Apagundi <62133262+sachin-apa@users.noreply.github.com> Co-authored-by: Shivam Sharma --- docs/README.md | 8 +- docs/modules/idrac_diagnostics.rst | 4 +- docs/modules/idrac_license.rst | 4 +- docs/modules/idrac_system_erase.rst | 188 ++++++ plugins/README.md | 1 + plugins/modules/idrac_diagnostics.py | 4 +- plugins/modules/idrac_license.py | 4 +- plugins/modules/idrac_system_erase.py | 574 ++++++++++++++++++ .../modules/test_idrac_system_erase.py | 569 +++++++++++++++++ 9 files changed, 1346 insertions(+), 10 deletions(-) create mode 100644 docs/modules/idrac_system_erase.rst create mode 100644 plugins/modules/idrac_system_erase.py create mode 100644 tests/unit/plugins/modules/test_idrac_system_erase.py diff --git a/docs/README.md b/docs/README.md index 5c50f4b50..edd8a6be4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -24,19 +24,23 @@ You may obtain a copy of the License at | [idrac_diagnostics](modules/idrac_diagnostics.rst) | ✕ | ✓ | | [idrac_firmware](modules/idrac_firmware.rst) | ✓ | ✓ | | [idrac_firmware_info](modules/idrac_firmware_info.rst) | ✓ | ✓ | -| [idrac_lifecycle_controller_jobs](modules/idrac_lifecycle_controller_jobs.rst) | ✓ | ✓ | +| [idrac_license](modules/idrac_license.rst) | ✕ | ✓ | | [idrac_lifecycle_controller_job_status_info](modules/idrac_lifecycle_controller_job_status_info.rst) | ✓ | ✓ | +| [idrac_lifecycle_controller_jobs](modules/idrac_lifecycle_controller_jobs.rst) | ✓ | ✓ | | [idrac_lifecycle_controller_logs](modules/idrac_lifecycle_controller_logs.rst) | ✓ | ✓ | | [idrac_lifecycle_controller_status_info](modules/idrac_lifecycle_controller_status_info.rst) | ✓ | ✓ | -| [idrac_network_attributes](modules/idrac_network_attributes.rst) | ✓ | ✓ | | [idrac_network](modules/idrac_network.rst) | ✓ | ✓ | +| [idrac_network_attributes](modules/idrac_network_attributes.rst) | ✓ | ✓ | | [idrac_os_deployment](modules/idrac_os_deployment.rst) | ✓ | ✓ | | [idrac_redfish_storage_controller](modules/idrac_redfish_storage_controller.rst) | ✕ | ✓ | | [idrac_reset](modules/idrac_reset.rst) | ✓ | ✓ | +| [idrac_secure_boot](modules/idrac_secure_boot.rst) | ✕ | ✓ | | [idrac_server_config_profile](modules/idrac_server_config_profile.rst) | ✓ | ✓ | | [idrac_session](modules/idrac_session.rst) | ✓ | ✓ | | [idrac_storage_volume](modules/idrac_storage_volume.rst) | ✓ | ✓ | +| [idrac_support_assist](modules/idrac_support_assists.rst) | ✕ | ✓ | | [idrac_syslog](modules/idrac_syslog.rst) | ✓ | ✓ | +| [idrac_system_erase](modules/idrac_system_erase.rst) | ✕ | ✓ | | [idrac_system_info](modules/idrac_system_info.rst) | ✓ | ✓ | | [idrac_timezone_ntp](modules/idrac_timezone_ntp.rst) | ✓ | ✓ | | [idrac_user](modules/idrac_user.rst) | ✓ | ✓ | diff --git a/docs/modules/idrac_diagnostics.rst b/docs/modules/idrac_diagnostics.rst index ba5831ab7..5158167b0 100644 --- a/docs/modules/idrac_diagnostics.rst +++ b/docs/modules/idrac_diagnostics.rst @@ -150,9 +150,9 @@ Parameters ignore_certificate_warning (optional, str, off) Ignores the certificate warning while connecting to Share and is only applicable when \ :emphasis:`share\_type`\ is \ :literal:`https`\ . - \ :literal:`off`\ ignores the certificate warning. + \ :literal:`on`\ ignores the certificate warning. - \ :literal:`on`\ does not ignore the certificate warning. + \ :literal:`off`\ does not ignore the certificate warning. proxy_support (optional, str, off) diff --git a/docs/modules/idrac_license.rst b/docs/modules/idrac_license.rst index 6dd16768c..e6adfd3d1 100644 --- a/docs/modules/idrac_license.rst +++ b/docs/modules/idrac_license.rst @@ -116,9 +116,9 @@ Parameters ignore_certificate_warning (optional, str, off) Ignores the certificate warning while connecting to Share and is only applicable when \ :emphasis:`share\_type`\ is \ :literal:`https`\ . - \ :literal:`off`\ ignores the certificate warning. + \ :literal:`on`\ ignores the certificate warning. - \ :literal:`on`\ does not ignore the certificate warning. + \ :literal:`off`\ does not ignore the certificate warning. proxy_support (optional, str, off) diff --git a/docs/modules/idrac_system_erase.rst b/docs/modules/idrac_system_erase.rst new file mode 100644 index 000000000..bbc75978d --- /dev/null +++ b/docs/modules/idrac_system_erase.rst @@ -0,0 +1,188 @@ +.. _idrac_system_erase_module: + + +idrac_system_erase -- Erase system and storage components of the server +======================================================================= + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- + +This module allows you to erase system components such as iDRAC, BIOS, DIAG, and so forth. You can also erase storage components such as PERC NV cache, non-volatile memory, cryptographic erase of physical disks, and so on of the server + + + +Requirements +------------ +The below requirements are needed on the host that executes this module. + +- python \>= 3.9.6 + + + +Parameters +---------- + + component (True, list, None) + List of system and storage components that can be deleted. + + The following are the supported components. AllApps BIOS CryptographicErasePD DIAG DPU DrvPack IDRAC LCData NonVolatileMemory OverwritePD PERCNVCache ReinstallFW vFlash + + + power_on (optional, bool, False) + This parameter allows you to power on the server after the erase operation is completed. This is applicable when :emphasis:`job\_wait` is :literal:`true`. + + :literal:`true` power on the server. + + :literal:`false` does not power on the server. + + + job_wait (optional, bool, True) + Whether to wait till completion of the job. This is applicable when :emphasis:`power\_on` is :literal:`true`. + + :literal:`true` waits for job completion. + + :literal:`false` does not wait for job completion. + + + job_wait_timeout (optional, int, 1200) + The maximum wait time of :emphasis:`job\_wait` in seconds. The job is tracked only for this duration. + + This option is applicable when :emphasis:`job\_wait` is :literal:`true`. + + + resource_id (optional, str, None) + Manager ID of the iDRAC. + + + idrac_ip (True, str, None) + iDRAC IP Address. + + + idrac_user (False, str, None) + iDRAC username. + + If the username is not provided, then the environment variable :envvar:`IDRAC\_USERNAME` is used. + + Example: export IDRAC\_USERNAME=username + + + idrac_password (False, str, None) + iDRAC user password. + + If the password is not provided, then the environment variable :envvar:`IDRAC\_PASSWORD` is used. + + Example: export IDRAC\_PASSWORD=password + + + x_auth_token (False, str, None) + Authentication token. + + If the x\_auth\_token is not provided, then the environment variable :envvar:`IDRAC\_X\_AUTH\_TOKEN` is used. + + Example: export IDRAC\_X\_AUTH\_TOKEN=x\_auth\_token + + + idrac_port (optional, int, 443) + iDRAC port. + + + validate_certs (optional, bool, True) + If :literal:`false`\ , the SSL certificates will not be validated. + + Configure :literal:`false` only on personally controlled sites where self-signed certificates are used. + + Prior to collection version :literal:`5.0.0`\ , the :emphasis:`validate\_certs` is :literal:`false` by default. + + + ca_path (optional, path, None) + The Privacy Enhanced Mail (PEM) file that contains a CA certificate to be used for the validation. + + + timeout (optional, int, 30) + The socket level timeout in seconds. + + + + + +Notes +----- + +.. note:: + - Run this module from a system that has direct access to Dell iDRAC. + - This module supports only iDRAC9 and above. + - This module supports IPv4 and IPv6 addresses. + + + + +Examples +-------- + +.. code-block:: yaml+jinja + + + --- + - name: Erase a single component and power on the server + dellemc.openmanage.idrac_system_erase: + idrac_ip: 198.162.0.1 + idrac_user: username + idrac_password: passw0rd + ca_path: "/path/to/ca_cert.pem" + component: ["BIOS"] + power_on: true + + - name: Erase multiple components and do not power on the server after the erase operation is completed + dellemc.openmanage.idrac_system_erase: + idrac_ip: 198.162.0.1 + idrac_user: username + idrac_password: passw0rd + ca_path: "/path/to/ca_cert.pem" + component: ["BIOS", "DIAG", "PERCNVCache"] + + - name: Erase multiple components and do not wait for the job completion + dellemc.openmanage.idrac_system_erase: + idrac_ip: 198.162.0.1 + idrac_user: username + idrac_password: passw0rd + ca_path: "/path/to/ca_cert.pem" + component: ["IDRAC", "DPU", "LCData"] + job_wait: false + + + +Return Values +------------- + +msg (always, str, Successfully completed the system erase operation.) + Status of the component system erase operation. + + +job_details (For system erase operation, dict, {'ActualRunningStartTime': None, 'ActualRunningStopTime': None, 'CompletionTime': '2024-08-06T19:55:01', 'Description': 'Job Instance', 'EndTime': 'TIME_NA', 'Id': 'JID_229917427823', 'JobState': 'Completed', 'JobType': 'SystemErase', 'Message': 'Job completed successfully.', 'MessageArgs': [], 'MessageArgs@odata.count': 0, 'MessageId': 'SYS018', 'Name': 'System_Erase', 'PercentComplete': 100, 'StartTime': '2024-08-06T19:49:02', 'TargetSettingsURI': None}) + Returns the output for status of the job. + + +error_info (On HTTP error, dict, {'error': {'@Message.ExtendedInfo': [{'Message': 'Unable to complete the operation because the value NonVolatileMemor entered for the property Component is not in the list of acceptable values.', 'MessageArgs': ['NonVolatileMemor', 'Component'], 'MessageArgs@odata.count': 2, 'MessageId': 'IDRAC.2.9.SYS426', 'RelatedProperties': [], 'RelatedProperties@odata.count': 0, 'Resolution': "Enter a valid value from the enumeration list that Redfish service supports and retry the operation.For information about valid values, see the iDRAC User's Guide available on the support site.", 'Severity': 'Warning'}, {'Message': "The value 'NonVolatileMemor' for the property Component is not in the list of acceptable values.", 'MessageArgs': ['NonVolatileMemor', 'Component'], 'MessageArgs@odata.count': 2, 'MessageId': 'Base.1.12.PropertyValueNotInList', 'RelatedProperties': [], 'RelatedProperties@odata.count': 0, 'Resolution': 'Choose a value from the enumeration list that the implementation can support and resubmit the request if the operation failed.', 'Severity': 'Warning'}], 'code': 'Base.1.12.GeneralError', 'message': 'A general error has occurred. See ExtendedInfo for more information'}}) + Details of the HTTP Error. + + + + + +Status +------ + + + + + +Authors +~~~~~~~ + +- Rajshekar P(@rajshekarp87) + diff --git a/plugins/README.md b/plugins/README.md index c23b24311..e48c595d1 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -47,6 +47,7 @@ Here are the list of modules and module_utils supported by Dell. ├── idrac_storage_volume.py ├── idrac_support_assist.py ├── idrac_syslog.py + ├── idrac_system_erase.py ├── idrac_system_info.py ├── idrac_timezone_ntp.py ├── idrac_user.py diff --git a/plugins/modules/idrac_diagnostics.py b/plugins/modules/idrac_diagnostics.py index e19c0107b..0cc2366a0 100644 --- a/plugins/modules/idrac_diagnostics.py +++ b/plugins/modules/idrac_diagnostics.py @@ -130,8 +130,8 @@ ignore_certificate_warning: description: - Ignores the certificate warning while connecting to Share and is only applicable when I(share_type) is C(https). - - C(off) ignores the certificate warning. - - C(on) does not ignore the certificate warning. + - C(on) ignores the certificate warning. + - C(off) does not ignore the certificate warning. type: str choices: ["off", "on"] default: "off" diff --git a/plugins/modules/idrac_license.py b/plugins/modules/idrac_license.py index 1bf0e2b3c..0744e3648 100644 --- a/plugins/modules/idrac_license.py +++ b/plugins/modules/idrac_license.py @@ -102,8 +102,8 @@ ignore_certificate_warning: description: - Ignores the certificate warning while connecting to Share and is only applicable when I(share_type) is C(https). - - C(off) ignores the certificate warning. - - C(on) does not ignore the certificate warning. + - C(on) ignores the certificate warning. + - C(off) does not ignore the certificate warning. type: str choices: ["off", "on"] default: "off" diff --git a/plugins/modules/idrac_system_erase.py b/plugins/modules/idrac_system_erase.py new file mode 100644 index 000000000..bcbed9c25 --- /dev/null +++ b/plugins/modules/idrac_system_erase.py @@ -0,0 +1,574 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# +# Dell OpenManage Ansible Modules +# Version 9.7.0 +# Copyright (C) 2024 Dell Inc. or its subsidiaries. All Rights Reserved. + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: idrac_system_erase +short_description: Erase system and storage components of the server +version_added: "9.7.0" +description: + - This module allows you to erase system components such as iDRAC, BIOS, DIAG, + and so forth. You can also erase storage components such as PERC NV cache, + non-volatile memory, cryptographic erase of physical disks, and so on of the server +extends_documentation_fragment: + - dellemc.openmanage.idrac_x_auth_options +options: + component: + description: + - List of system and storage components that can be deleted. + - The following are the supported components. + AllApps + BIOS + CryptographicErasePD + DIAG + DPU + DrvPack + IDRAC + LCData + NonVolatileMemory + OverwritePD + PERCNVCache + ReinstallFW + vFlash + type: list + elements: str + required: true + power_on: + description: + - This parameter allows you to power on the server after the erase operation is completed. This is applicable when I(job_wait) is C(true). + - C(true) power on the server. + - C(false) does not power on the server. + type: bool + default: false + job_wait: + description: + - Whether to wait till completion of the job. This is applicable when I(power_on) is C(true). + - C(true) waits for job completion. + - C(false) does not wait for job completion. + type: bool + default: true + job_wait_timeout: + description: + - The maximum wait time of I(job_wait) in seconds. The job is tracked only for this duration. + - This option is applicable when I(job_wait) is C(true). + type: int + default: 1200 + resource_id: + description: + - Manager ID of the iDRAC. + type: str +requirements: + - "python >= 3.9.6" +author: + - "Rajshekar P(@rajshekarp87)" +attributes: + check_mode: + description: Runs task to validate without performing action on the target machine. + support: full + diff_mode: + description: Runs the task to report the changes made or to be made. + support: none +notes: + - Run this module from a system that has direct access to Dell iDRAC. + - This module supports only iDRAC9 and above. + - This module supports IPv4 and IPv6 addresses. +""" + +EXAMPLES = r""" +--- +- name: Erase a single component and power on the server + dellemc.openmanage.idrac_system_erase: + idrac_ip: 198.162.0.1 + idrac_user: username + idrac_password: passw0rd + ca_path: "/path/to/ca_cert.pem" + component: ["BIOS"] + power_on: true + +- name: Erase multiple components and do not power on the server after the erase operation is completed + dellemc.openmanage.idrac_system_erase: + idrac_ip: 198.162.0.1 + idrac_user: username + idrac_password: passw0rd + ca_path: "/path/to/ca_cert.pem" + component: ["BIOS", "DIAG", "PERCNVCache"] + +- name: Erase multiple components and do not wait for the job completion + dellemc.openmanage.idrac_system_erase: + idrac_ip: 198.162.0.1 + idrac_user: username + idrac_password: passw0rd + ca_path: "/path/to/ca_cert.pem" + component: ["IDRAC", "DPU", "LCData"] + job_wait: false +""" + +RETURN = r''' +--- +msg: + description: Status of the component system erase operation. + returned: always + type: str + sample: "Successfully completed the system erase operation." +job_details: + description: Returns the output for status of the job. + returned: For system erase operation + type: dict + sample: { + "ActualRunningStartTime": null, + "ActualRunningStopTime": null, + "CompletionTime": "2024-08-06T19:55:01", + "Description": "Job Instance", + "EndTime": "TIME_NA", + "Id": "JID_229917427823", + "JobState": "Completed", + "JobType": "SystemErase", + "Message": "Job completed successfully.", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "SYS018", + "Name": "System_Erase", + "PercentComplete": 100, + "StartTime": "2024-08-06T19:49:02", + "TargetSettingsURI": null + } +error_info: + description: Details of the HTTP Error. + returned: On HTTP error + type: dict + sample: { + "error": { + "@Message.ExtendedInfo": [ + { + "Message": "Unable to complete the operation because the value NonVolatileMemor entered for the property Component is not in the list + of acceptable values.", + "MessageArgs": [ + "NonVolatileMemor", + "Component" + ], + "MessageArgs@odata.count": 2, + "MessageId": "IDRAC.2.9.SYS426", + "RelatedProperties": [], + "RelatedProperties@odata.count": 0, + "Resolution": "Enter a valid value from the enumeration list that Redfish service supports and retry the operation.For information + about valid values, see the iDRAC User's Guide available on the support site.", + "Severity": "Warning" + }, + { + "Message": "The value 'NonVolatileMemor' for the property Component is not in the list of acceptable values.", + "MessageArgs": [ + "NonVolatileMemor", + "Component" + ], + "MessageArgs@odata.count": 2, + "MessageId": "Base.1.12.PropertyValueNotInList", + "RelatedProperties": [], + "RelatedProperties@odata.count": 0, + "Resolution": "Choose a value from the enumeration list that the implementation can support and resubmit the request if the operation + failed.", + "Severity": "Warning" + } + ], + "code": "Base.1.12.GeneralError", + "message": "A general error has occurred. See ExtendedInfo for more information" + } + } +''' + + +import json +from urllib.error import HTTPError, URLError +from ansible_collections.dellemc.openmanage.plugins.module_utils.idrac_redfish import iDRACRedfishAPI, IdracAnsibleModule +from ansible.module_utils.urls import ConnectionError, SSLValidationError +from ansible_collections.dellemc.openmanage.plugins.module_utils.utils import ( + get_dynamic_uri, trigger_restart_operation, validate_and_get_first_resource_id_uri, remove_key, idrac_redfish_job_tracking) + + +MANAGERS_URI = "/redfish/v1/Managers" +IDRAC_JOB_URI = "{res_uri}/Jobs/{job_id}" +ODATA = "@odata.id" +ODATA_REGEX = "(.*?)@odata" + +OEM = "Oem" +MANUFACTURER = "Dell" +LC_SERVICE = "DellLCService" +ACTIONS = "Actions" +SYSTEM_ERASE = "DellLCService.SystemErase" +SYSTEM_ERASE_FETCH = "#DellLCService.SystemErase" +COMPONENT_ALLOWABLE_VALUES = "Component@Redfish.AllowableValues" +JOB_FILTER = "Jobs?$expand=*($levels=1)" + +ERASE_SUCCESS_COMPLETION_MSG = "Successfully completed the system erase operation." +ERASE_SUCCESS_SCHEDULED_MSG = "Successfully submitted the job for system erase operation." +ERASE_SUCCESS_POWER_ON_MSG = "Successfully completed the system erase operation and powered on " \ + "the server." +NO_COMPONENT_MATCH = "Unable to complete the operation because the value entered for the " \ + "'component' is not in the list of acceptable values." +TIMEOUT_NEGATIVE_OR_ZERO_MSG = "The value for the 'job_wait_timeout' parameter cannot be " \ + "negative or zero." +WAIT_TIMEOUT_MSG = "The job is not complete after {0} seconds." +INVALID_COMPONENT_WARN_MSG = "Erase operation is not performed on these components - " \ + "{unmatching_components_str_format} as they are either invalid or " \ + "inapplicable." +FAILURE_MSG = "Unable to complete the system erase operation." +CHANGES_FOUND_MSG = "Changes found to be applied." +NO_CHANGES_FOUND_MSG = "No changes found to be applied." + + +class SystemErase(): + def __init__(self, idrac, module): + """ + Initializes the class instance with the provided idrac and module parameters. + + :param idrac: The idrac parameter. + :type idrac: Any + :param module: The module parameter. + :type module: Any + """ + self.idrac = idrac + self.module = module + + def execute(self): + """ + Executes the function with the given module. + + :param module: The module to execute. + :type module: Any + :return: None + """ + + def get_system_erase_url(self): + """ + Retrieves the URL for the system erase operation. + + Returns: + str: The URL for the system erase operation. + """ + url = self.get_url() + resp = get_dynamic_uri(self.idrac, url) + system_erase_url = ( + resp.get('Links', {}) + .get(OEM, {}) + .get(MANUFACTURER, {}) + .get(LC_SERVICE, {}) + .get(ODATA, {}) + ) + return system_erase_url + + def get_url(self): + """ + Retrieves the URL for the resource. + + Returns: + str: The URL for the resource. + + Raises: + AnsibleExitJson: If the resource ID is not found. + """ + res_id = self.module.params.get('resource_id') + if res_id: + uri = MANAGERS_URI + "/" + res_id + else: + uri, error_msg = validate_and_get_first_resource_id_uri( + self.module, self.idrac, MANAGERS_URI) + if error_msg: + self.module.exit_json(msg=error_msg, failed=True) + return uri + + def get_job_status(self, erase_component_response): + """ + Retrieves the status of a job. + + Args: + erase_component_response (object): The response object for the erase component + operation. + + Returns: + dict: The job details. + + Raises: + AnsibleExitJson: If the job tracking times out. + """ + job_wait_timeout = self.module.params.get('job_wait_timeout') + res_uri = validate_and_get_first_resource_id_uri(self.module, self.idrac, MANAGERS_URI) + job_tracking_uri = erase_component_response.headers.get("Location") + job_id = job_tracking_uri.split("/")[-1] + job_uri = IDRAC_JOB_URI.format(job_id=job_id, res_uri=res_uri[0]) + job_failed, msg, job_dict, wait_time = idrac_redfish_job_tracking( + self.idrac, job_uri, max_job_wait_sec=job_wait_timeout) + job_dict = remove_key(job_dict, regex_pattern=ODATA_REGEX) + if int(wait_time) >= int(job_wait_timeout): + self.module.exit_json(msg=WAIT_TIMEOUT_MSG.format( + job_wait_timeout), changed=True, job_status=job_dict) + if job_failed: + self.module.exit_json( + msg=job_dict.get('Message'), + failed=True, + job_details=job_dict) + return job_dict + + def get_job_details(self, erase_component_response): + """ + Retrieves the details of a job. + + Args: + erase_component_response (object): The response object for the erase component + operation. + + Returns: + dict: The job details. + + Raises: + None. + """ + res_uri = validate_and_get_first_resource_id_uri(self.module, self.idrac, MANAGERS_URI) + job_tracking_uri = erase_component_response.headers.get("Location") + job_id = job_tracking_uri.split("/")[-1] + job_uri = IDRAC_JOB_URI.format(job_id=job_id, res_uri=res_uri[0]) + job_response = self.idrac.invoke_request(job_uri, 'GET') + job_details = job_response.json_data + job_details = remove_key(job_details, regex_pattern=ODATA_REGEX) + return job_details + + def check_system_erase_job(self): + """ + Retrieves the state of the most recent SystemErase job. + + Returns: + str: The state of the most recent SystemErase job. Possible values are: + - "New" + - "Scheduling" + - "Running" + - "Completed" + - "Failed" + - "Unknown" if no SystemErase job is found. + """ + url = self.get_url() + job_details_url = url + f"/{JOB_FILTER}" + job_resp = self.idrac.invoke_request(job_details_url, "GET") + job_list = job_resp.json_data.get('Members', []) + job_list_reversed = list(reversed(job_list)) + jb_state = 'Unknown' + for jb in job_list_reversed: + if jb.get("JobType") == "SystemErase" and jb.get("JobState") in [ + "New", "Scheduling", "Running", "Completed", "Failed"]: + jb_state = jb.get("JobState") + break + return jb_state + + def validate_job_wait(self): + """ + Validates job_wait and job_wait_timeout parameters. + """ + if self.module.params.get('job_wait') and self.module.params.get('job_wait_timeout') <= 0: + self.module.exit_json( + msg=TIMEOUT_NEGATIVE_OR_ZERO_MSG, failed=True) + + def check_allowable_value(self, component): + """ + Check if the given component values are in the allowable values. + + Args: + component (list): A list of component values. + + Returns: + tuple: A tuple containing two lists. The first list contains the matching values, and + the second list contains the unmatching values. + + Raises: + None. + """ + sytem_erase_url = self.get_system_erase_url() + system_erase_response = self.idrac.invoke_request(sytem_erase_url, "GET") + allowable_values = system_erase_response.json_data[ACTIONS][SYSTEM_ERASE_FETCH][ + COMPONENT_ALLOWABLE_VALUES] + actual_values = component + matching_values = [] + unmatching_values = [] + for value in actual_values: + if value in allowable_values: + matching_values.append(value) + else: + unmatching_values.append(value) + if len(matching_values) == 0 and not self.module.check_mode: + self.module.exit_json(msg=NO_COMPONENT_MATCH, skipped=True) + if len(unmatching_values) > 0 and self.module.check_mode: + if len(matching_values) > 0 : + self.module.exit_json(msg=CHANGES_FOUND_MSG, changed=True) + else: + self.module.exit_json(msg=NO_CHANGES_FOUND_MSG, changed=False) + return matching_values, unmatching_values + + def warn_unmatching_components(self, unmatching_components): + """ + Warn the user about unmatching components. + + Args: + unmatching_components (list): A list of unmatching components. + + Returns: + None + """ + if len(unmatching_components) > 0: + unmatching_components_str_format = ", '".join( + item + "'" for item in unmatching_components) + self.module.warn(INVALID_COMPONENT_WARN_MSG.format( + unmatching_components_str_format=unmatching_components_str_format)) + + +class EraseComponent(SystemErase): + """ + Class to erase component and perform all the operations + """ + STATUS_SUCCESS = [202] + JOB_STATUS = ["New", "Scheduling", "Running"] + + def execute(self): + """ + Executes the system erase operation. + + This function checks if the job_wait parameter is set and validates the job wait. + It also checks if the module is in check mode and checks the state of the system erase job. + If the job state is in the JOB_STATUS list, it exits with a message indicating no changes + found. + If the job state is not in the JOB_STATUS list, it exits with a message indicating changes + found. + + The function then creates a payload dictionary and assigns the matching components to + the 'Component' key. + It retrieves the system erase URL and sends a POST request to initiate the erase operation. + The response status code is checked and if it is in the STATUS_SUCCESS list, it proceeds to + handle the job. + If the job_wait parameter is set and the power_on parameter is also set, it retrieves the + job status, performs the power on operation, warns about unmatching components, and exits + with a message indicating the erase operation was successful and the job details. + If the job_wait parameter is set but the power_on parameter is not set, it retrieves the + job status, warns about unmatching components, and exits with a message indicating the + erase operation was successful and the job details. + If the job_wait parameter is not set, it retrieves the job details, warns about unmatching + components, and exits with a message indicating the erase operation was scheduled and the + job details. + If the response status code is not in the STATUS_SUCCESS list, it retrieves the job status + and exits with a message indicating the operation failed. + + Returns: + None + """ + if self.module.params.get('job_wait'): + self.validate_job_wait() + component_list = self.module.params.get('component') + matching_components, unmatching_components = self.check_allowable_value(component_list) + if self.module.check_mode: + job_state = self.check_system_erase_job() + if job_state in self.JOB_STATUS: + self.module.exit_json(msg=NO_CHANGES_FOUND_MSG, changed=False) + else: + self.module.exit_json(msg=CHANGES_FOUND_MSG, changed=True) + payload = {} + payload["Component"] = matching_components + system_erase_url = self.get_system_erase_url() + erase_component_response = self.idrac.invoke_request( + system_erase_url + f"/{ACTIONS}/{SYSTEM_ERASE}", "POST", data=payload) + status = erase_component_response.status_code + if status in self.STATUS_SUCCESS: + if self.module.params.get('job_wait'): + if self.module.params.get('power_on'): + job_dict = self.get_job_status(erase_component_response) + trigger_restart_operation(self.idrac, restart_type="On") + self.warn_unmatching_components(unmatching_components) + self.module.exit_json(msg=ERASE_SUCCESS_POWER_ON_MSG, changed=True, + job_details=job_dict) + else: + job_dict = self.get_job_status(erase_component_response) + self.warn_unmatching_components(unmatching_components) + self.module.exit_json(msg=ERASE_SUCCESS_COMPLETION_MSG, changed=True, + job_details=job_dict) + else: + job_dict = self.get_job_details(erase_component_response) + self.warn_unmatching_components(unmatching_components) + self.module.exit_json(msg=ERASE_SUCCESS_SCHEDULED_MSG, changed=False, + job_details=job_dict) + else: + job_dict = self.get_job_status(erase_component_response) + self.module.exit_json(msg=FAILURE_MSG, failed=True, job_details=job_dict) + + +def main(): + """ + Executes the main function of the program. + + This function initializes the necessary modules and arguments, and then + creates an instance of the iDRACRedfishAPI class. It then creates an + instance of the EraseComponent class and calls its execute method. + + If any exceptions are raised during the execution of the code, the + appropriate error message is returned. + + Parameters: + None + + Returns: + None + """ + specs = get_argument_spec() + module = IdracAnsibleModule( + argument_spec=specs, + supports_check_mode=True + ) + try: + with iDRACRedfishAPI(module.params) as idrac: + system_erase_obj = EraseComponent(idrac, module) + system_erase_obj.execute() + except HTTPError as err: + filter_err = remove_key(json.load(err), regex_pattern=ODATA_REGEX) + module.exit_json(msg=str(err), error_info=filter_err, failed=True) + except URLError as err: + module.exit_json(msg=str(err), unreachable=True) + except (SSLValidationError, ConnectionError, TypeError, ValueError, OSError) as err: + module.exit_json(msg=str(err), failed=True) + + +def get_argument_spec(): + """ + Returns a dictionary specifying the argument specification for the function. + + Parameters: + None + + Returns: + dict: A dictionary containing the argument specification. + The dictionary has the following keys: + - "component": A dictionary specifying the type and required status of the + "component" argument. + - "power_on": A dictionary specifying the type and default value of the "power_on" + argument. + - "job_wait": A dictionary specifying the type and default value of the "job_wait" + argument. + - "job_wait_timeout": A dictionary specifying the type and default value of the + "job_wait_timeout" argument. + - "resource_id": A dictionary specifying the type of the "resource_id" argument. + """ + return { + "component": {"type": 'list', "elements": 'str', "required": True}, + "power_on": {"type": 'bool', "default": False}, + "job_wait": {"type": 'bool', "default": True}, + "job_wait_timeout": {"type": 'int', "default": 1200}, + "resource_id": {"type": 'str'} + } + + +if __name__ == '__main__': + main() diff --git a/tests/unit/plugins/modules/test_idrac_system_erase.py b/tests/unit/plugins/modules/test_idrac_system_erase.py new file mode 100644 index 000000000..98f025977 --- /dev/null +++ b/tests/unit/plugins/modules/test_idrac_system_erase.py @@ -0,0 +1,569 @@ +# -*- coding: utf-8 -*- + +# +# Dell OpenManage Ansible Modules +# Version 9.7.0 +# Copyright (C) 2024 Dell Inc. or its subsidiaries. All Rights Reserved. + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + +import pytest +from unittest.mock import patch, MagicMock +from urllib.error import HTTPError, URLError +from ansible.module_utils._text import to_text +from io import StringIO +import json +from ansible.module_utils.urls import ConnectionError, SSLValidationError +from ansible_collections.dellemc.openmanage.plugins.modules import idrac_system_erase +from ansible_collections.dellemc.openmanage.plugins.modules.idrac_system_erase import SystemErase, main +from ansible_collections.dellemc.openmanage.tests.unit.plugins.modules.common import FakeAnsibleModule + +MODULE_PATH = 'ansible_collections.dellemc.openmanage.plugins.modules.idrac_system_erase.' +MODULE_UTILS_PATH = 'ansible_collections.dellemc.openmanage.plugins.module_utils.utils.' +HTTPS_PATH = 'HTTPS_PATH/job_tracking/12345' + +MANAGERS_URI = "/redfish/v1/Managers" +MANAGER_URI_ONE = "/redfish/v1/managers/1" +REDFISH_MOCK_URL = "/redfish/v1/Managers/1234" +MANAGER_URI_RESOURCE = "/redfish/v1/Managers/iDRAC.Embedded.1" +IDRAC_JOB_URI = "{res_uri}/Jobs/{job_id}" +ODATA = "@odata.id" +ODATA_REGEX = "(.*?)@odata" +OEM = "Oem" +MANUFACTURER = "Dell" +LC_SERVICE = "DellLCService" +ACTIONS = "Actions" +SYSTEM_ERASE = "DellLCService.SystemErase" +SYSTEM_ERASE_FETCH = "#DellLCService.SystemErase" +SYSTEM_ERASE_URL = "SystemErase.get_system_erase_url" +SYSTEM_ERASE_JOB_STATUS = "SystemErase.get_job_status" +COMPONENT_ALLOWABLE_VALUES = "Component@Redfish.AllowableValues" +JOB_FILTER = "Jobs?$expand=*($levels=1)" +API_ONE = "/local/action" +API_INVOKE_MOCKER = "iDRACRedfishAPI.invoke_request" +ALLOWABLE_VALUE_FUNC = "SystemErase.check_allowable_value" +REDFISH_API = "iDRACRedfishAPI" +HTTPS_PATH = "https://testhost.com" +HTTP_ERROR = "http error message" +APPLICATION_JSON = "application/json" +MESSAGE_EXTENDED = "@Message.ExtendedInfo" + +ERASE_SUCCESS_COMPLETION_MSG = "Successfully completed the system erase operation." +ERASE_SUCCESS_SCHEDULED_MSG = "Successfully submitted the job for system erase operation." +ERASE_SUCCESS_POWER_ON_MSG = "Successfully completed the system erase operation and powered on " \ + "the server." +NO_COMPONENT_MATCH = "Unable to complete the operation because the value entered for the " \ + "'component' is not in the list of acceptable values." +TIMEOUT_NEGATIVE_OR_ZERO_MSG = "The value for the 'job_wait_timeout' parameter cannot be " \ + "negative or zero." +WAIT_TIMEOUT_MSG = "The job is not complete after {0} seconds." +INVALID_COMPONENT_WARN_MSG = "Erase operation is not performed on these components - " \ + "{unmatching_components_str_format} as they are either invalid or " \ + "inapplicable." +FAILURE_MSG = "Unable to complete the system erase operation." +CHANGES_FOUND_MSG = "Changes found to be applied." +NO_CHANGES_FOUND_MSG = "No changes found to be applied." + + +class TestSystemErase(FakeAnsibleModule): + module = idrac_system_erase + + @pytest.fixture + def idrac_system_erase_mock(self): + idrac_obj = MagicMock() + return idrac_obj + + @pytest.fixture + def idrac_connection_system_erase_mock(self, mocker, idrac_system_erase_mock): + idrac_conn_mock = mocker.patch(MODULE_PATH + REDFISH_API, + return_value=idrac_system_erase_mock) + idrac_conn_mock.return_value.__enter__.return_value = idrac_system_erase_mock + return idrac_conn_mock + + @pytest.fixture + def mock_module(self): + """Fixture for creating a mock Ansible module.""" + module_mock = MagicMock() + # Initialize with a dictionary + module_mock.params = {'resource_id': None} + return module_mock + + @pytest.fixture + def system_erase_obj(self, mock_module): + """Fixture for creating an instance of SystemErase with mocks.""" + idrac_mock = MagicMock() + return SystemErase(idrac_mock, mock_module) + + def test_get_url_with_resource_id(self, system_erase_obj, mock_module): + """Test get_url when 'resource_id' is provided.""" + mock_module.params['resource_id'] = 'iDRAC.Embedded.1' # Set resource_id directly + + expected_url = MANAGER_URI_RESOURCE + assert system_erase_obj.get_url() == expected_url + + @patch(MODULE_UTILS_PATH + 'validate_and_get_first_resource_id_uri') + @patch(MODULE_UTILS_PATH + 'get_dynamic_uri') + def test_get_url_without_resource_id_success(self, mock_get_dynamic_uri, mock_validate, system_erase_obj, mock_module): + """Test get_url when 'resource_id' is not provided and validation succeeds.""" + mock_module.params['resource_id'] = None # No resource_id + mock_validate.return_value = ( + MANAGER_URI_RESOURCE, None) # Mock URI and no error + mock_get_dynamic_uri.return_value = {'Members': [ + {'@odata.id': 'MANAGER_URI_RESOURCE'}]} # Mock a valid response + try: + result_url = system_erase_obj.get_url() + expected_url = MANAGER_URI_RESOURCE + assert result_url == expected_url + mock_validate.assert_called_once_with( + mock_module, system_erase_obj.idrac, "/redfish/v1/Managers") + except Exception as e: + print("Error occurred:", str(e)) + + def test_get_job_status_success(self, mocker, idrac_system_erase_mock): + # Mocking necessary objects and functions + module_mock = self.get_module_mock() + system_erase_job_response_mock = mocker.MagicMock() + system_erase_job_response_mock.headers.get.return_value = HTTPS_PATH + module_mock.params['wait_time'] = 10 + module_mock.params['job_wait_timeout'] = 100 + + mocker.patch(MODULE_PATH + "remove_key", + return_value={"job_details": "mocked_job_details"}) + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", + return_value=[MANAGER_URI_ONE]) + + # Creating an instance of the class + obj_under_test = self.module.SystemErase( + idrac_system_erase_mock, module_mock) + + # Mocking the idrac_redfish_job_tracking function to simulate a successful job tracking + mocker.patch(MODULE_PATH + "idrac_redfish_job_tracking", return_value=( + False, "mocked_message", {"job_details": "mocked_job_details"}, 0)) + + # Calling the method under test + result = obj_under_test.get_job_status(system_erase_job_response_mock) + + # Assertions + assert result == {"job_details": "mocked_job_details"} + + def test_get_job_status_failure(self, mocker, idrac_system_erase_mock): + # Mocking necessary objects and functions + module_mock = self.get_module_mock() + system_erase_job_response_mock = mocker.MagicMock() + system_erase_job_response_mock.headers.get.return_value = HTTPS_PATH + module_mock.params['wait_time'] = 10 + module_mock.params['job_wait_timeout'] = 100 + + mocker.patch(MODULE_PATH + "remove_key", + return_value={"Message": "None"}) + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", + return_value=[MANAGER_URI_ONE]) + + # Creating an instance of the class + obj_under_test = self.module.SystemErase( + idrac_system_erase_mock, module_mock) + + # Mocking the idrac_redfish_job_tracking function to simulate a failed job tracking + mocker.patch(MODULE_PATH + "idrac_redfish_job_tracking", + return_value=(True, "None", {"Message": "None"}, 0)) + + # Mocking module.exit_json + exit_json_mock = mocker.patch.object(module_mock, "exit_json") + + # Calling the method under test + result = obj_under_test.get_job_status(system_erase_job_response_mock) + + # Assertions + exit_json_mock.assert_called_once_with( + msg="None", failed=True, job_details={"Message": "None"}) + assert result == {"Message": "None"} + + def test_get_details_status_success(self, mocker, idrac_system_erase_mock): + # Mocking necessary objects and functions + module_mock = self.get_module_mock() + system_erase_job_response_mock = mocker.MagicMock() + system_erase_job_response_mock.headers.get.return_value = HTTPS_PATH + + mocker.patch(MODULE_PATH + "remove_key", + return_value={"job_details": "mocked_job_details"}) + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", + return_value=[MANAGER_URI_ONE]) + + # Creating an instance of the class + obj_under_test = self.module.SystemErase( + idrac_system_erase_mock, module_mock) + + # Mocking the idrac_redfish_job_tracking function to simulate a successful job tracking + mocker.patch(MODULE_PATH + "idrac_redfish_job_tracking", return_value=( + False, "mocked_message", {"job_details": "mocked_job_details"}, 0)) + + # Calling the method under test + result = obj_under_test.get_job_details(system_erase_job_response_mock) + + # Assertions + assert result == {"job_details": "mocked_job_details"} + + @patch(MODULE_UTILS_PATH + 'get_dynamic_uri') + def test_get_system_erase_url(self, mock_get_dynamic_uri, system_erase_obj, mock_module): + """Test get_system_erase_url retrieves the correct URL for the system erase operation.""" + mock_module.params['resource_id'] = "iDRAC.Embedded.1" + + # Mock the response of get_dynamic_uri + mock_get_dynamic_uri.return_value = { + 'Links': { + 'Oem': { + 'Dell': { + 'DellLCService': { + '@odata.id': 'MANAGER_URI_RESOURCE/Oem/DellLCService' + } + } + } + } + } + + expected_system_erase_url = 'MANAGER_URI_RESOURCE/Oem/DellLCService' + + try: + # Call the method under test + result_url = system_erase_obj.get_system_erase_url() + + # Assert the expected outcome + assert result_url == expected_system_erase_url + + except Exception as e: + print("Error occurred:", str(e)) + + @patch(MODULE_PATH + 'SystemErase.get_url') + def test_check_system_erase_job_success(self, mock_get_url, mock_module): + """Test check_system_erase_job when a SystemErase job is found with a valid state.""" + mock_url = REDFISH_MOCK_URL + mock_get_url.return_value = mock_url + + # Create a mock idrac object + mock_idrac = MagicMock() + mock_idrac.invoke_request.return_value = MagicMock( + json_data={ + 'Members': [ + {'JobType': 'SystemErase', 'JobState': 'New'}, + {'JobType': 'SystemErase', 'JobState': 'Completed'} + ] + } + ) + + # Initialize SystemErase object with mock idrac + system_erase_obj = SystemErase(module=mock_module, idrac=mock_idrac) + + result = system_erase_obj.check_system_erase_job() + assert result == 'Completed' + + @patch(MODULE_PATH + 'SystemErase.get_url') + def test_check_system_erase_job_failed(self, mock_get_url, mock_module): + """Test check_system_erase_job when a SystemErase job is found with 'Failed' state.""" + mock_url = REDFISH_MOCK_URL + mock_get_url.return_value = mock_url + + # Create a mock idrac object + mock_idrac = MagicMock() + mock_idrac.invoke_request.return_value = MagicMock( + json_data={ + 'Members': [ + {'JobType': 'SystemErase', 'JobState': 'New'}, + {'JobType': 'SystemErase', 'JobState': 'Failed'} + ] + } + ) + + # Initialize SystemErase object with mock idrac + system_erase_obj = SystemErase(module=mock_module, idrac=mock_idrac) + + result = system_erase_obj.check_system_erase_job() + assert result == 'Failed' + + @patch(MODULE_PATH + 'SystemErase.get_url') + def test_check_system_erase_job_multiple_states(self, mock_get_url, mock_module): + """Test check_system_erase_job with multiple jobs having different states.""" + mock_url = REDFISH_MOCK_URL + mock_get_url.return_value = mock_url + + # Create a mock idrac object + mock_idrac = MagicMock() + mock_idrac.invoke_request.return_value = MagicMock( + json_data={ + 'Members': [ + {'JobType': 'SystemErase', 'JobState': 'Running'}, + {'JobType': 'SystemErase', 'JobState': 'New'}, + {'JobType': 'SystemErase', 'JobState': 'Completed'} + ] + } + ) + + # Initialize SystemErase object with mock idrac + system_erase_obj = SystemErase(module=mock_module, idrac=mock_idrac) + + result = system_erase_obj.check_system_erase_job() + assert result == 'Completed' + + @patch(MODULE_PATH + 'SystemErase.get_url') + def test_check_system_erase_job_no_jobs(self, mock_get_url, mock_module): + """Test check_system_erase_job when no SystemErase jobs are found.""" + mock_url = REDFISH_MOCK_URL + mock_get_url.return_value = mock_url + + # Create a mock idrac object + mock_idrac = MagicMock() + mock_idrac.invoke_request.return_value = MagicMock( + json_data={'Members': []}) + + # Initialize SystemErase object with mock idrac + system_erase_obj = SystemErase(module=mock_module, idrac=mock_idrac) + + result = system_erase_obj.check_system_erase_job() + assert result == 'Unknown' + + @patch(MODULE_PATH + 'SystemErase.get_url') + def test_check_system_erase_job_no_system_erase_type(self, mock_get_url, mock_module): + """Test check_system_erase_job when there are jobs but none of type 'SystemErase'.""" + mock_url = REDFISH_MOCK_URL + mock_get_url.return_value = mock_url + + # Create a mock idrac object + mock_idrac = MagicMock() + mock_idrac.invoke_request.return_value = MagicMock( + json_data={ + 'Members': [ + {'JobType': 'OtherType', 'JobState': 'Completed'}, + {'JobType': 'OtherType', 'JobState': 'New'} + ] + } + ) + + # Initialize SystemErase object with mock idrac + system_erase_obj = SystemErase(module=mock_module, idrac=mock_idrac) + + result = system_erase_obj.check_system_erase_job() + assert result == 'Unknown' + + @patch(MODULE_PATH + 'SystemErase.get_url') + def test_check_system_erase_job_partial_states(self, mock_get_url, mock_module): + """Test check_system_erase_job with a mix of SystemErase and other jobs.""" + mock_url = REDFISH_MOCK_URL + mock_get_url.return_value = mock_url + + # Create a mock idrac object + mock_idrac = MagicMock() + mock_idrac.invoke_request.return_value = MagicMock( + json_data={ + 'Members': [ + {'JobType': 'OtherType', 'JobState': 'Completed'}, + {'JobType': 'SystemErase', 'JobState': 'New'}, + {'JobType': 'SystemErase', 'JobState': 'Scheduling'}, + {'JobType': 'OtherType', 'JobState': 'Failed'} + ] + } + ) + + # Initialize SystemErase object with mock idrac + system_erase_obj = SystemErase(module=mock_module, idrac=mock_idrac) + + result = system_erase_obj.check_system_erase_job() + assert result == 'Scheduling' + + def test_validate_job_wait(self, idrac_default_args, idrac_connection_system_erase_mock, mocker): + # Scenario 1: Negative timeout + idrac_default_args.update({'job_wait': True, 'job_wait_timeout': -120}) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=False) + system_erase_obj = self.module.SystemErase( + idrac_connection_system_erase_mock, f_module) + with pytest.raises(Exception) as exc: + system_erase_obj.validate_job_wait() + assert exc.value.args[0] == TIMEOUT_NEGATIVE_OR_ZERO_MSG + + # Scenario 2: Valid timeout + idrac_default_args.update({'job_wait': True, 'job_wait_timeout': 120}) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=False) + system_erase_obj = self.module.SystemErase( + idrac_connection_system_erase_mock, f_module) + resp = system_erase_obj.validate_job_wait() + assert resp is None + + def test_check_allowable_value(self, idrac_default_args, idrac_connection_system_erase_mock, mocker): + obj = MagicMock() + obj.json_data = { + "Actions": { + "#DellLCService.SystemErase": { + "Component@Redfish.AllowableValues": [ + "AllApps", + "BIOS", + "CryptographicErasePD", + "DIAG", + "DPU", + "DrvPack", + "IDRAC", + "LCData", + "NonVolatileMemory", + "OverwritePD", + "PERCNVCache", + "ReinstallFW", + "vFlash" + ]}}} + # Scenario 1: Valid component + mocker.patch( + MODULE_PATH + SYSTEM_ERASE_URL, return_value=API_ONE) + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, return_value=obj) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=False) + system_erase_obj = self.module.SystemErase( + idrac_connection_system_erase_mock, f_module) + component = ["BIOS", "vFlash"] + resp = system_erase_obj.check_allowable_value(component) + assert resp == (['BIOS', 'vFlash'], []) + + # Scenario 2: Invalid component + mocker.patch( + MODULE_PATH + SYSTEM_ERASE_URL, return_value=API_ONE) + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, return_value=obj) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=False) + system_erase_obj = self.module.SystemErase( + idrac_connection_system_erase_mock, f_module) + component = ["invalid"] + with pytest.raises(Exception) as exc: + system_erase_obj.check_allowable_value(component) + assert exc.value.args[0] == NO_COMPONENT_MATCH + + # Scenario 3: Invalid component in check_mode + mocker.patch( + MODULE_PATH + SYSTEM_ERASE_URL, return_value=API_ONE) + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, return_value=obj) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=True) + system_erase_obj = self.module.SystemErase( + idrac_connection_system_erase_mock, f_module) + component = ["invalid"] + with pytest.raises(Exception) as exc: + system_erase_obj.check_allowable_value(component) + assert exc.value.args[0] == NO_CHANGES_FOUND_MSG + + @pytest.mark.parametrize("exc", + [URLError, HTTPError, SSLValidationError, ConnectionError, TypeError, ValueError]) + def test_idrac_system_erase_main_exception_handling_case(self, exc, mocker, idrac_default_args): + idrac_default_args.update( + {"component": ['BIOS']}) + # Scenario 1: HTTPError with random message id + error_string = to_text(json.dumps({"error": {MESSAGE_EXTENDED: [ + { + 'MessageId': "123", + "Message": HTTP_ERROR + } + ]}})) + if exc in [HTTPError, SSLValidationError]: + mocker.patch(MODULE_PATH + "SystemErase.execute", + side_effect=exc(HTTPS_PATH, 400, + HTTP_ERROR, + {"accept-type": APPLICATION_JSON}, + StringIO(error_string))) + res_out = self._run_module(idrac_default_args) + assert 'msg' in res_out + + def test_main(self, mocker): + mock_module = mocker.MagicMock() + mock_idrac = mocker.MagicMock() + system_erase_mock = mocker.MagicMock() + system_erase_mock.execute.return_value = (None, None) + mocker.patch(MODULE_PATH + 'get_argument_spec', return_value={}) + mocker.patch(MODULE_PATH + 'IdracAnsibleModule', + return_value=mock_module) + mocker.patch(MODULE_PATH + REDFISH_API, return_value=mock_idrac) + mocker.patch(MODULE_PATH + 'SystemErase.validate_job_wait', + return_value=system_erase_mock) + main() + system_erase_mock.execute.return_value = (None, None) + mocker.patch(MODULE_PATH + 'SystemErase.validate_job_wait', + return_value=system_erase_mock) + main() + + class TestEraseComponent(FakeAnsibleModule): + module = idrac_system_erase + + @pytest.fixture + def idrac_system_erase_mock(self): + idrac_obj = MagicMock() + return idrac_obj + + @pytest.fixture + def idrac_connection_system_erase_mock(self, mocker, idrac_system_erase_mock): + idrac_conn_mock = mocker.patch(MODULE_PATH + REDFISH_API, + return_value=idrac_system_erase_mock) + idrac_conn_mock.return_value.__enter__.return_value = idrac_system_erase_mock + return idrac_conn_mock + + def test_execute(self, idrac_default_args, idrac_connection_system_erase_mock, mocker): + obj = MagicMock() + obj.status_code = 202 + # Scenario 1: Status is success and job_wait as false + idrac_default_args.update( + {'component': ['DIAG'], 'job_wait': False}) + mocker.patch( + MODULE_PATH + ALLOWABLE_VALUE_FUNC, return_value=(["DIAG"], [])) + mocker.patch( + MODULE_PATH + SYSTEM_ERASE_URL, return_value=API_ONE) + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, return_value=obj) + mocker.patch(MODULE_PATH + "validate_and_get_first_resource_id_uri", + return_value=("/redfish/v1", None)) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=False) + erase_component_obj = self.module.EraseComponent( + idrac_connection_system_erase_mock, f_module) + with pytest.raises(Exception) as exc: + erase_component_obj.execute() + assert exc.value.args[0] == ERASE_SUCCESS_SCHEDULED_MSG + + # Scenario 2: Status is success and job_wait as true with power_on as true + job = {"JobState": "Completed"} + idrac_default_args.update( + {'component': ['DIAG'], 'power_on': True, 'job_wait': True, 'job_wait_timeout': 1}) + mocker.patch( + MODULE_PATH + ALLOWABLE_VALUE_FUNC, return_value=(["DIAG"], [])) + mocker.patch( + MODULE_PATH + SYSTEM_ERASE_URL, return_value=API_ONE) + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, return_value=obj) + mocker.patch( + MODULE_PATH + SYSTEM_ERASE_JOB_STATUS, return_value=job) + mocker.patch(MODULE_UTILS_PATH + "validate_and_get_first_resource_id_uri", + return_value=([MANAGER_URI_ONE], None)) + mocker.patch(MODULE_UTILS_PATH + "get_dynamic_uri", + return_value={"Actions": {"#ComputerSystem.Reset": {"target": API_ONE}}}) + mocker.patch( + MODULE_UTILS_PATH + "trigger_restart_operation", return_value=(None, None)) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=False) + erase_component_obj = self.module.EraseComponent( + idrac_connection_system_erase_mock, f_module) + with pytest.raises(Exception) as exc: + erase_component_obj.execute() + assert exc.value.args[0] == ERASE_SUCCESS_POWER_ON_MSG + + # Scenario 3: Status is failure + obj.status_code = 404 + job = {"JobState": "Failed"} + idrac_default_args.update( + {'component': ['DIAG']}) + mocker.patch( + MODULE_PATH + ALLOWABLE_VALUE_FUNC, return_value=(["DIAG"], [])) + mocker.patch( + MODULE_PATH + SYSTEM_ERASE_URL, return_value=API_ONE) + mocker.patch(MODULE_PATH + API_INVOKE_MOCKER, return_value=obj) + mocker.patch( + MODULE_PATH + SYSTEM_ERASE_JOB_STATUS, return_value=job) + f_module = self.get_module_mock( + params=idrac_default_args, check_mode=False) + erase_component_obj = self.module.EraseComponent( + idrac_connection_system_erase_mock, f_module) + with pytest.raises(Exception) as exc: + erase_component_obj.execute() + assert exc.value.args[0] == FAILURE_MSG