diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index ce12bb6885..32394e9e9f 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -701,6 +701,8 @@ files: labels: cisco $modules/remote_management/ipmi/: maintainers: bgaifullin cloudnull + $modules/remote_management/lenovoxcc/: + maintainers: panyy3 renxulei $modules/remote_management/lxca/: maintainers: navalkp prabhosa $modules/remote_management/manageiq/: diff --git a/plugins/modules/remote_management/lenovoxcc/xcc_redfish_command.py b/plugins/modules/remote_management/lenovoxcc/xcc_redfish_command.py new file mode 100644 index 0000000000..d8966c6d64 --- /dev/null +++ b/plugins/modules/remote_management/lenovoxcc/xcc_redfish_command.py @@ -0,0 +1,673 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: xcc_redfish_command +short_description: Manages Lenovo Out-Of-Band controllers using Redfish APIs +version_added: 2.4.0 +description: + - Builds Redfish URIs locally and sends them to remote OOB controllers to + perform an action or get information back or update a configuration attribute. + - Manages virtual media. + - Supports getting information back via GET method. + - Supports updating a configuration attribute via PATCH method. + - Supports performing an action via POST method. +options: + category: + required: true + description: + - Category to execute on OOB controller. + type: str + command: + required: true + description: + - List of commands to execute on OOB controller. + type: list + elements: str + baseuri: + required: true + description: + - Base URI of OOB controller. + type: str + username: + description: + - Username for authentication with OOB controller. + type: str + password: + description: + - Password for authentication with OOB controller. + type: str + auth_token: + description: + - Security token for authentication with OOB controller + type: str + timeout: + description: + - Timeout in seconds for URL requests to OOB controller. + default: 10 + type: int + resource_id: + required: false + description: + - The ID of the System, Manager or Chassis to modify. + type: str + virtual_media: + required: false + description: + - The options for VirtualMedia commands. + type: dict + suboptions: + media_types: + description: + - The list of media types appropriate for the image. + type: list + elements: str + image_url: + description: + - The URL of the image to insert or eject. + type: str + inserted: + description: + - Indicates if the image is treated as inserted on command completion. + type: bool + default: true + write_protected: + description: + - Indicates if the media is treated as write-protected. + type: bool + default: true + username: + description: + - The username for accessing the image URL. + type: str + password: + description: + - The password for accessing the image URL. + type: str + transfer_protocol_type: + description: + - The network protocol to use with the image. + type: str + transfer_method: + description: + - The transfer method to use with the image. + type: str + resource_uri: + required: false + description: + - The resource uri to get or patch or post. + type: str + request_body: + required: false + description: + - The request body to patch or post. + type: dict + +author: "Yuyan Pan (@panyy3)" +''' + +EXAMPLES = ''' + - name: Insert Virtual Media + community.general.xcc_redfish_command: + category: Manager + command: VirtualMediaInsert + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + virtual_media: + image_url: "http://example.com/images/SomeLinux-current.iso" + media_types: + - CD + - DVD + resource_id: "1" + + - name: Eject Virtual Media + community.general.xcc_redfish_command: + category: Manager + command: VirtualMediaEject + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + virtual_media: + image_url: "http://example.com/images/SomeLinux-current.iso" + resource_id: "1" + + - name: Eject all Virtual Media + community.general.xcc_redfish_command: + category: Manager + command: VirtualMediaEject + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + resource_id: "1" + + - name: Get ComputeSystem Oem property SystemStatus via GetResource command + community.general.xcc_redfish_command: + category: Raw + command: GetResource + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + resource_uri: "/redfish/v1/Systems/1" + register: result + - ansible.builtin.debug: + msg: "{{ result.redfish_facts.data.Oem.Lenovo.SystemStatus }}" + + - name: Get Oem DNS setting via GetResource command + community.general.xcc_redfish_command: + category: Raw + command: GetResource + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + resource_uri: "/redfish/v1/Managers/1/NetworkProtocol/Oem/Lenovo/DNS" + register: result + - ansible.builtin.debug: + msg: "{{ result.redfish_facts.data }}" + + - name: Get Lenovo FoD key collection resource via GetCollectionResource command + community.general.xcc_redfish_command: + category: Raw + command: GetCollectionResource + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + resource_uri: "/redfish/v1/Managers/1/Oem/Lenovo/FoD/Keys" + register: result + - ansible.builtin.debug: + msg: "{{ result.redfish_facts.data_list }}" + + - name: Update ComputeSystem property AssetTag via PatchResource command + community.general.xcc_redfish_command: + category: Raw + command: PatchResource + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + resource_uri: "/redfish/v1/Systems/1" + request_body: + AssetTag: "new_asset_tag" + + - name: Perform BootToBIOSSetup action via PostResource command + community.general.xcc_redfish_command: + category: Raw + command: PostResource + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + resource_uri: "/redfish/v1/Systems/1/Actions/Oem/LenovoComputerSystem.BootToBIOSSetup" + request_body: {} + + - name: Perform SecureBoot.ResetKeys action via PostResource command + community.general.xcc_redfish_command: + category: Raw + command: PostResource + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + resource_uri: "/redfish/v1/Systems/1/SecureBoot/Actions/SecureBoot.ResetKeys" + request_body: + ResetKeysType: DeleteAllKeys + + - name: Create session + community.general.redfish_command: + category: Sessions + command: CreateSession + baseuri: "{{ baseuri }}" + username: "{{ username }}" + password: "{{ password }}" + register: result + + - name: Update Manager DateTimeLocalOffset property using security token for auth + community.general.xcc_redfish_command: + category: Raw + command: PatchResource + baseuri: "{{ baseuri }}" + auth_token: "{{ result.session.token }}" + resource_uri: "/redfish/v1/Managers/1" + request_body: + DateTimeLocalOffset: "+08:00" + + - name: Delete session using security token created by CreateSesssion above + community.general.redfish_command: + category: Sessions + command: DeleteSession + baseuri: "{{ baseuri }}" + auth_token: "{{ result.session.token }}" + session_uri: "{{ result.session.uri }}" +''' + +RETURN = ''' +msg: + description: A message related to the performed action(s). + returned: when failure or action/update success + type: str + sample: "Action was successful" +redfish_facts: + description: Resource content. + returned: when command == GetResource or command == GetCollectionResource + type: dict + sample: '{ + "redfish_facts": { + "data": { + "@odata.etag": "\"3179bf00d69f25a8b3c\"", + "@odata.id": "/redfish/v1/Managers/1/NetworkProtocol/Oem/Lenovo/DNS", + "@odata.type": "#LenovoDNS.v1_0_0.LenovoDNS", + "DDNS": [ + { + "DDNSEnable": true, + "DomainName": "", + "DomainNameSource": "DHCP" + } + ], + "DNSEnable": true, + "Description": "This resource is used to represent a DNS resource for a Redfish implementation.", + "IPv4Address1": "10.103.62.178", + "IPv4Address2": "0.0.0.0", + "IPv4Address3": "0.0.0.0", + "IPv6Address1": "::", + "IPv6Address2": "::", + "IPv6Address3": "::", + "Id": "LenovoDNS", + "PreferredAddresstype": "IPv4" + }, + "ret": true + } + }' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible_collections.community.general.plugins.module_utils.redfish_utils import RedfishUtils + + +class XCCRedfishUtils(RedfishUtils): + @staticmethod + def _find_empty_virt_media_slot(resources, media_types, + media_match_strict=True): + for uri, data in resources.items(): + # check MediaTypes + if 'MediaTypes' in data and media_types: + if not set(media_types).intersection(set(data['MediaTypes'])): + continue + else: + if media_match_strict: + continue + if 'RDOC' in uri: + continue + # if ejected, 'Inserted' should be False and 'ImageName' cleared + if (not data.get('Inserted', False) and + not data.get('ImageName')): + return uri, data + return None, None + + def virtual_media_eject_one(self, image_url): + # locate and read the VirtualMedia resources + response = self.get_request(self.root_uri + self.manager_uri) + if response['ret'] is False: + return response + data = response['data'] + if 'VirtualMedia' not in data: + return {'ret': False, 'msg': "VirtualMedia resource not found"} + virt_media_uri = data["VirtualMedia"]["@odata.id"] + response = self.get_request(self.root_uri + virt_media_uri) + if response['ret'] is False: + return response + data = response['data'] + virt_media_list = [] + for member in data[u'Members']: + virt_media_list.append(member[u'@odata.id']) + resources, headers = self._read_virt_media_resources(virt_media_list) + + # find the VirtualMedia resource to eject + uri, data, eject = self._find_virt_media_to_eject(resources, image_url) + if uri and eject: + if ('Actions' not in data or + '#VirtualMedia.EjectMedia' not in data['Actions']): + # try to eject via PATCH if no EjectMedia action found + h = headers[uri] + if 'allow' in h: + methods = [m.strip() for m in h.get('allow').split(',')] + if 'PATCH' not in methods: + # if Allow header present and PATCH missing, return error + return {'ret': False, + 'msg': "%s action not found and PATCH not allowed" + % '#VirtualMedia.EjectMedia'} + return self.virtual_media_eject_via_patch(uri) + else: + # POST to the EjectMedia Action + action = data['Actions']['#VirtualMedia.EjectMedia'] + if 'target' not in action: + return {'ret': False, + 'msg': "target URI property missing from Action " + "#VirtualMedia.EjectMedia"} + action_uri = action['target'] + # empty payload for Eject action + payload = {} + # POST to action + response = self.post_request(self.root_uri + action_uri, + payload) + if response['ret'] is False: + return response + return {'ret': True, 'changed': True, + 'msg': "VirtualMedia ejected"} + elif uri and not eject: + # already ejected: return success but changed=False + return {'ret': True, 'changed': False, + 'msg': "VirtualMedia image '%s' already ejected" % + image_url} + else: + # return failure (no resources matching image_url found) + return {'ret': False, 'changed': False, + 'msg': "No VirtualMedia resource found with image '%s' " + "inserted" % image_url} + + def virtual_media_eject(self, options): + if options: + image_url = options.get('image_url') + if image_url: # eject specified one media + return self.virtual_media_eject_one(image_url) + + # eject all inserted media when no image_url specified + # read all the VirtualMedia resources + response = self.get_request(self.root_uri + self.manager_uri) + if response['ret'] is False: + return response + data = response['data'] + if 'VirtualMedia' not in data: + return {'ret': False, 'msg': "VirtualMedia resource not found"} + virt_media_uri = data["VirtualMedia"]["@odata.id"] + response = self.get_request(self.root_uri + virt_media_uri) + if response['ret'] is False: + return response + data = response['data'] + virt_media_list = [] + for member in data[u'Members']: + virt_media_list.append(member[u'@odata.id']) + resources, headers = self._read_virt_media_resources(virt_media_list) + + # eject all inserted media one by one + ejected_media_list = [] + for uri, data in resources.items(): + if data.get('Image') and data.get('Inserted', True): + returndict = self.virtual_media_eject_one(data.get('Image')) + if not returndict['ret']: + return returndict + ejected_media_list.append(data.get('Image')) + + if len(ejected_media_list) == 0: + # no media inserted: return success but changed=False + return {'ret': True, 'changed': False, + 'msg': "No VirtualMedia image inserted"} + else: + return {'ret': True, 'changed': True, + 'msg': "VirtualMedia %s ejected" % str(ejected_media_list)} + + def raw_get_resource(self, resource_uri): + if resource_uri is None: + return {'ret': False, 'msg': "resource_uri is missing"} + response = self.get_request(self.root_uri + resource_uri) + if response['ret'] is False: + return response + data = response['data'] + return {'ret': True, 'data': data} + + def raw_get_collection_resource(self, resource_uri): + if resource_uri is None: + return {'ret': False, 'msg': "resource_uri is missing"} + response = self.get_request(self.root_uri + resource_uri) + if response['ret'] is False: + return response + if 'Members' not in response['data']: + return {'ret': False, 'msg': "Specified resource_uri doesn't have Members property"} + member_list = [i['@odata.id'] for i in response['data'].get('Members', [])] + + # get member resource one by one + data_list = [] + for member_uri in member_list: + uri = self.root_uri + member_uri + response = self.get_request(uri) + if response['ret'] is False: + return response + data = response['data'] + data_list.append(data) + + return {'ret': True, 'data_list': data_list} + + def raw_patch_resource(self, resource_uri, request_body): + if resource_uri is None: + return {'ret': False, 'msg': "resource_uri is missing"} + if request_body is None: + return {'ret': False, 'msg': "request_body is missing"} + # check whether resource_uri existing or not + response = self.get_request(self.root_uri + resource_uri) + if response['ret'] is False: + return response + original_etag = response['data']['@odata.etag'] + + # check validity of keys in request_body + data = response['data'] + for key in request_body.keys(): + if key not in data: + return {'ret': False, 'msg': "Key %s not found. Supported key list: %s" % (key, str(data.keys()))} + + # perform patch + response = self.patch_request(self.root_uri + resource_uri, request_body) + if response['ret'] is False: + return response + + # check whether changed or not + current_etag = '' + if 'data' in response and '@odata.etag' in response['data']: + current_etag = response['data']['@odata.etag'] + if current_etag != original_etag: + return {'ret': True, 'changed': True} + else: + return {'ret': True, 'changed': False} + + def raw_post_resource(self, resource_uri, request_body): + if resource_uri is None: + return {'ret': False, 'msg': "resource_uri is missing"} + if '/Actions/' not in resource_uri: + return {'ret': False, 'msg': "Bad uri %s. Keyword /Actions/ should be included in uri" % resource_uri} + if request_body is None: + return {'ret': False, 'msg': "request_body is missing"} + # get action base uri data for further checking + action_base_uri = resource_uri.split('/Actions/')[0] + response = self.get_request(self.root_uri + action_base_uri) + if response['ret'] is False: + return response + if 'Actions' not in response['data']: + return {'ret': False, 'msg': "Actions property not found in %s" % action_base_uri} + + # check resouce_uri with target uri found in action base uri data + action_found = False + action_info_uri = None + action_target_uri_list = [] + for key in response['data']['Actions'].keys(): + if action_found: + break + if not key.startswith('#'): + continue + if 'target' in response['data']['Actions'][key]: + if resource_uri == response['data']['Actions'][key]['target']: + action_found = True + if '@Redfish.ActionInfo' in response['data']['Actions'][key]: + action_info_uri = response['data']['Actions'][key]['@Redfish.ActionInfo'] + else: + action_target_uri_list.append(response['data']['Actions'][key]['target']) + if not action_found and 'Oem' in response['data']['Actions']: + for key in response['data']['Actions']['Oem'].keys(): + if action_found: + break + if not key.startswith('#'): + continue + if 'target' in response['data']['Actions']['Oem'][key]: + if resource_uri == response['data']['Actions']['Oem'][key]['target']: + action_found = True + if '@Redfish.ActionInfo' in response['data']['Actions']['Oem'][key]: + action_info_uri = response['data']['Actions']['Oem'][key]['@Redfish.ActionInfo'] + else: + action_target_uri_list.append(response['data']['Actions']['Oem'][key]['target']) + + if not action_found: + return {'ret': False, + 'msg': 'Specified resource_uri is not a supported action target uri, please specify a supported target uri instead. Supported uri: %s' + % (str(action_target_uri_list))} + + # check request_body with parameter name defined by @Redfish.ActionInfo + if action_info_uri is not None: + response = self.get_request(self.root_uri + action_info_uri) + if response['ret'] is False: + return response + for key in request_body.keys(): + key_found = False + for para in response['data']['Parameters']: + if key == para['Name']: + key_found = True + break + if not key_found: + return {'ret': False, + 'msg': 'Invalid property %s found in request_body. Please refer to @Redfish.ActionInfo Parameters: %s' + % (key, str(response['data']['Parameters']))} + + # perform post + response = self.post_request(self.root_uri + resource_uri, request_body) + if response['ret'] is False: + return response + return {'ret': True, 'changed': True} + + +# More will be added as module features are expanded +CATEGORY_COMMANDS_ALL = { + "Manager": ["VirtualMediaInsert", + "VirtualMediaEject"], + "Raw": ["GetResource", + "GetCollectionResource", + "PatchResource", + "PostResource"] +} + + +def main(): + result = {} + module = AnsibleModule( + argument_spec=dict( + category=dict(required=True), + command=dict(required=True, type='list', elements='str'), + baseuri=dict(required=True), + username=dict(), + password=dict(no_log=True), + auth_token=dict(no_log=True), + timeout=dict(type='int', default=10), + resource_id=dict(), + virtual_media=dict( + type='dict', + options=dict( + media_types=dict(type='list', elements='str', default=[]), + image_url=dict(), + inserted=dict(type='bool', default=True), + write_protected=dict(type='bool', default=True), + username=dict(), + password=dict(no_log=True), + transfer_protocol_type=dict(), + transfer_method=dict(), + ) + ), + resource_uri=dict(), + request_body=dict( + type='dict', + ), + ), + required_together=[ + ('username', 'password'), + ], + required_one_of=[ + ('username', 'auth_token'), + ], + mutually_exclusive=[ + ('username', 'auth_token'), + ], + 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 + timeout = module.params['timeout'] + + # System, Manager or Chassis ID to modify + resource_id = module.params['resource_id'] + + # VirtualMedia options + virtual_media = module.params['virtual_media'] + + # resource_uri + resource_uri = module.params['resource_uri'] + + # request_body + request_body = module.params['request_body'] + + # Build root URI + root_uri = "https://" + module.params['baseuri'] + rf_utils = XCCRedfishUtils(creds, root_uri, timeout, module, resource_id=resource_id, data_modification=True) + + # Check that Category is valid + if category not in CATEGORY_COMMANDS_ALL: + module.fail_json(msg=to_native("Invalid Category '%s'. Valid Categories = %s" % (category, CATEGORY_COMMANDS_ALL.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_ALL[category]: + module.fail_json(msg=to_native("Invalid Command '%s'. Valid Commands = %s" % (cmd, CATEGORY_COMMANDS_ALL[category]))) + + # Organize by Categories / Commands + if category == "Manager": + # execute only if we find a Manager service resource + result = rf_utils._find_managers_resource() + if result['ret'] is False: + module.fail_json(msg=to_native(result['msg'])) + + for command in command_list: + if command == 'VirtualMediaInsert': + result = rf_utils.virtual_media_insert(virtual_media) + elif command == 'VirtualMediaEject': + result = rf_utils.virtual_media_eject(virtual_media) + elif category == "Raw": + for command in command_list: + if command == 'GetResource': + result = rf_utils.raw_get_resource(resource_uri) + elif command == 'GetCollectionResource': + result = rf_utils.raw_get_collection_resource(resource_uri) + elif command == 'PatchResource': + result = rf_utils.raw_patch_resource(resource_uri, request_body) + elif command == 'PostResource': + result = rf_utils.raw_post_resource(resource_uri, request_body) + + # Return data back or fail with proper message + if result['ret'] is True: + if command == 'GetResource' or command == 'GetCollectionResource': + module.exit_json(redfish_facts=result) + else: + changed = result.get('changed', True) + msg = result.get('msg', 'Action was successful') + module.exit_json(changed=changed, msg=msg) + else: + module.fail_json(msg=to_native(result['msg'])) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/xcc_redfish_command.py b/plugins/modules/xcc_redfish_command.py new file mode 120000 index 0000000000..4fa967b410 --- /dev/null +++ b/plugins/modules/xcc_redfish_command.py @@ -0,0 +1 @@ +remote_management/lenovoxcc/xcc_redfish_command.py \ No newline at end of file diff --git a/tests/unit/plugins/modules/remote_management/lenovoxcc/test_xcc_redfish_command.py b/tests/unit/plugins/modules/remote_management/lenovoxcc/test_xcc_redfish_command.py new file mode 100644 index 0000000000..38a6652fb1 --- /dev/null +++ b/tests/unit/plugins/modules/remote_management/lenovoxcc/test_xcc_redfish_command.py @@ -0,0 +1,626 @@ +# 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 + +import json +from ansible_collections.community.general.tests.unit.compat import mock +from ansible_collections.community.general.tests.unit.compat.mock import patch +from ansible_collections.community.general.tests.unit.compat import unittest +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes +import ansible_collections.community.general.plugins.modules.remote_management.lenovoxcc.xcc_redfish_command as module +from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson +from ansible_collections.community.general.tests.unit.plugins.modules.utils import set_module_args, exit_json, fail_json + + +def get_bin_path(self, arg, required=False): + """Mock AnsibleModule.get_bin_path""" + return arg + + +class TestXCCRedfishCommand(unittest.TestCase): + + def setUp(self): + self.mock_module_helper = patch.multiple(basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + get_bin_path=get_bin_path) + self.mock_module_helper.start() + self.addCleanup(self.mock_module_helper.stop) + + def test_module_fail_when_required_args_missing(self): + with self.assertRaises(AnsibleFailJson): + set_module_args({}) + module.main() + + def test_module_fail_when_unknown_category(self): + with self.assertRaises(AnsibleFailJson): + set_module_args({ + 'category': 'unknown', + 'command': 'VirtualMediaEject', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + }) + module.main() + + def test_module_fail_when_unknown_command(self): + with self.assertRaises(AnsibleFailJson): + set_module_args({ + 'category': 'Manager', + 'command': 'unknown', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + }) + module.main() + + def test_module_command_VirtualMediaInsert_pass(self): + set_module_args({ + 'category': 'Manager', + 'command': 'VirtualMediaInsert', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'timeout': 30, + 'virtual_media': { + 'image_url': "nfs://10.245.52.18:/home/nfs/bootable-sr635-20210111-autorun.iso", + 'media_types': ['CD'], + 'inserted': True, + 'write_protected': True, + 'transfer_protocol_type': 'NFS' + } + }) + with patch.object(module.XCCRedfishUtils, '_find_managers_resource') as mock__find_managers_resource: + mock__find_managers_resource.return_value = {'ret': True, 'changed': True, 'msg': 'success'} + + with patch.object(module.XCCRedfishUtils, 'virtual_media_insert') as mock_virtual_media_insert: + mock_virtual_media_insert.return_value = {'ret': True, 'changed': True, 'msg': 'success'} + + with self.assertRaises(AnsibleExitJson) as result: + module.main() + + def test_module_command_VirtualMediaEject_pass(self): + set_module_args({ + 'category': 'Manager', + 'command': 'VirtualMediaEject', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'timeout': 30, + 'virtual_media': { + 'image_url': "nfs://10.245.52.18:/home/nfs/bootable-sr635-20210111-autorun.iso", + } + }) + with patch.object(module.XCCRedfishUtils, '_find_managers_resource') as mock__find_managers_resource: + mock__find_managers_resource.return_value = {'ret': True, 'changed': True, 'msg': 'success'} + + with patch.object(module.XCCRedfishUtils, 'virtual_media_eject') as mock_virtual_media_eject: + mock_virtual_media_eject.return_value = {'ret': True, 'changed': True, 'msg': 'success'} + + with self.assertRaises(AnsibleExitJson) as result: + module.main() + + def test_module_command_VirtualMediaEject_fail_when_required_args_missing(self): + with self.assertRaises(AnsibleFailJson): + set_module_args({ + 'category': 'Manager', + 'command': 'VirtualMediaEject', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + }) + module.main() + + def test_module_command_GetResource_fail_when_required_args_missing(self): + set_module_args({ + 'category': 'Raw', + 'command': 'GetResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx'}} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_GetResource_fail_when_get_return_false(self): + set_module_args({ + 'category': 'Raw', + 'command': 'GetResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': False, 'msg': '404 error'} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_GetResource_pass(self): + set_module_args({ + 'category': 'Raw', + 'command': 'GetResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx'}} + + with self.assertRaises(AnsibleExitJson) as result: + module.main() + + def test_module_command_GetCollectionResource_fail_when_required_args_missing(self): + set_module_args({ + 'category': 'Raw', + 'command': 'GetCollectionResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx'}} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_GetCollectionResource_fail_when_get_return_false(self): + set_module_args({ + 'category': 'Raw', + 'command': 'GetCollectionResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': False, 'msg': '404 error'} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_GetCollectionResource_fail_when_get_not_colection(self): + set_module_args({ + 'category': 'Raw', + 'command': 'GetCollectionResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx'}} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_GetCollectionResource_pass_when_get_empty_collection(self): + set_module_args({ + 'category': 'Raw', + 'command': 'GetCollectionResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'Members': [], 'Members@odata.count': 0}} + + with self.assertRaises(AnsibleExitJson) as result: + module.main() + + def test_module_command_GetCollectionResource_pass_when_get_collection(self): + set_module_args({ + 'category': 'Raw', + 'command': 'GetCollectionResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'Members': [{'@odata.id': '/redfish/v1/testuri/1'}], 'Members@odata.count': 1}} + + with self.assertRaises(AnsibleExitJson) as result: + module.main() + + def test_module_command_PatchResource_fail_when_required_args_missing(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PatchResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx', '@odata.etag': '27f6eb13fa1c28a2711'}} + + with patch.object(module.XCCRedfishUtils, 'patch_request') as mock_patch_request: + mock_patch_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx'}} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PatchResource_fail_when_required_args_missing_no_requestbody(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PatchResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx', '@odata.etag': '27f6eb13fa1c28a2711'}} + + with patch.object(module.XCCRedfishUtils, 'patch_request') as mock_patch_request: + mock_patch_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx'}} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PatchResource_fail_when_noexisting_property_in_requestbody(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PatchResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + 'request_body': {'teststr': 'yyyy', 'otherkey': 'unknownkey'} + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx', '@odata.etag': '27f6eb13fa1c28a2711'}} + + with patch.object(module.XCCRedfishUtils, 'patch_request') as mock_patch_request: + mock_patch_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx'}} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PatchResource_fail_when_get_return_false(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PatchResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + 'request_body': {'teststr': 'yyyy'} + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx', '@odata.etag': '27f6eb13fa1c28a2711'}} + + with patch.object(module.XCCRedfishUtils, 'patch_request') as mock_patch_request: + mock_patch_request.return_value = {'ret': False, 'msg': '500 internal error'} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PatchResource_pass(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PatchResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + 'request_body': {'teststr': 'yyyy'} + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': True, 'data': {'teststr': 'xxxx', '@odata.etag': '27f6eb13fa1c28a2711'}} + + with patch.object(module.XCCRedfishUtils, 'patch_request') as mock_patch_request: + mock_patch_request.return_value = {'ret': True, 'data': {'teststr': 'yyyy', '@odata.etag': '322e0d45d9572723c98'}} + + with self.assertRaises(AnsibleExitJson) as result: + module.main() + + def test_module_command_PostResource_fail_when_required_args_missing(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PostResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = { + 'ret': True, + 'data': { + 'Actions': { + '#Bios.ChangePassword': { + '@Redfish.ActionInfo': "/redfish/v1/Systems/1/Bios/ChangePasswordActionInfo", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword", + 'title': "ChangePassword", + 'PasswordName@Redfish.AllowableValues': [ + "UefiAdminPassword", + "UefiPowerOnPassword" + ] + }, + '#Bios.ResetBios': { + 'title': "ResetBios", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios" + } + }, + } + } + + with patch.object(module.XCCRedfishUtils, 'post_request') as mock_post_request: + mock_post_request.return_value = {'ret': True} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PostResource_fail_when_invalid_resourceuri(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PostResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/testuri', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = { + 'ret': True, + 'data': { + 'Actions': { + '#Bios.ChangePassword': { + '@Redfish.ActionInfo': "/redfish/v1/Systems/1/Bios/ChangePasswordActionInfo", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword", + 'title': "ChangePassword", + 'PasswordName@Redfish.AllowableValues': [ + "UefiAdminPassword", + "UefiPowerOnPassword" + ] + }, + '#Bios.ResetBios': { + 'title': "ResetBios", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios" + } + }, + } + } + + with patch.object(module.XCCRedfishUtils, 'post_request') as mock_post_request: + mock_post_request.return_value = {'ret': True} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PostResource_fail_when_no_requestbody(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PostResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = { + 'ret': True, + 'data': { + 'Actions': { + '#Bios.ChangePassword': { + '@Redfish.ActionInfo': "/redfish/v1/Systems/1/Bios/ChangePasswordActionInfo", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword", + 'title': "ChangePassword", + 'PasswordName@Redfish.AllowableValues': [ + "UefiAdminPassword", + "UefiPowerOnPassword" + ] + }, + '#Bios.ResetBios': { + 'title': "ResetBios", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios" + } + }, + } + } + + with patch.object(module.XCCRedfishUtils, 'post_request') as mock_post_request: + mock_post_request.return_value = {'ret': True} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PostResource_fail_when_no_requestbody(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PostResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword', + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = { + 'ret': True, + 'data': { + 'Actions': { + '#Bios.ChangePassword': { + '@Redfish.ActionInfo': "/redfish/v1/Systems/1/Bios/ChangePasswordActionInfo", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword", + 'title': "ChangePassword", + 'PasswordName@Redfish.AllowableValues': [ + "UefiAdminPassword", + "UefiPowerOnPassword" + ] + }, + '#Bios.ResetBios': { + 'title': "ResetBios", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios" + } + }, + } + } + + with patch.object(module.XCCRedfishUtils, 'post_request') as mock_post_request: + mock_post_request.return_value = {'ret': True} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PostResource_fail_when_requestbody_mismatch_with_data_from_actioninfo_uri(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PostResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword', + 'request_body': {'PasswordName': 'UefiAdminPassword', 'NewPassword': 'PASSW0RD=='} + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = { + 'ret': True, + 'data': { + 'Parameters': [], + 'Actions': { + '#Bios.ChangePassword': { + '@Redfish.ActionInfo': "/redfish/v1/Systems/1/Bios/ChangePasswordActionInfo", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword", + 'title': "ChangePassword", + 'PasswordName@Redfish.AllowableValues': [ + "UefiAdminPassword", + "UefiPowerOnPassword" + ] + }, + '#Bios.ResetBios': { + 'title': "ResetBios", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios" + } + }, + } + } + + with patch.object(module.XCCRedfishUtils, 'post_request') as mock_post_request: + mock_post_request.return_value = {'ret': True} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PostResource_fail_when_get_return_false(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PostResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword', + 'request_body': {'PasswordName': 'UefiAdminPassword', 'NewPassword': 'PASSW0RD=='} + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = {'ret': False, 'msg': '404 error'} + + with patch.object(module.XCCRedfishUtils, 'post_request') as mock_post_request: + mock_post_request.return_value = {'ret': True} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PostResource_fail_when_post_return_false(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PostResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios', + 'request_body': {} + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = { + 'ret': True, + 'data': { + 'Actions': { + '#Bios.ChangePassword': { + '@Redfish.ActionInfo': "/redfish/v1/Systems/1/Bios/ChangePasswordActionInfo", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword", + 'title': "ChangePassword", + 'PasswordName@Redfish.AllowableValues': [ + "UefiAdminPassword", + "UefiPowerOnPassword" + ] + }, + '#Bios.ResetBios': { + 'title': "ResetBios", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios" + } + }, + } + } + + with patch.object(module.XCCRedfishUtils, 'post_request') as mock_post_request: + mock_post_request.return_value = {'ret': False, 'msg': '500 internal error'} + + with self.assertRaises(AnsibleFailJson) as result: + module.main() + + def test_module_command_PostResource_pass(self): + set_module_args({ + 'category': 'Raw', + 'command': 'PostResource', + 'baseuri': '10.245.39.251', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_uri': '/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios', + 'request_body': {} + }) + + with patch.object(module.XCCRedfishUtils, 'get_request') as mock_get_request: + mock_get_request.return_value = { + 'ret': True, + 'data': { + 'Actions': { + '#Bios.ChangePassword': { + '@Redfish.ActionInfo': "/redfish/v1/Systems/1/Bios/ChangePasswordActionInfo", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ChangePassword", + 'title': "ChangePassword", + 'PasswordName@Redfish.AllowableValues': [ + "UefiAdminPassword", + "UefiPowerOnPassword" + ] + }, + '#Bios.ResetBios': { + 'title': "ResetBios", + 'target': "/redfish/v1/Systems/1/Bios/Actions/Bios.ResetBios" + } + }, + } + } + + with patch.object(module.XCCRedfishUtils, 'post_request') as mock_post_request: + mock_post_request.return_value = {'ret': True, 'msg': 'post success'} + + with self.assertRaises(AnsibleExitJson) as result: + module.main()