From 0b631e789c9c063c3a410153b25b2fbcad7a63c8 Mon Sep 17 00:00:00 2001 From: Srujana-2000 <85608509+Srujana-2000@users.noreply.github.com> Date: Thu, 1 Feb 2024 14:34:55 +0530 Subject: [PATCH] firmware update --- .github/BOTMETA.yml | 6 + .../module_utils/hpc_system_firmware_utils.py | 444 ++++++++++++++++++ plugins/modules/hpc_get_power_state.py | 170 +++++++ plugins/modules/hpc_get_system_fw_inv.py | 161 +++++++ plugins/modules/hpc_update_system_firmware.py | 226 +++++++++ tests/unit/requirements.txt | 3 + 6 files changed, 1010 insertions(+) create mode 100644 plugins/module_utils/hpc_system_firmware_utils.py create mode 100644 plugins/modules/hpc_get_power_state.py create mode 100644 plugins/modules/hpc_get_system_fw_inv.py create mode 100644 plugins/modules/hpc_update_system_firmware.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 5700445910..b127e6b96d 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -610,6 +610,12 @@ files: maintainers: jameslivulpi $modules/honeybadger_deployment.py: maintainers: stympy + $modules/hpc_get_power_state.py: + maintainers: srujana + $modules/hpc_get_system_fw_inv.py: + maintainers: srujana + $modules/hpc_update_system_firmware.py: + maintainers: srujana $modules/hpilo_: ignore: dagwieers maintainers: haad diff --git a/plugins/module_utils/hpc_system_firmware_utils.py b/plugins/module_utils/hpc_system_firmware_utils.py new file mode 100644 index 0000000000..ca1d13f51e --- /dev/null +++ b/plugins/module_utils/hpc_system_firmware_utils.py @@ -0,0 +1,444 @@ +# !/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2021-2022 Hewlett Packard Enterprise, Inc. All rights reserved. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import json +import subprocess +import time + +from ansible_collections.community.general.plugins.module_utils.redfish_utils import RedfishUtils +from ansible.module_utils.urls import open_url +try: + from requests_toolbelt import MultipartEncoder + HAS_REQUESTS_TOOLBELT = True +except ImportError: + HAS_REQUESTS_TOOLBELT = False + + +REQUESTS_TOOLBELT_REQUIRED = "Requests_toolbelt is required for this module." + + +def has_requests_toolbelt(module): + """ + Check Request_toolbelt is installed + :param module: + """ + if not HAS_REQUESTS_TOOLBELT: + module.fail_json(msg=REQUESTS_TOOLBELT_REQUIRED) + + +supported_models = ["HPE CRAY XD220V", "HPE CRAY XD225V", "HPE CRAY XD295V", "HPE CRAY XD665", "HPE CRAY XD670"] + +# To get inventory, update +supported_targets = { + "HPE CRAY XD220V": ["BMC", "BIOS", "MainCPLD", "HDDBPPIC", "PDBPIC"], + "HPE CRAY XD225V": ["BMC", "BIOS", "MainCPLD", "HDDBPPIC", "PDBPIC"], + "HPE CRAY XD295V": ["BMC", "BIOS", "MainCPLD", "HDDBPPIC", "PDBPIC"], + "HPE CRAY XD665": ["BMC", "BIOS", "RT_NVME", "RT_OTHER", "RT_SA", "PDB", "MainCPLD", "UBM6"], + "HPE CRAY XD670": ["BMCImage1", "BMCImage2", "BIOS", "BIOS2", "BPB_CPLD1", "BPB_CPLD2", "MB_CPLD1", "SCM_CPLD1"], +} + +unsupported_targets = ["BMCImage1", "BPB_CPLD1", "BPB_CPLD2", "MB_CPLD1", "SCM_CPLD1"] # Only of Jakku +# BMCImage1 equivalent to BMC +# BPB_CPLD1 and BPB_CPLD2 together equivalent to BPB_CPLD +# MB_CPLD1 and SCM_CPLD1 together equivalent to MB_CPLD1_SCM_CPLD1 +all_targets = ['BMC', 'BMCImage1', 'BMCImage2', 'BIOS', 'BIOS2', 'MainCPLD', + 'MB_CPLD1', 'BPB_CPLD1', 'BPB_CPLD2', 'SCM_CPLD1', 'PDB', 'PDBPIC', 'HDDBPPIC', 'RT_NVME', 'RT_OTHER', 'RT_SA', 'UBM6'] + +reboot = { + "BIOS": ["AC_PC_redfish"], + "BIOS2": ["AC_PC_redfish"], + "MainCPLD": ["AC_PC_ipmi"], + "PDB": ["AC_PC_ipmi"], + "RT_NVME": ["AC_PC_redfish", "AC_PC_ipmi", "AC_PC_redfish"], + "RT_SA": ["AC_PC_redfish", "AC_PC_ipmi", "AC_PC_redfish"], + "RT_OTHER": ["AC_PC_redfish", "AC_PC_ipmi", "AC_PC_redfish"], + "HDDBPPIC": ["AC_PC_redfish"], + "PDBPIC": ["AC_PC_redfish"] +} + +routing = { + "HPE CRAY XD220V": "0x34 0xa2 0x00 0x19 0xA9", + "HPE CRAY XD225V": "0x34 0xa2 0x00 0x19 0xA9", + "HPE CRAY XD295V": "0x34 0xa2 0x00 0x19 0xA9", + "HPE CRAY XD665": "0x34 0xA2 0x00 0x19 0xa9 0x00" +} + + +class CrayRedfishUtils(RedfishUtils): + def post_multi_request(self, uri, headers, payload): + username, password, basic_auth = self._auth_params(headers) + try: + resp = open_url(uri, data=payload, headers=headers, method="POST", + url_username=username, url_password=password, + force_basic_auth=basic_auth, validate_certs=False, + follow_redirects='all', + use_proxy=True, timeout=self.timeout) + resp_headers = dict((k.lower(), v) for (k, v) in resp.info().items()) + return True + except Exception as e: + return False + + def get_model(self): + response = self.get_request(self.root_uri + "/redfish/v1/Systems/Self") + if response['ret'] is False: + return "NA" + try: + if 'Model' in response['data']: + model = response['data'][u'Model'] + if model is not None: + model = model[:15] + else: + model = 'None' + else: + model = 'None' + return model + except Exception: + if 'Model' in response: + model = response[u'Model'] + if model is not None: + model = model[:15] + else: + model = 'None' + else: + model = 'None' + return model + + def power_state(self): + response = self.get_request(self.root_uri + "/redfish/v1/Systems/Self") + if response['ret'] is False: + return "NA" + try: + if 'PowerState' in response['data']: + state = response['data'][u'PowerState'] + if state is None: + state = 'None' + else: + state = 'None' + return state + except Exception: + if 'PowerState' in response: + state = response[u'PowerState'] + if state is None: + state = 'None' + else: + state = 'None' + return state + + def power_on(self): + payload = {"ResetType": "On"} + target_uri = "/redfish/v1/Systems/Self/Actions/ComputerSystem.Reset" + response1 = self.post_request(self.root_uri + target_uri, payload) + time.sleep(120) + + def power_off(self): + payload = {"ResetType": "ForceOff"} + target_uri = "/redfish/v1/Systems/Self/Actions/ComputerSystem.Reset" + response1 = self.post_request(self.root_uri + target_uri, payload) + time.sleep(120) + + def get_PS_CrayXD670(self, attr): + IP = attr.get('baseuri') + option = attr.get('power_state') + csv_file_name = attr.get('output_file_name') + if not os.path.exists(csv_file_name): + f = open(csv_file_name, "w") + to_write = "IP_Address, Model, Power_State\n" + f.write(to_write) + f.close() + model = self.get_model() + if model.upper() == "HPE CRAY XD670": + power_state = self.power_state() + if option.upper() == "NA": + lis = [IP, model, power_state] + elif option.upper() == "ON": + if power_state.upper() == "OFF": + self.power_on() + power_state = self.power_state() + lis = [IP, model, power_state] + elif option.upper() == "OFF": + if power_state.upper() == "ON": + self.power_off() + power_state = self.power_state() + lis = [IP, model, power_state] + else: + return {'ret': False, 'changed': True, 'msg': 'Must specify the correct required option for power_state'} + + else: + lis = [IP, model, "unsupported_model"] + new_data = ", ".join(lis) + return {'ret': True, 'changed': True, 'msg': str(new_data)} + + def target_supported(self, model, target): + if target in supported_targets[model.upper()]: + return True + return False + + def get_fw_version(self, target): + try: + response = self.get_request(self.root_uri + "/redfish/v1/UpdateService/FirmwareInventory" + "/" + target) + try: + version = response['data']['Version'] + return version + except Exception: + version = response['Version'] + return version + except Exception: + return "failed_FI_GET_call/no_version_field" + + def AC_PC_redfish(self): + payload = {"ResetType": "ForceRestart"} + target_uri = "/redfish/v1/Systems/Self/Actions/ComputerSystem.Reset" + response1 = self.post_request(self.root_uri + target_uri, payload) + time.sleep(180) + target_uri = "/redfish/v1/Chassis/Self/Actions/Chassis.Reset" + response2 = self.post_request(self.root_uri + target_uri, payload) + time.sleep(180) + return response1 or response2 + + def AC_PC_ipmi(self, IP, username, password, routing_value): + try: + command = 'ipmitool -I lanplus -H ' + IP + ' -U ' + username + ' -P ' + password + ' raw ' + routing_value + subprocess.run(command, shell=True, check=True, timeout=15, capture_output=True) + time.sleep(300) + return True + except Exception: + return False + + def get_sys_fw_inventory(self, attr): + IP = attr.get('baseuri') + csv_file_name = attr.get('output_file_name') + if not os.path.exists(csv_file_name): + f = open(csv_file_name, "w") + to_write = """IP_Address, Model, BMC, BMCImage1, BMCImage2, BIOS, BIOS2, MainCPLD, MB_CPLD1, + BPB_CPLD1, BPB_CPLD2, SCM_CPLD1, PDB, PDBPIC, HDDBPPIC, RT_NVME, RT_OTHER, RT_SA, UBM6\n""" + f.write(to_write) + f.close() + model = self.get_model() + entry = [] + entry.append(IP) + if model.upper() not in supported_models: + entry.append("unsupported_model") + for target in all_targets: + entry.append("NA") + else: + entry.append(model) + for target in all_targets: + if target in supported_targets[model.upper()]: + version = self.get_fw_version(target) + if version.startswith("failed"): + version = "NA" # "no_comp/no_version" + else: + version = "NA" + entry.append(version) + new_data = ", ".join(entry) + return {'ret': True, 'changed': True, 'msg': str(new_data)} + + def helper_update(self, update_status, target, image_path, image_type, IP, username, password, model): + before_version = "failed" + if target != "BPB_CPLD" and target != "SCM_CPLD1" and target != "MB_CPLD1": + before_version = self.get_fw_version(target) + after_version = "NA" + else: + before_version = "NA" + if not before_version.startswith("failed"): + # proceed for update + response = self.get_request(self.root_uri + "/redfish/v1/UpdateService") + if response['ret'] is False: + update_status = "UpdateService api not found" + else: + data = response['data'] + if 'MultipartHttpPushUri' in data: + headers = {'Expect': 'Continue', 'Content-Type': 'multipart/form-data'} + body = {} + if target != "BPB_CPLD": + targets_uri = '/redfish/v1/UpdateService/FirmwareInventory/' + target + '/' + body['UpdateParameters'] = (None, json.dumps({"Targets": [targets_uri]}), 'application/json') + else: + body['UpdateParameters'] = (None, json.dumps({"Targets": + ['/redfish/v1/UpdateService/FirmwareInventory/BPB_CPLD1/', + '/redfish/v1/UpdateService/FirmwareInventory/BPB_CPLD2/']}), + 'application/json') + body['OemParameters'] = (None, json.dumps({"ImageType": image_type}), 'application/json') + with open(image_path, 'rb') as image_path_rb: + body['UpdateFile'] = (image_path, image_path_rb, 'application/octet-stream') + encoder = MultipartEncoder(body) + body = encoder.to_string() + headers['Content-Type'] = encoder.content_type + + response = self.post_multi_request(self.root_uri + data['MultipartHttpPushUri'], + headers=headers, payload=body) + if response is False: + update_status = "failed_POST" + else: + # Add time.sleep (for system to comeback after flashing) + time.sleep(300) + # Call reboot logic based on target + if target in reboot: + what_reboots = reboot[target] + for reb in what_reboots: + if reb == "AC_PC_redfish": + result = self.AC_PC_redfish() + if not result: + update_status = "reboot_failed" + break + time.sleep(300) + elif reb == "AC_PC_ipmi": + # based on the model end routing code changes + result = self.AC_PC_ipmi(IP, username, password, routing[model.upper()]) + if not result: + update_status = "reboot_failed" + break + + # if target=="MB_CPLD1" or "BPB" in target: + # turn node back to on -- call power_on_node function + # self.power_on() + # not required to power on the node as it useful only after physical POWER CYCLE and we can't keep the track of the + # physical power cycle so skipping it + if update_status.lower() == "success": + # call version of respective target and store versions after update + time.sleep(180) # extra time requiring as of now for systems under test + if target != "BPB_CPLD" and target != "SCM_CPLD1" and target != "MB_CPLD1": + after_version = self.get_fw_version(target) + else: + if target != "BPB_CPLD" and target != "SCM_CPLD1" and target != "MB_CPLD1": + after_version = "NA" + update_status = "failed" + + if target != "BPB_CPLD" and target != "SCM_CPLD1" and target != "MB_CPLD1": + return before_version, after_version, update_status + else: + return update_status + + def system_fw_update(self, attr): + IP = attr.get('baseuri') + username = attr.get('username') + password = attr.get('password') + image_type = attr.get('update_image_type') + update_status = "Success" + is_target_supported = False + image_path = "NA" + target = attr.get('update_target') + image_path_inputs = { + "HPE CRAY XD220V": attr.get('update_image_path_xd220V'), + "HPE CRAY XD225V": attr.get('update_image_path_xd225V'), + "HPE CRAY XD295V": attr.get('update_image_path_xd295V'), + "HPE CRAY XD665": attr.get('update_image_path_xd665'), + "HPE CRAY XD670": attr.get('update_image_path_xd670')} + csv_file_name = attr.get('output_file_name') + + # Have a check that atleast one image path set based out of the above new logic + if not any(image_path_inputs.values()): + return {'ret': False, 'changed': True, 'msg': 'Must specify atleast one update_image_path'} + + if target == "" or target.upper() in unsupported_targets: + return {'ret': False, 'changed': True, 'msg': 'Must specify the correct target for firmware update'} + + model = self.get_model() + + if not os.path.exists(csv_file_name): + f = open(csv_file_name, "w") + if target == "BPB_CPLD" or target == "SCM_CPLD1_MB_CPLD1": + to_write = "IP_Address, Model, Update_Status, Remarks\n" + else: + to_write = "IP_Address, Model, " + target + '_Pre_Ver, ' + target + '_Post_Ver, ' + "Update_Status\n" + f.write(to_write) + f.close() + + # check if model is Cray XD670 and target is BMC assign default value of BMC as BMCImage1 + if model.upper() == "HPE CRAY XD670" and target == "BMC": + target = "BMCImage1" + + if model.upper() not in supported_models: + update_status = "unsupported_model" + if target == "SCM_CPLD1_MB_CPLD1" or target == "BPB_CPLD": + lis = [IP, model, update_status, "NA"] + else: + lis = [IP, model, "NA", "NA", update_status] + new_data = ", ".join(lis) + return {'ret': True, 'changed': True, 'msg': str(new_data)} + else: + image_path = image_path_inputs[model.upper()] + if model.upper() == "HPE CRAY XD670" and "CPLD" in target.upper(): + power_state = self.power_state() + if power_state.lower() != "on": + update_status = "NA" + lis = [IP, model, update_status, "node is not ON, please power on the node"] + new_data = ", ".join(lis) + return {'ret': True, 'changed': True, 'msg': str(new_data)} + elif target == 'SCM_CPLD1_MB_CPLD1': + is_target_supported = True + image_paths = image_path_inputs["HPE CRAY XD670"].split() + if len(image_paths) != 2: + return {'ret': False, 'changed': True, 'msg': '''Must specify exactly 2 image_paths, + first for SCM_CPLD1 of Cray XD670 and second for MB_CPLD1 of Cray XD670'''} + for img_path in image_paths: + if not os.path.isfile(img_path): + return {'ret': False, 'changed': True, + 'msg': '''Must specify correct image_paths for SCM_CPLD1_MB_CPLD1, first for SCM_CPLD1 + of Cray XD670 and second for MB_CPLD1 of Cray XD670'''} + + if target != "SCM_CPLD1_MB_CPLD1" and not os.path.isfile(image_path): + update_status = "NA_fw_file_absent" + if target == "BPB_CPLD": + lis = [IP, model, update_status, "NA"] + else: + lis = [IP, model, "NA", "NA", update_status] + new_data = ", ".join(lis) + return {'ret': True, 'changed': True, 'msg': str(new_data)} + else: + if target != "SCM_CPLD1_MB_CPLD1" and target != "BPB_CPLD": + is_target_supported = self.target_supported(model, target) + if model.upper() == "HPE CRAY XD670" and (target == "BMC" or target == "BPB_CPLD"): + is_target_supported = True + + if not is_target_supported: + update_status = "target_not_supported" + if target == "SCM_CPLD1_MB_CPLD1" or target == "BPB_CPLD": + lis = [IP, model, update_status, "NA"] + else: + lis = [IP, model, "NA", "NA", update_status] + new_data = ", ".join(lis) + return {'ret': True, 'changed': True, 'msg': str(new_data)} + else: + # check if model is Cray XD670 and target is BMC assign default value of BMC as BMCImage1 + if model.upper() == "HPE CRAY XD670" and target == "BMC": + target = "BMCImage1" + + # call version of respective target and store versions before update + if target == "SCM_CPLD1_MB_CPLD1": + update_status = self.helper_update(update_status, "SCM_CPLD1", image_paths[0], image_type, IP, username, password, "HPE Cray XD670") + if update_status.lower() == "success": + # SCM has updates successfully, proceed for MB_CPLD1 update + # check node to be off -- call power_off_node function + power_state = self.power_state() + if power_state.lower() == "on": + self.power_off() + power_state = self.power_state() + if power_state.lower() == "on": + lis = [IP, model, "NA", "MB_CPLD1 requires node off, tried powering off the node, but failed to power off"] + update_status = self.helper_update(update_status, "MB_CPLD1", + image_paths[1], image_type, IP, username, password, "HPE Cray XD670") + if update_status.lower() == "success": + remarks = "Please plug out and plug in power cables physically" + else: + remarks = "Please reflash the firmware and DO NOT DO physical power cycle" + lis = [IP, model, update_status, remarks] + elif target == "BPB_CPLD": + update_status = self.helper_update(update_status, target, image_path, image_type, IP, username, password, model) + if update_status.lower() == "success": + remarks = "Please plug out and plug in power cables physically" + else: + remarks = "Please reflash the firmware and DO NOT DO physical power cycle" + lis = [IP, model, update_status, remarks] + else: + bef_ver, aft_ver, update_status = self.helper_update(update_status, target, image_path, image_type, IP, username, password, model) + lis = [IP, model, bef_ver, aft_ver, update_status] + new_data = ", ".join(lis) + return {'ret': True, 'changed': True, 'msg': str(new_data)} diff --git a/plugins/modules/hpc_get_power_state.py b/plugins/modules/hpc_get_power_state.py new file mode 100644 index 0000000000..9e2b0ece4f --- /dev/null +++ b/plugins/modules/hpc_get_power_state.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021-2022 Hewlett Packard Enterprise, Inc. All rights reserved. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: hpc_get_power_state +short_description: Inventory Information of CrayXD components using Redfish APIs +version_added: 1.1.0 +description: + - using Redfish URI's Fetch the CrayXD components Inventory Information +attributes: + check_mode: + support: none + diff_mode: + support: none +extends_documentation_fragment: + - community.general.attributes +options: + category: + required: true + description: + - Category to Get Inventory of the CrayXD components. + type: str + command: + required: true + description: + - List of commands to execute on the CrayXD. + type: list + elements: str + baseuri: + required: true + description: + - Base URI of OOB controller. + type: str + username: + required: true + description: + - Username for authenticating to CrayXD. + type: str + password: + required: true + description: + - Password for authenticating to CrayXD. + type: str + auth_token: + required: false + description: + - Security token for authenticating to CrayXD. + type: str + timeout: + required: false + description: + - Timeout in seconds for HTTP requests to CrayXD. + default: 300 + type: int + power_state: + required: true + description: + - To get or modify the power state of CrayXD670 + choices: ['ON', 'OFF', 'NA'] + type: str + output_file_name: + required: false + description: + - To save the output of the Inventory, mention the output file name in csv. + default: Power_output.csv + type: str + + +author: + - Srujana Yasa (@srujana) +''' + +EXAMPLES = r''' + - name: Getting Power State of Cray XD670 Server nodes + get_power_state: + category: Get_Power_State + command: Get_PS + baseuri: "baseuri" + username: "bmc_username" + password: "bmc_password" + power_state: "off" + output_file_name: "output_file" +''' + +RETURN = r''' +csv: + description: Output of this Task is saved to a csv file. + returned: Returned an output file containing the details of update + type: str + sample: Output_file.csv +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.hpc_system_firmware_utils import CrayRedfishUtils +from ansible.module_utils.common.text.converters import to_native + + +category_commands = { + "Get_Power_State": ["Get_PS"], +} + + +def main(): + result = {} + return_values = {} + module = AnsibleModule( + argument_spec=dict( + category=dict(required=True), + command=dict(required=True, type='list', elements='str'), + baseuri=dict(required=True), + username=dict(required=True), + password=dict(required=True, no_log=True), + auth_token=dict(no_log=True), + timeout=dict(type='int', default=300), + power_state=dict(required=True, choices=['ON', 'OFF', 'NA']), + output_file_name=dict(type='str', default='Power_output.csv'), + ), + supports_check_mode=False + ) + + category = module.params['category'] + command_list = module.params['command'] + + # admin credentials used for authentication + creds = {'user': module.params['username'], + 'pswd': module.params['password'], + 'token': module.params['auth_token']} + + timeout = module.params['timeout'] + # Build root URI + root_uri = "https://" + module.params['baseuri'] + # update_uri = "/redfish/v1/UpdateService" + rf_utils = CrayRedfishUtils(creds, root_uri, timeout, module, data_modification=True) + + # Check that Category is valid + if category not in category_commands: + module.fail_json(msg=to_native("Invalid Category '%s'. Valid Categories = %s" % (category, list(category_commands.keys())))) + + # Check that all commands are valid + for cmd in command_list: + # Fail if even one command given is invalid + if cmd not in category_commands[category]: + module.fail_json(msg=to_native("Invalid Command '%s'. Valid Commands = %s" % (cmd, category_commands[category]))) + + if category == "Get_Power_State": + for command in command_list: + if command == "Get_PS": + result = rf_utils.get_PS_CrayXD670({'baseuri': module.params['baseuri'], + 'username': module.params['username'], + 'password': module.params['password'], + 'power_state': module.params['power_state'], + 'output_file_name': module.params['output_file_name']}) + if result['ret']: + msg = result.get('msg', False) + module.exit_json(msg=msg) + else: + module.fail_json(msg=to_native(result)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/hpc_get_system_fw_inv.py b/plugins/modules/hpc_get_system_fw_inv.py new file mode 100644 index 0000000000..c5605db1d1 --- /dev/null +++ b/plugins/modules/hpc_get_system_fw_inv.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021-2022 Hewlett Packard Enterprise, Inc. All rights reserved. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: hpc_get_system_fw_inv +short_description: Inventory Information of CrayXD components using Redfish APIs +version_added: 1.1.0 +description: + - using Redfish URI's Fetch the CrayXD components Inventory Information +attributes: + check_mode: + support: none + diff_mode: + support: none +extends_documentation_fragment: + - community.general.attributes +options: + category: + required: true + description: + - Category to Get Inventory of the CrayXD components. + type: str + command: + required: true + description: + - List of commands to execute on the CrayXD. + type: list + elements: str + baseuri: + required: true + description: + - Base URI of OOB controller. + type: str + username: + required: true + description: + - Username for authenticating to CrayXD. + type: str + password: + required: true + description: + - Password for authenticating to CrayXD. + type: str + auth_token: + required: false + description: + - Security token for authenticating to CrayXD. + type: str + timeout: + required: false + description: + - Timeout in seconds for HTTP requests to CrayXD. + default: 300 + type: int + output_file_name: + required: false + description: + - To save the output of the Inventory, mention the output file name in csv. + default: get_output.csv + type: str + + +author: + - Srujana Yasa (@srujana) +''' + +EXAMPLES = r''' + - name: Fetching System Firmware Inventory Details + hpc_get_system_fw_inv: + category: GetInventory + command: GetSystemFWInventory + baseuri: "baseuri" + username: "bmc_username" + password: "bmc_password" + output_file_name: "output_file.csv" +''' + +RETURN = r''' +csv: + description: Output of this Task is saved to a csv file. + returned: Returned an output file containing the details of update. + type: str + sample: Output_file.csv +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.hpc_system_firmware_utils import CrayRedfishUtils +from ansible.module_utils.common.text.converters import to_native + + +category_commands = { + "GetInventory": ["GetSystemFWInventory"], +} + + +def main(): + result = {} + return_values = {} + module = AnsibleModule( + argument_spec=dict( + category=dict(required=True), + command=dict(required=True, type='list', elements='str'), + baseuri=dict(required=True), + username=dict(required=True), + password=dict(no_log=True, required=True), + auth_token=dict(no_log=True), + timeout=dict(type='int', default=300), + output_file_name=dict(type='str', default='get_output.csv'), + ), + supports_check_mode=False + ) + + category = module.params['category'] + command_list = module.params['command'] + + # admin credentials used for authentication + creds = {'user': module.params['username'], + 'pswd': module.params['password'], + 'token': module.params['auth_token']} + + timeout = module.params['timeout'] + # Build root URI + root_uri = "https://" + module.params['baseuri'] + # update_uri = "/redfish/v1/UpdateService" + rf_utils = CrayRedfishUtils(creds, root_uri, timeout, module, data_modification=True) + + # Check that Category is valid + if category not in category_commands: + module.fail_json(msg=to_native("Invalid Category '%s'. Valid Categories = %s" % (category, list(category_commands.keys())))) + + # Check that all commands are valid + for cmd in command_list: + # Fail if even one command given is invalid + if cmd not in category_commands[category]: + module.fail_json(msg=to_native("Invalid Command '%s'. Valid Commands = %s" % (cmd, category_commands[category]))) + + if category == "GetInventory": + for command in command_list: + if command == "GetSystemFWInventory": + result = rf_utils.get_sys_fw_inventory({'baseuri': module.params['baseuri'], + 'username': module.params['username'], + 'password': module.params['password'], + 'output_file_name': module.params['output_file_name']}) + if result['ret']: + msg = result.get('msg', False) + module.exit_json(msg=msg) + else: + module.fail_json(msg=to_native(result)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/hpc_update_system_firmware.py b/plugins/modules/hpc_update_system_firmware.py new file mode 100644 index 0000000000..48844b6b05 --- /dev/null +++ b/plugins/modules/hpc_update_system_firmware.py @@ -0,0 +1,226 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021-2022 Hewlett Packard Enterprise, Inc. All rights reserved. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: hpc_update_system_firmware +short_description: Updates CrayXD components using Redfish APIs +version_added: 1.1.0 +description: + - using Redfish URI's updates the CrayXD components from the local HPM file +attributes: + check_mode: + support: none + diff_mode: + support: none +extends_documentation_fragment: + - community.general.attributes +options: + category: + required: true + description: + - Category to Update the components of CrayXD. + type: str + command: + required: true + description: + - List of commands to execute on the CrayXD. + type: list + elements: str + baseuri: + required: true + description: + - Base URI of OOB controller. + type: str + username: + required: true + description: + - Username for authenticating to CrayXD. + type: str + password: + required: true + description: + - Password for authenticating to CrayXD. + type: str + auth_token: + required: false + description: + - Security token for authenticating to CrayXD. + type: str + timeout: + required: false + description: + - Timeout in seconds for HTTP requests to CrayXD. + default: 300 + type: int + output_file_name: + required: false + description: + - To save the output of the update mention the output file name in csv. + type: str + default: update_output.csv + update_target: + required: true + description: + - To build the Redfish URI and to update that component it is required. + type: str + choices: [BMC , BIOS , BIOS2 , MainCPLD , HDDBPPIC , PDBPIC , RT_NVME , RT_OTHER , RT_SA , PDB , UBM6 , SCM_CPLD1_MB_CPLD1 , BPB_CPLD] + update_image_path_xd295V: + required: false + description: + - To get the path of local HPM file the Image path needs to be mentioned + type: str + default: NA + update_image_path_xd225V: + required: false + description: + - To get the path of local HPM file the Image path needs to be mentioned + type: str + default: NA + update_image_path_xd220V: + required: false + description: + - To get the path of local HPM file the Image path needs to be mentioned + type: str + default: NA + update_image_path_xd665: + required: false + description: + - To get the path of local HPM file the Image path needs to be mentioned + type: str + default: NA + update_image_path_xd670: + required: false + description: + - To get the path of local HPM file the Image path needs to be mentioned + type: str + default: NA + update_image_type: + required: false + description: + - Type of the file that is being uploaded for the update + default: HPM + type: str + +author: + - Srujana Yasa (@srujana) +''' + +EXAMPLES = r''' + - name: Running Firmware Update for Cray XD Servers + hpc_update_system_firmware: + category: Update + command: SystemFirmwareUpdate + baseuri: "baseuri" + username: "bmc_username" + password: "bmc_password" + output_file_name: "output_file_name" + update_target: "target" + update_image_path_xd295V: "path" + update_image_path_xd225V: "path" + update_image_path_xd220V: "path" + update_image_path_xd665: "path" + update_image_path_xd670: "path" +''' + +RETURN = r''' +csv: + description: Output of this Task is saved to a csv file. + returned: Returned an output file containing the details of update. + type: str + sample: Output_file.csv +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.hpc_system_firmware_utils import CrayRedfishUtils +from ansible.module_utils.common.text.converters import to_native + + +category_commands = { + "Update": ["SystemFirmwareUpdate"], +} + + +def main(): + result = {} + return_values = {} + module = AnsibleModule( + argument_spec=dict( + category=dict(required=True), + command=dict(required=True, type='list', elements='str'), + baseuri=dict(required=True), + username=dict(required=True), + password=dict(no_log=True, required=True), + auth_token=dict(no_log=True), + timeout=dict(type='int', default=300), + update_image_type=dict(type='str', default='HPM'), + update_target=dict(required=True, type='str', choices=['BMC', 'BIOS', 'BIOS2', 'MainCPLD', + 'HDDBPPIC', 'PDBPIC', 'RT_NVME', + 'RT_OTHER', 'RT_SA', 'PDB', 'UBM6', + 'SCM_CPLD1_MB_CPLD1', 'BPB_CPLD']), + update_image_path_xd220V=dict(type='str', default='NA'), + update_image_path_xd225V=dict(type='str', default='NA'), + update_image_path_xd295V=dict(type='str', default='NA'), + update_image_path_xd665=dict(type='str', default='NA'), + update_image_path_xd670=dict(type='str', default='NA'), + output_file_name=dict(type='str', default='update_output.csv') + ), + supports_check_mode=False + ) + + category = module.params['category'] + command_list = module.params['command'] + + # admin credentials used for authentication + creds = {'user': module.params['username'], + 'pswd': module.params['password'], + 'token': module.params['auth_token']} + + timeout = module.params['timeout'] + # Build root URI + root_uri = "https://" + module.params['baseuri'] + update_uri = "/redfish/v1/UpdateService" + rf_utils = CrayRedfishUtils(creds, root_uri, timeout, module, data_modification=True) + + # Check that Category is valid + if category not in category_commands: + module.fail_json(msg=to_native("Invalid Category '%s'. Valid Categories = %s" % (category, list(category_commands.keys())))) + + # Check that all commands are valid + for cmd in command_list: + # Fail if even one command given is invalid + if cmd not in category_commands[category]: + module.fail_json(msg=to_native("Invalid Command '%s'. Valid Commands = %s" % (cmd, category_commands[category]))) + + if category == "Update": + for command in command_list: + if command == "SystemFirmwareUpdate": + result = rf_utils.system_fw_update({'baseuri': module.params['baseuri'], + 'username': module.params['username'], + 'password': module.params['password'], + 'update_image_type': module.params['update_image_type'], + 'update_target': module.params['update_target'], + 'power_state': module.params['power_state'], + 'update_image_path_xd220V': module.params['update_image_path_xd220V'], + 'update_image_path_xd225V': module.params['update_image_path_xd225V'], + 'update_image_path_xd295V': module.params['update_image_path_xd295V'], + 'update_image_path_xd665': module.params['update_image_path_xd665'], + 'update_image_path_xd670': module.params['update_image_path_xd670'], + 'output_file_name': module.params['output_file_name']}) + if result['ret']: + msg = result.get('msg', False) + module.exit_json(msg=msg) + else: + module.fail_json(msg=to_native(result)) + + +if __name__ == '__main__': + main() diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index 218fe45673..023c952a66 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -54,3 +54,6 @@ proxmoxer ; python_version > '3.6' #requirements for nomad_token modules python-nomad < 2.0.0 ; python_version <= '3.6' python-nomad >= 2.0.0 ; python_version >= '3.7' + +#requirements for hpc modules +requests-toolbelt