diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index f5bad2bbb6..00a27cd837 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -139,7 +139,6 @@ files: $module_utils/redfish_utils.py: maintainers: $team_redfish labels: redfish_utils - $module_utils/remote_management/dellemc/: rajeevarakkal $module_utils/remote_management/lxca/common.py: navalkp prabhosa $module_utils/scaleway.py: maintainers: $team_scaleway @@ -691,12 +690,6 @@ files: maintainers: matze $modules/remote_management/cobbler/: maintainers: dagwieers - $modules/remote_management/dellemc/: - maintainers: rajeevarakkal - $modules/remote_management/dellemc/idrac_server_config_profile.py: - maintainers: jagadeeshnv - $modules/remote_management/dellemc/ome_device_info.py: - maintainers: Sajna-Shetty $modules/remote_management/hpilo/: maintainers: haad ignore: dagwieers diff --git a/changelogs/fragments/948-dellemc-migration-removal.yml b/changelogs/fragments/948-dellemc-migration-removal.yml new file mode 100644 index 0000000000..c4f64a815f --- /dev/null +++ b/changelogs/fragments/948-dellemc-migration-removal.yml @@ -0,0 +1,13 @@ +removed_features: + - | + The ``ome_device_info``, ``idrac_firmware`` and ``idrac_server_config_profile`` modules have now been migrated from community.general to the `dellemc.openmanage `_ Ansible collection. + If you use ansible-base 2.10 or newer, redirections have been provided. + + If you use Ansible 2.9 and installed this collection, you need to adjust the FQCNs (``community.general.idrac_firmware`` → ``dellemc.openmanage.idrac_firmware``) and make sure to install the dellemc.openmanage collection. +breaking_changes: + - | + If you use Ansible 2.9 and these plugins or modules from this collection, community.general 3.0.0 results in errors when trying to use the DellEMC content by FQCN, like ``community.general.idrac_firmware``. + Since Ansible 2.9 is not able to use redirections, you will have to adjust your playbooks and roles manually to use the new FQCNs (``dellemc.openmanage.idrac_firmware`` for the previous example) and to make sure that you have ``dellemc.openmanage`` installed. + + If you use ansible-base 2.10 or newer and did not install Ansible 4.0.0, but installed (and/or upgraded) community.general manually, you need to make sure to also install the ``dellemc.openmanage`` collection if you are using any of these plugins or modules. + While ansible-base 2.10 or newer can use the redirects that community.general 3.0.0 adds, the collection they point to (such as dellemc.openmanage) must be installed for them to work. diff --git a/meta/runtime.yml b/meta/runtime.yml index 91b5869f69..1d599d7728 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -199,10 +199,14 @@ plugin_routing: deprecation: removal_version: 3.0.0 warning_text: see plugin documentation for details + idrac_firmware: + redirect: dellemc.openmanage.idrac_firmware idrac_redfish_facts: deprecation: removal_version: 3.0.0 warning_text: see plugin documentation for details + idrac_server_config_profile: + redirect: dellemc.openmanage.idrac_server_config_profile jenkins_job_facts: deprecation: removal_version: 3.0.0 @@ -283,6 +287,8 @@ plugin_routing: deprecation: removal_version: 3.0.0 warning_text: see plugin documentation for details + ome_device_info: + redirect: dellemc.openmanage.ome_device_info one_image_facts: deprecation: removal_version: 3.0.0 @@ -565,6 +571,8 @@ plugin_routing: postgresql: redirect: community.postgresql.postgresql module_utils: + remote_management.dellemc.dellemc_idrac: + redirect: dellemc.openmanage.dellemc_idrac docker.common: redirect: community.docker.common docker.swarm: @@ -579,6 +587,8 @@ plugin_routing: redirect: community.hrobot.robot kubevirt: redirect: community.kubevirt.kubevirt + remote_management.dellemc.ome: + redirect: dellemc.openmanage.ome postgresql: redirect: community.postgresql.postgresql callback: diff --git a/plugins/module_utils/remote_management/dellemc/__init__.py b/plugins/module_utils/remote_management/dellemc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/module_utils/remote_management/dellemc/dellemc_idrac.py b/plugins/module_utils/remote_management/dellemc/dellemc_idrac.py deleted file mode 100644 index 93d3bfcb74..0000000000 --- a/plugins/module_utils/remote_management/dellemc/dellemc_idrac.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- - -# -# Dell EMC OpenManage Ansible Modules -# Version 1.0 -# Copyright (C) 2018 Dell Inc. - -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# All rights reserved. Dell, EMC, and other trademarks are trademarks of Dell Inc. or its subsidiaries. -# Other trademarks may be trademarks of their respective owners. -# - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -try: - from omsdk.sdkinfra import sdkinfra - from omsdk.sdkcreds import UserCredentials - from omsdk.sdkfile import FileOnShare, file_share_manager - from omsdk.sdkprotopref import ProtoPreference, ProtocolEnum - from omsdk.http.sdkwsmanbase import WsManOptions - HAS_OMSDK = True -except ImportError: - HAS_OMSDK = False - - -class iDRACConnection: - - def __init__(self, module_params): - if not HAS_OMSDK: - raise ImportError("Dell EMC OMSDK library is required for this module") - self.idrac_ip = module_params['idrac_ip'] - self.idrac_user = module_params['idrac_user'] - self.idrac_pwd = module_params['idrac_password'] - self.idrac_port = module_params['idrac_port'] - if not all((self.idrac_ip, self.idrac_user, self.idrac_pwd)): - raise ValueError("hostname, username and password required") - self.handle = None - self.creds = UserCredentials(self.idrac_user, self.idrac_pwd) - self.pOp = WsManOptions(port=self.idrac_port) - self.sdk = sdkinfra() - if self.sdk is None: - msg = "Could not initialize iDRAC drivers." - raise RuntimeError(msg) - - def __enter__(self): - self.sdk.importPath() - self.handle = self.sdk.get_driver(self.sdk.driver_enum.iDRAC, self.idrac_ip, self.creds, pOptions=self.pOp) - if self.handle is None: - msg = "Could not find device driver for iDRAC with IP Address: {0}".format(self.idrac_ip) - raise RuntimeError(msg) - return self.handle - - def __exit__(self, exc_type, exc_val, exc_tb): - self.handle.disconnect() - return False diff --git a/plugins/module_utils/remote_management/dellemc/ome.py b/plugins/module_utils/remote_management/dellemc/ome.py deleted file mode 100644 index 9d02e55004..0000000000 --- a/plugins/module_utils/remote_management/dellemc/ome.py +++ /dev/null @@ -1,163 +0,0 @@ -# -*- coding: utf-8 -*- - -# Dell EMC OpenManage Ansible Modules -# Version 1.3 -# Copyright (C) 2019 Dell Inc. or its subsidiaries. All Rights Reserved. -# -# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import json -from ansible.module_utils.urls import open_url, ConnectionError, SSLValidationError -from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError -from ansible.module_utils.six.moves.urllib.parse import urlencode - -SESSION_RESOURCE_COLLECTION = { - "SESSION": "SessionService/Sessions", - "SESSION_ID": "SessionService/Sessions('{Id}')", -} - - -class OpenURLResponse(object): - """Handles HTTPResponse""" - - def __init__(self, resp): - self.body = None - self.resp = resp - if self.resp: - self.body = self.resp.read() - - @property - def json_data(self): - try: - return json.loads(self.body) - except ValueError: - raise ValueError("Unable to parse json") - - @property - def status_code(self): - return self.resp.getcode() - - @property - def success(self): - return self.status_code in (200, 201, 202, 204) - - @property - def token_header(self): - return self.resp.headers.get('X-Auth-Token') - - -class RestOME(object): - """Handles OME API requests""" - - def __init__(self, module_params=None, req_session=False): - self.module_params = module_params - self.hostname = self.module_params["hostname"] - self.username = self.module_params["username"] - self.password = self.module_params["password"] - self.port = self.module_params["port"] - self.req_session = req_session - self.session_id = None - self.protocol = 'https' - self._headers = {'Content-Type': 'application/json', 'Accept': 'application/json'} - - def _get_base_url(self): - """builds base url""" - return '{0}://{1}:{2}/api'.format(self.protocol, self.hostname, self.port) - - def _build_url(self, path, query_param=None): - """builds complete url""" - url = path - base_uri = self._get_base_url() - if path: - url = '{0}/{1}'.format(base_uri, path) - if query_param: - url += "?{0}".format(urlencode(query_param)) - return url - - def _url_common_args_spec(self, method, api_timeout, headers=None): - """Creates an argument common spec""" - req_header = self._headers - if headers: - req_header.update(headers) - url_kwargs = { - "method": method, - "validate_certs": False, - "use_proxy": True, - "headers": req_header, - "timeout": api_timeout, - "follow_redirects": 'all', - } - return url_kwargs - - def _args_without_session(self, method, api_timeout=30, headers=None): - """Creates an argument spec in case of basic authentication""" - req_header = self._headers - if headers: - req_header.update(headers) - url_kwargs = self._url_common_args_spec(method, api_timeout, headers=headers) - url_kwargs["url_username"] = self.username - url_kwargs["url_password"] = self.password - url_kwargs["force_basic_auth"] = True - return url_kwargs - - def _args_with_session(self, method, api_timeout=30, headers=None): - """Creates an argument spec, in case of authentication with session""" - url_kwargs = self._url_common_args_spec(method, api_timeout, headers=headers) - url_kwargs["force_basic_auth"] = False - return url_kwargs - - def invoke_request(self, method, path, data=None, query_param=None, headers=None, - api_timeout=30, dump=True): - """ - Sends a request via open_url - Returns :class:`OpenURLResponse` object. - :arg method: HTTP verb to use for the request - :arg path: path to request without query parameter - :arg data: (optional) Payload to send with the request - :arg query_param: (optional) Dictionary of query parameter to send with request - :arg headers: (optional) Dictionary of HTTP Headers to send with the - request - :arg api_timeout: (optional) How long to wait for the server to send - data before giving up - :arg dump: (Optional) boolean value for dumping payload data. - :returns: OpenURLResponse - """ - try: - if 'X-Auth-Token' in self._headers: - url_kwargs = self._args_with_session(method, api_timeout, headers=headers) - else: - url_kwargs = self._args_without_session(method, api_timeout, headers=headers) - if data and dump: - data = json.dumps(data) - url = self._build_url(path, query_param=query_param) - resp = open_url(url, data=data, **url_kwargs) - resp_data = OpenURLResponse(resp) - except (HTTPError, URLError, SSLValidationError, ConnectionError) as err: - raise err - return resp_data - - def __enter__(self): - """Creates sessions by passing it to header""" - if self.req_session: - payload = {'UserName': self.username, - 'Password': self.password, - 'SessionType': 'API', } - path = SESSION_RESOURCE_COLLECTION["SESSION"] - resp = self.invoke_request('POST', path, data=payload) - if resp and resp.success: - self.session_id = resp.json_data.get("Id") - self._headers["X-Auth-Token"] = resp.token_header - else: - msg = "Could not create the session" - raise ConnectionError(msg) - return self - - def __exit__(self, exc_type, exc_value, traceback): - """Deletes a session id, which is in use for request""" - if self.session_id: - path = SESSION_RESOURCE_COLLECTION["SESSION_ID"].format(Id=self.session_id) - self.invoke_request('DELETE', path) - return False diff --git a/plugins/modules/idrac_firmware.py b/plugins/modules/idrac_firmware.py deleted file mode 120000 index cb7e8da471..0000000000 --- a/plugins/modules/idrac_firmware.py +++ /dev/null @@ -1 +0,0 @@ -./remote_management/dellemc/idrac_firmware.py \ No newline at end of file diff --git a/plugins/modules/idrac_server_config_profile.py b/plugins/modules/idrac_server_config_profile.py deleted file mode 120000 index ff98a9d1aa..0000000000 --- a/plugins/modules/idrac_server_config_profile.py +++ /dev/null @@ -1 +0,0 @@ -./remote_management/dellemc/idrac_server_config_profile.py \ No newline at end of file diff --git a/plugins/modules/ome_device_info.py b/plugins/modules/ome_device_info.py deleted file mode 120000 index ccca666861..0000000000 --- a/plugins/modules/ome_device_info.py +++ /dev/null @@ -1 +0,0 @@ -./remote_management/dellemc/ome_device_info.py \ No newline at end of file diff --git a/plugins/modules/remote_management/dellemc/idrac_firmware.py b/plugins/modules/remote_management/dellemc/idrac_firmware.py deleted file mode 100644 index fa8ac66ce5..0000000000 --- a/plugins/modules/remote_management/dellemc/idrac_firmware.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# -# Dell EMC OpenManage Ansible Modules -# Version 2.0 -# Copyright (C) 2018-2019 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_firmware -short_description: Firmware update from a repository on a network share (CIFS, NFS). -description: - - Update the Firmware by connecting to a network share (either CIFS or NFS) that contains a catalog of - available updates. - - Network share should contain a valid repository of Update Packages (DUPs) and a catalog file describing the DUPs. - - All applicable updates contained in the repository are applied to the system. - - This feature is available only with iDRAC Enterprise License. -options: - idrac_ip: - description: iDRAC IP Address. - type: str - required: True - idrac_user: - description: iDRAC username. - type: str - required: True - idrac_password: - description: iDRAC user password. - type: str - required: True - aliases: ['idrac_pwd'] - idrac_port: - description: iDRAC port. - type: int - default: 443 - share_name: - description: CIFS or NFS Network share. - type: str - required: True - share_user: - description: Network share user in the format 'user@domain' or 'domain\\user' if user is - part of a domain else 'user'. This option is mandatory for CIFS Network Share. - type: str - share_password: - description: Network share user password. This option is mandatory for CIFS Network Share. - type: str - aliases: ['share_pwd'] - share_mnt: - description: Local mount path of the network share with read-write permission for ansible user. - This option is mandatory for Network Share. - type: str - required: True - reboot: - description: Whether to reboots after applying the updates or not. - type: bool - default: false - job_wait: - description: Whether to wait for job completion or not. - type: bool - default: true - catalog_file_name: - required: False - description: Catalog file name relative to the I(share_name). - type: str - default: 'Catalog.xml' - -requirements: - - "omsdk" - - "python >= 2.7.5" -author: "Rajeev Arakkal (@rajeevarakkal)" -''' - -EXAMPLES = """ ---- -- name: Update firmware from repository on a Network Share - community.general.idrac_firmware: - idrac_ip: "192.168.0.1" - idrac_user: "user_name" - idrac_password: "user_password" - share_name: "192.168.0.0:/share" - share_user: "share_user_name" - share_password: "share_user_pwd" - share_mnt: "/mnt/share" - reboot: True - job_wait: True - catalog_file_name: "Catalog.xml" -""" - -RETURN = """ ---- -msg: - type: str - description: Over all firmware update status. - returned: always - sample: "Successfully updated the firmware." -update_status: - type: dict - description: Firmware Update job and progress details from the iDRAC. - returned: success - sample: { - 'InstanceID': 'JID_XXXXXXXXXXXX', - 'JobState': 'Completed', - 'Message': 'Job completed successfully.', - 'MessageId': 'REDXXX', - 'Name': 'Repository Update', - 'JobStartTime': 'NA', - 'Status': 'Success', - } -""" - - -from ansible_collections.community.general.plugins.module_utils.remote_management.dellemc.dellemc_idrac import iDRACConnection -from ansible.module_utils.basic import AnsibleModule -try: - from omsdk.sdkcreds import UserCredentials - from omsdk.sdkfile import FileOnShare - HAS_OMSDK = True -except ImportError: - HAS_OMSDK = False - - -def _validate_catalog_file(catalog_file_name): - normilized_file_name = catalog_file_name.lower() - if not normilized_file_name: - raise ValueError('catalog_file_name should be a non-empty string.') - elif not normilized_file_name.endswith("xml"): - raise ValueError('catalog_file_name should be an XML file.') - - -def update_firmware(idrac, module): - """Update firmware from a network share and return the job details.""" - msg = {} - msg['changed'] = False - msg['update_status'] = {} - - try: - upd_share = FileOnShare(remote=module.params['share_name'] + "/" + module.params['catalog_file_name'], - mount_point=module.params['share_mnt'], - isFolder=False, - creds=UserCredentials( - module.params['share_user'], - module.params['share_password']) - ) - - idrac.use_redfish = True - if '12' in idrac.ServerGeneration or '13' in idrac.ServerGeneration: - idrac.use_redfish = False - - apply_update = True - msg['update_status'] = idrac.update_mgr.update_from_repo(upd_share, - apply_update, - module.params['reboot'], - module.params['job_wait']) - except RuntimeError as e: - module.fail_json(msg=str(e)) - - if "Status" in msg['update_status']: - if msg['update_status']['Status'] == "Success": - if module.params['job_wait']: - msg['changed'] = True - else: - module.fail_json(msg='Failed to update firmware.', update_status=msg['update_status']) - return msg - - -def main(): - module = AnsibleModule( - argument_spec={ - "idrac_ip": {"required": True, "type": 'str'}, - "idrac_user": {"required": True, "type": 'str'}, - "idrac_password": {"required": True, "type": 'str', "aliases": ['idrac_pwd'], "no_log": True}, - "idrac_port": {"required": False, "default": 443, "type": 'int'}, - - "share_name": {"required": True, "type": 'str'}, - "share_user": {"required": False, "type": 'str'}, - "share_password": {"required": False, "type": 'str', "aliases": ['share_pwd'], "no_log": True}, - "share_mnt": {"required": True, "type": 'str'}, - - "catalog_file_name": {"required": False, "type": 'str', "default": "Catalog.xml"}, - "reboot": {"required": False, "type": 'bool', "default": False}, - "job_wait": {"required": False, "type": 'bool', "default": True}, - }, - - supports_check_mode=False) - - try: - # Validate the catalog file - _validate_catalog_file(module.params['catalog_file_name']) - # Connect to iDRAC and update firmware - with iDRACConnection(module.params) as idrac: - update_status = update_firmware(idrac, module) - except (ImportError, ValueError, RuntimeError) as e: - module.fail_json(msg=str(e)) - - module.exit_json(msg='Successfully updated the firmware.', update_status=update_status) - - -if __name__ == '__main__': - main() diff --git a/plugins/modules/remote_management/dellemc/idrac_server_config_profile.py b/plugins/modules/remote_management/dellemc/idrac_server_config_profile.py deleted file mode 100644 index 39857fd30a..0000000000 --- a/plugins/modules/remote_management/dellemc/idrac_server_config_profile.py +++ /dev/null @@ -1,301 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# -# Dell EMC OpenManage Ansible Modules -# Version 2.0 -# Copyright (C) 2019 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_server_config_profile -short_description: Export or Import iDRAC Server Configuration Profile (SCP). -description: - - Export the Server Configuration Profile (SCP) from the iDRAC or Import from a network share or a local file. -options: - idrac_ip: - description: iDRAC IP Address. - type: str - required: True - idrac_user: - description: iDRAC username. - type: str - required: True - idrac_password: - description: iDRAC user password. - type: str - required: True - aliases: ['idrac_pwd'] - idrac_port: - description: iDRAC port. - type: int - default: 443 - command: - description: - - If C(import), will perform SCP import operations. - - If C(export), will perform SCP export operations. - choices: ['import', 'export'] - default: 'export' - job_wait: - description: Whether to wait for job completion or not. - type: bool - required: True - share_name: - description: CIFS or NFS Network Share or a local path. - type: str - required: True - share_user: - description: Network share user in the format 'user@domain' or 'domain\\user' if user is - part of a domain else 'user'. This option is mandatory for CIFS Network Share. - type: str - share_password: - description: Network share user password. This option is mandatory for CIFS Network Share. - type: str - aliases: ['share_pwd'] - scp_file: - description: Server Configuration Profile file name. This option is mandatory for C(import) command. - type: str - scp_components: - description: - - If C(ALL), this module will import all components configurations from SCP file. - - If C(IDRAC), this module will import iDRAC configuration from SCP file. - - If C(BIOS), this module will import BIOS configuration from SCP file. - - If C(NIC), this module will import NIC configuration from SCP file. - - If C(RAID), this module will import RAID configuration from SCP file. - choices: ['ALL', 'IDRAC', 'BIOS', 'NIC', 'RAID'] - default: 'ALL' - shutdown_type: - description: - - This option is applicable for C(import) command. - - If C(Graceful), it gracefully shuts down the server. - - If C(Forced), it forcefully shuts down the server. - - If C(NoReboot), it does not reboot the server. - choices: ['Graceful', 'Forced', 'NoReboot'] - default: 'Graceful' - end_host_power_state: - description: - - This option is applicable for C(import) command. - - If C(On), End host power state is on. - - If C(Off), End host power state is off. - choices: ['On' ,'Off'] - default: 'On' - export_format: - description: Specify the output file format. This option is applicable for C(export) command. - choices: ['JSON', 'XML'] - default: 'XML' - export_use: - description: Specify the type of server configuration profile (SCP) to be exported. - This option is applicable for C(export) command. - choices: ['Default', 'Clone', 'Replace'] - default: 'Default' - -requirements: - - "omsdk" - - "python >= 2.7.5" -author: "Jagadeesh N V(@jagadeeshnv)" - -''' - -EXAMPLES = r''' ---- -- name: Import Server Configuration Profile from a network share - community.general.idrac_server_config_profile: - idrac_ip: "192.168.0.1" - idrac_user: "user_name" - idrac_password: "user_password" - command: "import" - share_name: "192.168.0.2:/share" - share_user: "share_user_name" - share_password: "share_user_password" - scp_file: "scp_filename.xml" - scp_components: "ALL" - job_wait: True - -- name: Import Server Configuration Profile from a local path - community.general.idrac_server_config_profile: - idrac_ip: "192.168.0.1" - idrac_user: "user_name" - idrac_password: "user_password" - command: "import" - share_name: "/scp_folder" - share_user: "share_user_name" - share_password: "share_user_password" - scp_file: "scp_filename.xml" - scp_components: "ALL" - job_wait: True - -- name: Export Server Configuration Profile to a network share - community.general.idrac_server_config_profile: - idrac_ip: "192.168.0.1" - idrac_user: "user_name" - idrac_password: "user_password" - share_name: "192.168.0.2:/share" - share_user: "share_user_name" - share_password: "share_user_password" - job_wait: False - -- name: Export Server Configuration Profile to a local path - community.general.idrac_server_config_profile: - idrac_ip: "192.168.0.1" - idrac_user: "user_name" - idrac_password: "user_password" - share_name: "/scp_folder" - share_user: "share_user_name" - share_password: "share_user_password" - job_wait: False -''' - -RETURN = r''' ---- -msg: - type: str - description: Status of the import or export SCP job. - returned: always - sample: "Successfully imported the Server Configuration Profile" -scp_status: - type: dict - description: SCP operation job and progress details from the iDRAC. - returned: success - sample: - { - "Id": "JID_XXXXXXXXX", - "JobState": "Completed", - "JobType": "ImportConfiguration", - "Message": "Successfully imported and applied Server Configuration Profile.", - "MessageArgs": [], - "MessageId": "XXX123", - "Name": "Import Configuration", - "PercentComplete": 100, - "StartTime": "TIME_NOW", - "Status": "Success", - "TargetSettingsURI": null, - "retval": true - } -''' - -import os -from ansible_collections.community.general.plugins.module_utils.remote_management.dellemc.dellemc_idrac import iDRACConnection -from ansible.module_utils.basic import AnsibleModule -try: - from omsdk.sdkfile import file_share_manager - from omsdk.sdkcreds import UserCredentials - from omdrivers.enums.iDRAC.iDRACEnums import (SCPTargetEnum, EndHostPowerStateEnum, - ShutdownTypeEnum, ExportFormatEnum, ExportUseEnum) -except ImportError: - pass - - -def run_import_server_config_profile(idrac, module): - """Import Server Configuration Profile from a network share.""" - target = SCPTargetEnum[module.params['scp_components']] - job_wait = module.params['job_wait'] - end_host_power_state = EndHostPowerStateEnum[module.params['end_host_power_state']] - shutdown_type = ShutdownTypeEnum[module.params['shutdown_type']] - idrac.use_redfish = True - - try: - myshare = file_share_manager.create_share_obj( - share_path="{0}{1}{2}".format(module.params['share_name'], os.sep, module.params['scp_file']), - creds=UserCredentials(module.params['share_user'], - module.params['share_password']), isFolder=False) - import_status = idrac.config_mgr.scp_import(myshare, - target=target, shutdown_type=shutdown_type, - end_host_power_state=end_host_power_state, - job_wait=job_wait) - if not import_status or import_status.get('Status') != "Success": - module.fail_json(msg='Failed to import scp.', scp_status=import_status) - except RuntimeError as e: - module.fail_json(msg=str(e)) - return import_status - - -def run_export_server_config_profile(idrac, module): - """Export Server Configuration Profile to a network share.""" - export_format = ExportFormatEnum[module.params['export_format']] - scp_file_name_format = "%ip_%Y%m%d_%H%M%S_scp.{0}".format(module.params['export_format'].lower()) - target = SCPTargetEnum[module.params['scp_components']] - export_use = ExportUseEnum[module.params['export_use']] - idrac.use_redfish = True - - try: - myshare = file_share_manager.create_share_obj(share_path=module.params['share_name'], - creds=UserCredentials(module.params['share_user'], - module.params['share_password']), - isFolder=True) - scp_file_name = myshare.new_file(scp_file_name_format) - export_status = idrac.config_mgr.scp_export(scp_file_name, - target=target, - export_format=export_format, - export_use=export_use, - job_wait=module.params['job_wait']) - if not export_status or export_status.get('Status') != "Success": - module.fail_json(msg='Failed to export scp.', scp_status=export_status) - except RuntimeError as e: - module.fail_json(msg=str(e)) - return export_status - - -def main(): - module = AnsibleModule( - argument_spec={ - "idrac_ip": {"required": True, "type": 'str'}, - "idrac_user": {"required": True, "type": 'str'}, - "idrac_password": {"required": True, "type": 'str', - "aliases": ['idrac_pwd'], "no_log": True}, - "idrac_port": {"required": False, "default": 443, "type": 'int'}, - - "command": {"required": False, "type": 'str', - "choices": ['export', 'import'], "default": 'export'}, - "job_wait": {"required": True, "type": 'bool'}, - - "share_name": {"required": True, "type": 'str'}, - "share_user": {"required": False, "type": 'str'}, - "share_password": {"required": False, "type": 'str', - "aliases": ['share_pwd'], "no_log": True}, - "scp_components": {"required": False, - "choices": ['ALL', 'IDRAC', 'BIOS', 'NIC', 'RAID'], - "default": 'ALL'}, - - "scp_file": {"required": False, "type": 'str'}, - "shutdown_type": {"required": False, - "choices": ['Graceful', 'Forced', 'NoReboot'], - "default": 'Graceful'}, - "end_host_power_state": {"required": False, - "choices": ['On', 'Off'], - "default": 'On'}, - - "export_format": {"required": False, "type": 'str', - "choices": ['JSON', 'XML'], "default": 'XML'}, - "export_use": {"required": False, "type": 'str', - "choices": ['Default', 'Clone', 'Replace'], "default": 'Default'} - }, - required_if=[ - ["command", "import", ["scp_file"]] - ], - supports_check_mode=False) - - try: - changed = False - with iDRACConnection(module.params) as idrac: - command = module.params['command'] - if command == 'import': - scp_status = run_import_server_config_profile(idrac, module) - if "No changes were applied" not in scp_status.get('Message', ""): - changed = True - else: - scp_status = run_export_server_config_profile(idrac, module) - module.exit_json(changed=changed, msg="Successfully {0}ed the Server Configuration Profile.".format(command), - scp_status=scp_status) - except (ImportError, ValueError, RuntimeError) as e: - module.fail_json(msg=str(e)) - - -if __name__ == '__main__': - main() diff --git a/plugins/modules/remote_management/dellemc/ome_device_info.py b/plugins/modules/remote_management/dellemc/ome_device_info.py deleted file mode 100644 index 68fbb1e680..0000000000 --- a/plugins/modules/remote_management/dellemc/ome_device_info.py +++ /dev/null @@ -1,413 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# -# Dell EMC OpenManage Ansible Modules -# Version 1.2 -# Copyright (C) 2019 Dell Inc. - -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# All rights reserved. Dell, EMC, and other trademarks are trademarks of Dell Inc. or its subsidiaries. -# Other trademarks may be trademarks of their respective owners. -# - - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -DOCUMENTATION = r''' ---- -module: ome_device_info -short_description: Retrieves the information about Device. -description: - - This module retrieves the list of all devices information with the exhaustive inventory of each - device. -options: - hostname: - description: - - Target IP Address or hostname. - type: str - required: True - username: - description: - - Target username. - type: str - required: True - password: - description: - - Target user password. - type: str - required: True - port: - description: - - Target HTTPS port. - type: int - default: 443 - fact_subset: - description: - - C(basic_inventory) returns the list of the devices. - - C(detailed_inventory) returns the inventory details of specified devices. - - C(subsystem_health) returns the health status of specified devices. - type: str - choices: [basic_inventory, detailed_inventory, subsystem_health ] - default: basic_inventory - system_query_options: - description: - - I(system_query_options) applicable for the choices of the fact_subset. Either I(device_id) or I(device_service_tag) - is mandatory for C(detailed_inventory) and C(subsystem_health) or both can be applicable. - type: dict - suboptions: - device_id: - description: - - A list of unique identifier is applicable - for C(detailed_inventory) and C(subsystem_health). - type: list - device_service_tag: - description: - - A list of service tags are applicable for C(detailed_inventory) - and C(subsystem_health). - type: list - inventory_type: - description: - - For C(detailed_inventory), it returns details of the specified inventory type. - type: str - filter: - description: - - For C(basic_inventory), it filters the collection of devices. - I(filter) query format should be aligned with OData standards. - type: str - -requirements: - - "python >= 2.7.5" -author: "Sajna Shetty(@Sajna-Shetty)" -''' - -EXAMPLES = """ ---- -- name: Retrieve basic inventory of all devices. - community.general.ome_device_info: - hostname: "192.168.0.1" - username: "username" - password: "password" - -- name: Retrieve basic inventory for devices identified by IDs 33333 or 11111 using filtering. - community.general.ome_device_info: - hostname: "192.168.0.1" - username: "username" - password: "password" - fact_subset: "basic_inventory" - system_query_options: - filter: "Id eq 33333 or Id eq 11111" - -- name: Retrieve inventory details of specified devices identified by IDs 11111 and 22222. - community.general.ome_device_info: - hostname: "192.168.0.1" - username: "username" - password: "password" - fact_subset: "detailed_inventory" - system_query_options: - device_id: - - 11111 - - 22222 - -- name: Retrieve inventory details of specified devices identified by service tags MXL1234 and MXL4567. - community.general.ome_device_info: - hostname: "192.168.0.1" - username: "username" - password: "password" - fact_subset: "detailed_inventory" - system_query_options: - device_service_tag: - - MXL1234 - - MXL4567 - -- name: Retrieve details of specified inventory type of specified devices identified by ID and service tags. - community.general.ome_device_info: - hostname: "192.168.0.1" - username: "username" - password: "password" - fact_subset: "detailed_inventory" - system_query_options: - device_id: - - 11111 - device_service_tag: - - MXL1234 - - MXL4567 - inventory_type: "serverDeviceCards" - -- name: Retrieve subsystem health of specified devices identified by service tags. - community.general.ome_device_info: - hostname: "192.168.0.1" - username: "username" - password: "password" - fact_subset: "subsystem_health" - system_query_options: - device_service_tag: - - MXL1234 - - MXL4567 - -""" - -RETURN = ''' ---- -msg: - type: str - description: Over all device information status. - returned: on error - sample: "Failed to fetch the device information" -device_info: - type: dict - description: Returns the information collected from the Device. - returned: success - sample: { - "value": [ - { - "Actions": null, - "AssetTag": null, - "ChassisServiceTag": null, - "ConnectionState": true, - "DeviceManagement": [ - { - "DnsName": "dnsname.host.com", - "InstrumentationName": "MX-12345", - "MacAddress": "11:10:11:10:11:10", - "ManagementId": 12345, - "ManagementProfile": [ - { - "HasCreds": 0, - "ManagementId": 12345, - "ManagementProfileId": 12345, - "ManagementURL": "https://192.168.0.1:443", - "Status": 1000, - "StatusDateTime": "2019-01-21 06:30:08.501" - } - ], - "ManagementType": 2, - "NetworkAddress": "192.168.0.1" - } - ], - "DeviceName": "MX-0003I", - "DeviceServiceTag": "MXL1234", - "DeviceSubscription": null, - "LastInventoryTime": "2019-01-21 06:30:08.501", - "LastStatusTime": "2019-01-21 06:30:02.492", - "ManagedState": 3000, - "Model": "PowerEdge MX7000", - "PowerState": 17, - "SlotConfiguration": {}, - "Status": 4000, - "SystemId": 2031, - "Type": 2000 - } - ] - } -''' - -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.community.general.plugins.module_utils.remote_management.dellemc.ome import RestOME -from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError -from ansible.module_utils.urls import ConnectionError, SSLValidationError - -DEVICES_INVENTORY_DETAILS = "detailed_inventory" -DEVICES_SUBSYSTEM_HEALTH = "subsystem_health" -DEVICES_INVENTORY_TYPE = "inventory_type" -DEVICE_LIST = "basic_inventory" -DESC_HTTP_ERROR = "HTTP Error 404: Not Found" -device_fact_error_report = {} - -DEVICE_RESOURCE_COLLECTION = { - DEVICE_LIST: {"resource": "DeviceService/Devices"}, - DEVICES_INVENTORY_DETAILS: {"resource": "DeviceService/Devices({Id})/InventoryDetails"}, - DEVICES_INVENTORY_TYPE: {"resource": "DeviceService/Devices({Id})/InventoryDetails('{InventoryType}')"}, - DEVICES_SUBSYSTEM_HEALTH: {"resource": "DeviceService/Devices({Id})/SubSystemHealth"}, -} - - -def _get_device_id_from_service_tags(service_tags, rest_obj): - """ - Get device ids from device service tag - Returns :dict : device_id to service_tag map - :arg service_tags: service tag - :arg rest_obj: RestOME class object in case of request with session. - :returns: dict eg: {1345:"MXL1245"} - """ - try: - path = DEVICE_RESOURCE_COLLECTION[DEVICE_LIST]["resource"] - resp = rest_obj.invoke_request('GET', path) - if resp.success: - devices_list = resp.json_data["value"] - service_tag_dict = {} - for item in devices_list: - if item["DeviceServiceTag"] in service_tags: - service_tag_dict.update({item["Id"]: item["DeviceServiceTag"]}) - available_service_tags = service_tag_dict.values() - not_available_service_tag = list(set(service_tags) - set(available_service_tags)) - device_fact_error_report.update(dict((tag, DESC_HTTP_ERROR) for tag in not_available_service_tag)) - else: - raise ValueError(resp.json_data) - except (URLError, HTTPError, SSLValidationError, ConnectionError, TypeError, ValueError) as err: - raise err - return service_tag_dict - - -def is_int(val): - """check when device_id numeric represented value is int""" - try: - int(val) - return True - except ValueError: - return False - - -def _check_duplicate_device_id(device_id_list, service_tag_dict): - """If service_tag is duplicate of device_id, then updates the message as Duplicate report - :arg1: device_id_list : list of device_id - :arg2: service_tag_id_dict: dictionary of device_id to service tag map""" - if device_id_list: - device_id_represents_int = [int(device_id) for device_id in device_id_list if device_id and is_int(device_id)] - common_val = list(set(device_id_represents_int) & set(service_tag_dict.keys())) - for device_id in common_val: - device_fact_error_report.update( - {service_tag_dict[device_id]: "Duplicate report of device_id: {0}".format(device_id)}) - del service_tag_dict[device_id] - - -def _get_device_identifier_map(module_params, rest_obj): - """ - Builds the identifiers mapping - :returns: the dict of device_id to server_tag map - eg: {"device_id":{1234: None},"device_service_tag":{1345:"MXL1234"}}""" - system_query_options_param = module_params.get("system_query_options") - device_id_service_tag_dict = {} - if system_query_options_param is not None: - device_id_list = system_query_options_param.get("device_id") - device_service_tag_list = system_query_options_param.get("device_service_tag") - if device_id_list: - device_id_dict = dict((device_id, None) for device_id in list(set(device_id_list))) - device_id_service_tag_dict["device_id"] = device_id_dict - if device_service_tag_list: - service_tag_dict = _get_device_id_from_service_tags(device_service_tag_list, - rest_obj) - - _check_duplicate_device_id(device_id_list, service_tag_dict) - device_id_service_tag_dict["device_service_tag"] = service_tag_dict - return device_id_service_tag_dict - - -def _get_query_parameters(module_params): - """ - Builds query parameter - :returns: dictionary, which is applicable builds the query format - eg : {"$filter":"Type eq 2000"} - """ - system_query_options_param = module_params.get("system_query_options") - query_parameter = None - if system_query_options_param: - filter_by_val = system_query_options_param.get("filter") - if filter_by_val: - query_parameter = {"$filter": filter_by_val} - return query_parameter - - -def _get_resource_parameters(module_params, rest_obj): - """ - Identifies the resource path by different states - :returns: dictionary containing identifier with respective resource path - eg:{"device_id":{1234:""DeviceService/Devices(1234)/InventoryDetails"}, - "device_service_tag":{"MXL1234":"DeviceService/Devices(1345)/InventoryDetails"}} - """ - fact_subset = module_params["fact_subset"] - path_dict = {} - if fact_subset != DEVICE_LIST: - inventory_type = None - device_id_service_tag_dict = _get_device_identifier_map(module_params, rest_obj) - if fact_subset == DEVICES_INVENTORY_DETAILS: - system_query_options = module_params.get("system_query_options") - inventory_type = system_query_options.get(DEVICES_INVENTORY_TYPE) - path_identifier = DEVICES_INVENTORY_TYPE if inventory_type else fact_subset - for identifier_type, identifier_dict in device_id_service_tag_dict.items(): - path_dict[identifier_type] = {} - for device_id, service_tag in identifier_dict.items(): - key_identifier = service_tag if identifier_type == "device_service_tag" else device_id - path = DEVICE_RESOURCE_COLLECTION[path_identifier]["resource"].format(Id=device_id, - InventoryType=inventory_type) - path_dict[identifier_type].update({key_identifier: path}) - else: - path_dict.update({DEVICE_LIST: DEVICE_RESOURCE_COLLECTION[DEVICE_LIST]["resource"]}) - return path_dict - - -def _check_mutually_inclusive_arguments(val, module_params, required_args): - """" - Throws error if arguments detailed_inventory, subsystem_health - not exists with qualifier device_id or device_service_tag""" - system_query_options_param = module_params.get("system_query_options") - if system_query_options_param is None or (system_query_options_param is not None and not any( - system_query_options_param.get(qualifier) for qualifier in required_args)): - raise ValueError("One of the following {0} is required for {1}".format(required_args, val)) - - -def _validate_inputs(module_params): - """validates input parameters""" - fact_subset = module_params["fact_subset"] - if fact_subset != "basic_inventory": - _check_mutually_inclusive_arguments(fact_subset, module_params, ["device_id", "device_service_tag"]) - - -def main(): - system_query_options = {"type": 'dict', "required": False, "options": { - "device_id": {"type": 'list'}, - "device_service_tag": {"type": 'list'}, - "inventory_type": {"type": 'str'}, - "filter": {"type": 'str', "required": False}, - }} - - module = AnsibleModule( - argument_spec={ - "hostname": {"required": True, "type": 'str'}, - "username": {"required": True, "type": 'str'}, - "password": {"required": True, "type": 'str', "no_log": True}, - "port": {"required": False, "default": 443, "type": 'int'}, - "fact_subset": {"required": False, "default": "basic_inventory", - "choices": ['basic_inventory', 'detailed_inventory', 'subsystem_health']}, - "system_query_options": system_query_options, - }, - required_if=[['fact_subset', 'detailed_inventory', ['system_query_options']], - ['fact_subset', 'subsystem_health', ['system_query_options']], ], - supports_check_mode=False) - - try: - _validate_inputs(module.params) - with RestOME(module.params, req_session=True) as rest_obj: - device_facts = _get_resource_parameters(module.params, rest_obj) - resp_status = [] - if device_facts.get("basic_inventory"): - query_param = _get_query_parameters(module.params) - resp = rest_obj.invoke_request('GET', device_facts["basic_inventory"], query_param=query_param) - device_facts = resp.json_data - resp_status.append(resp.status_code) - else: - for identifier_type, path_dict_map in device_facts.items(): - for identifier, path in path_dict_map.items(): - try: - resp = rest_obj.invoke_request('GET', path) - data = resp.json_data - resp_status.append(resp.status_code) - except HTTPError as err: - data = str(err) - path_dict_map[identifier] = data - if any(device_fact_error_report): - if "device_service_tag" in device_facts: - device_facts["device_service_tag"].update(device_fact_error_report) - else: - device_facts["device_service_tag"] = device_fact_error_report - if 200 in resp_status: - module.exit_json(device_info=device_facts) - else: - module.fail_json(msg="Failed to fetch the device information") - except (URLError, HTTPError, SSLValidationError, ConnectionError, TypeError, ValueError) as err: - module.fail_json(msg=str(err)) - - -if __name__ == '__main__': - main() diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index e1241f89d1..46142bf001 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -237,9 +237,6 @@ plugins/modules/packaging/os/redhat_subscription.py validate-modules:return-synt plugins/modules/packaging/os/slackpkg.py validate-modules:parameter-invalid plugins/modules/packaging/os/urpmi.py validate-modules:parameter-invalid plugins/modules/packaging/os/xbps.py validate-modules:parameter-invalid -plugins/modules/remote_management/dellemc/idrac_server_config_profile.py validate-modules:doc-missing-type -plugins/modules/remote_management/dellemc/idrac_server_config_profile.py validate-modules:parameter-type-not-in-doc -plugins/modules/remote_management/dellemc/ome_device_info.py validate-modules:parameter-list-no-elements plugins/modules/remote_management/hpilo/hpilo_boot.py validate-modules:parameter-type-not-in-doc plugins/modules/remote_management/hpilo/hpilo_info.py validate-modules:parameter-type-not-in-doc plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index e1241f89d1..46142bf001 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -237,9 +237,6 @@ plugins/modules/packaging/os/redhat_subscription.py validate-modules:return-synt plugins/modules/packaging/os/slackpkg.py validate-modules:parameter-invalid plugins/modules/packaging/os/urpmi.py validate-modules:parameter-invalid plugins/modules/packaging/os/xbps.py validate-modules:parameter-invalid -plugins/modules/remote_management/dellemc/idrac_server_config_profile.py validate-modules:doc-missing-type -plugins/modules/remote_management/dellemc/idrac_server_config_profile.py validate-modules:parameter-type-not-in-doc -plugins/modules/remote_management/dellemc/ome_device_info.py validate-modules:parameter-list-no-elements plugins/modules/remote_management/hpilo/hpilo_boot.py validate-modules:parameter-type-not-in-doc plugins/modules/remote_management/hpilo/hpilo_info.py validate-modules:parameter-type-not-in-doc plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 2a3c828361..604672ab19 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -202,8 +202,6 @@ plugins/modules/packaging/os/redhat_subscription.py validate-modules:return-synt plugins/modules/packaging/os/slackpkg.py validate-modules:parameter-invalid plugins/modules/packaging/os/urpmi.py validate-modules:parameter-invalid plugins/modules/packaging/os/xbps.py validate-modules:parameter-invalid -plugins/modules/remote_management/dellemc/idrac_server_config_profile.py validate-modules:doc-missing-type -plugins/modules/remote_management/dellemc/idrac_server_config_profile.py validate-modules:parameter-type-not-in-doc plugins/modules/remote_management/hpilo/hpilo_boot.py validate-modules:parameter-type-not-in-doc plugins/modules/remote_management/hpilo/hpilo_info.py validate-modules:parameter-type-not-in-doc plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc diff --git a/tests/unit/plugins/module_utils/remote_management/dellemc/__init__.py b/tests/unit/plugins/module_utils/remote_management/dellemc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/unit/plugins/module_utils/remote_management/dellemc/test_ome.py b/tests/unit/plugins/module_utils/remote_management/dellemc/test_ome.py deleted file mode 100644 index cc698d0b21..0000000000 --- a/tests/unit/plugins/module_utils/remote_management/dellemc/test_ome.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- - -# -# Dell EMC OpenManage Ansible Modules -# Version 2.0 -# Copyright (C) 2019 Dell Inc. - -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# All rights reserved. Dell, EMC, and other trademarks are trademarks of Dell Inc. or its subsidiaries. -# Other trademarks may be trademarks of their respective owners. -# - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest -from ansible.module_utils.urls import ConnectionError, SSLValidationError -from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError -from ansible_collections.community.general.plugins.module_utils.remote_management.dellemc.ome import RestOME -from ansible_collections.community.general.tests.unit.compat.mock import MagicMock -import json - - -class TestRestOME(object): - @pytest.fixture - def mock_response(self): - mock_response = MagicMock() - mock_response.getcode.return_value = 200 - mock_response.headers = mock_response.getheaders.return_value = {'X-Auth-Token': 'token_id'} - mock_response.read.return_value = json.dumps({"value": "data"}) - return mock_response - - def test_invoke_request_with_session(self, mock_response, mocker): - mocker.patch('ansible_collections.community.general.plugins.module_utils.remote_management.dellemc.ome.open_url', - return_value=mock_response) - module_params = {'hostname': '192.168.0.1', 'username': 'username', - 'password': 'password', "port": 443} - req_session = True - with RestOME(module_params, req_session) as obj: - response = obj.invoke_request("/testpath", "GET") - assert response.status_code == 200 - assert response.json_data == {"value": "data"} - assert response.success is True - - def test_invoke_request_without_session(self, mock_response, mocker): - mocker.patch('ansible_collections.community.general.plugins.module_utils.remote_management.dellemc.ome.open_url', - return_value=mock_response) - module_params = {'hostname': '192.168.0.1', 'username': 'username', - 'password': 'password', "port": 443} - req_session = False - with RestOME(module_params, req_session) as obj: - response = obj.invoke_request("/testpath", "GET") - assert response.status_code == 200 - assert response.json_data == {"value": "data"} - assert response.success is True - - @pytest.mark.parametrize("exc", [URLError, SSLValidationError, ConnectionError]) - def test_invoke_request_error_case_handling(self, exc, mock_response, mocker): - open_url_mock = mocker.patch('ansible_collections.community.general.plugins.module_utils.remote_management.dellemc.ome.open_url', - return_value=mock_response) - open_url_mock.side_effect = exc("test") - module_params = {'hostname': '192.168.0.1', 'username': 'username', - 'password': 'password', "port": 443} - req_session = False - with pytest.raises(exc) as e: - with RestOME(module_params, req_session) as obj: - obj.invoke_request("/testpath", "GET") - - def test_invoke_request_http_error_handling(self, mock_response, mocker): - open_url_mock = mocker.patch('ansible_collections.community.general.plugins.module_utils.remote_management.dellemc.ome.open_url', - return_value=mock_response) - open_url_mock.side_effect = HTTPError('http://testhost.com/', 400, - 'Bad Request Error', {}, None) - module_params = {'hostname': '192.168.0.1', 'username': 'username', - 'password': 'password', "port": 443} - req_session = False - with pytest.raises(HTTPError) as e: - with RestOME(module_params, req_session) as obj: - obj.invoke_request("/testpath", "GET") diff --git a/tests/unit/plugins/modules/remote_management/dellemc/__init__.py b/tests/unit/plugins/modules/remote_management/dellemc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/unit/plugins/modules/remote_management/dellemc/test_ome_device_info.py b/tests/unit/plugins/modules/remote_management/dellemc/test_ome_device_info.py deleted file mode 100644 index 5e825c4222..0000000000 --- a/tests/unit/plugins/modules/remote_management/dellemc/test_ome_device_info.py +++ /dev/null @@ -1,196 +0,0 @@ -# -*- coding: utf-8 -*- - -# -# Dell EMC OpenManage Ansible Modules -# Version 2.0 -# Copyright (C) 2019 Dell Inc. - -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# All rights reserved. Dell, EMC, and other trademarks are trademarks of Dell Inc. or its subsidiaries. -# Other trademarks may be trademarks of their respective owners. -# - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest -from ansible_collections.community.general.tests.unit.plugins.modules.utils import set_module_args, exit_json, \ - fail_json, AnsibleFailJson, AnsibleExitJson -from ansible.module_utils import basic -from ansible_collections.community.general.plugins.modules.remote_management.dellemc import ome_device_info -from ansible.module_utils.six.moves.urllib.error import HTTPError - -default_args = {'hostname': '192.168.0.1', 'username': 'username', 'password': 'password'} -resource_basic_inventory = {"basic_inventory": "DeviceService/Devices"} -resource_detailed_inventory = {"detailed_inventory:": {"device_id": {1234: None}, - "device_service_tag": {1345: "MXL1234"}}} - - -class TestOmeDeviceInfo(object): - module = ome_device_info - - @pytest.fixture(autouse=True) - def module_mock(self, mocker): - return mocker.patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) - - @pytest.fixture - def connection_mock(self, mocker): - connection_class_mock = mocker.patch('ansible_collections.community.general.plugins.modules.remote_management.dellemc.ome_device_info.RestOME') - return connection_class_mock.return_value - - @pytest.fixture - def response_mock(self, mocker): - response_class_mock = mocker.patch('ansible_collections.community.general.plugins.module_utils.remote_management.dellemc.ome.OpenURLResponse') - return response_class_mock - - @pytest.fixture - def validate_inputs_mock(self, mocker): - response_class_mock = mocker.patch('ansible_collections.community.general.plugins.modules.remote_management.dellemc.ome_device_info._validate_inputs') - response_class_mock.return_value = None - - @pytest.fixture - def get_device_identifier_map_mock(self, mocker): - response_class_mock = mocker.patch( - 'ansible_collections.community.general.plugins.modules.remote_management.dellemc.ome_device_info._get_device_identifier_map' - ) - response_class_mock.return_value = resource_detailed_inventory - return response_class_mock.return_value - - @pytest.fixture - def get_resource_parameters_mock(self, mocker): - response_class_mock = mocker.patch( - 'ansible_collections.community.general.plugins.modules.remote_management.dellemc.ome_device_info._get_resource_parameters' - ) - return response_class_mock - - def test_main_basic_inventory_success_case(self, module_mock, validate_inputs_mock, connection_mock, get_resource_parameters_mock, response_mock): - get_resource_parameters_mock.return_value = resource_basic_inventory - connection_mock.__enter__.return_value = connection_mock - connection_mock.invoke_request.return_value = response_mock - response_mock.json_data = {"value": [{"device_id1": "details", "device_id2": "details"}]} - response_mock.status_code = 200 - result = self._run_module(default_args) - assert result['changed'] is False - assert 'device_info' in result - - def test_main_basic_inventory_failure_case(self, module_mock, validate_inputs_mock, connection_mock, get_resource_parameters_mock, response_mock): - get_resource_parameters_mock.return_value = resource_basic_inventory - connection_mock.__enter__.return_value = connection_mock - connection_mock.invoke_request.return_value = response_mock - response_mock.status_code = 500 - result = self._run_module_with_fail_json(default_args) - assert result['msg'] == 'Failed to fetch the device information' - - def test_main_detailed_inventory_success_case(self, module_mock, validate_inputs_mock, connection_mock, get_resource_parameters_mock, response_mock): - default_args.update({"fact_subset": "detailed_inventory", "system_query_options": {"device_id": [1234], "device_service_tag": ["MXL1234"]}}) - detailed_inventory = {"detailed_inventory:": {"device_id": {1234: "DeviceService/Devices(1234)/InventoryDetails"}, - "device_service_tag": {"MXL1234": "DeviceService/Devices(4321)/InventoryDetails"}}} - get_resource_parameters_mock.return_value = detailed_inventory - connection_mock.__enter__.return_value = connection_mock - connection_mock.invoke_request.return_value = response_mock - response_mock.json_data = {"value": [{"device_id": {"1234": "details"}}, {"device_service_tag": {"MXL1234": "details"}}]} - response_mock.status_code = 200 - result = self._run_module(default_args) - assert result['changed'] is False - assert 'device_info' in result - - def test_main_HTTPError_error_case(self, module_mock, validate_inputs_mock, connection_mock, get_resource_parameters_mock, response_mock): - get_resource_parameters_mock.return_value = resource_basic_inventory - connection_mock.__enter__.return_value = connection_mock - connection_mock.invoke_request.side_effect = HTTPError('http://testhost.com', 400, '', {}, None) - response_mock.json_data = {"value": [{"device_id1": "details", "device_id2": "details"}]} - response_mock.status_code = 400 - result = self._run_module_with_fail_json(default_args) - assert 'device_info' not in result - assert result['failed'] is True - - @pytest.mark.parametrize("fact_subset, mutually_exclusive_call", [("basic_inventory", False), ("detailed_inventory", True)]) - def test_validate_inputs(self, fact_subset, mutually_exclusive_call, mocker): - module_params = {"fact_subset": fact_subset} - check_mutually_inclusive_arguments_mock = mocker.patch( - 'ansible_collections.community.general.plugins.modules.remote_management.dellemc.ome_device_info._check_mutually_inclusive_arguments') - check_mutually_inclusive_arguments_mock.return_value = None - self.module._validate_inputs(module_params) - if mutually_exclusive_call: - check_mutually_inclusive_arguments_mock.assert_called() - else: - check_mutually_inclusive_arguments_mock.assert_not_called() - check_mutually_inclusive_arguments_mock.reset_mock() - - system_query_options_params = [{"system_query_options": None}, {"system_query_options": {"device_id": None}}, - {"system_query_options": {"device_service_tag": None}}] - - @pytest.mark.parametrize("system_query_options_params", system_query_options_params) - def test_check_mutually_inclusive_arguments(self, system_query_options_params): - module_params = {"fact_subset": "subsystem_health"} - required_args = ["device_id", "device_service_tag"] - module_params.update(system_query_options_params) - with pytest.raises(ValueError) as ex: - self.module._check_mutually_inclusive_arguments(module_params["fact_subset"], module_params, ["device_id", "device_service_tag"]) - assert "One of the following {0} is required for {1}".format(required_args, module_params["fact_subset"]) == str(ex.value) - - params = [{"fact_subset": "basic_inventory", "system_query_options": {"device_id": [1234]}}, - {"fact_subset": "subsystem_health", "system_query_options": {"device_service_tag": ["MXL1234"]}}, - {"fact_subset": "detailed_inventory", "system_query_options": {"device_id": [1234], "inventory_type": "serverDeviceCards"}}] - - @pytest.mark.parametrize("module_params", params) - def test_get_resource_parameters(self, module_params, connection_mock): - self.module._get_resource_parameters(module_params, connection_mock) - - @pytest.mark.parametrize("module_params,data", [({"system_query_options": None}, None), ({"system_query_options": {"fileter": None}}, None), - ({"system_query_options": {"filter": "abc"}}, "$filter")]) - def test_get_query_parameters(self, module_params, data): - res = self.module._get_query_parameters(module_params) - if data is not None: - assert data in res - else: - assert res is None - - @pytest.mark.parametrize("module_params", params) - def test_get_device_identifier_map(self, module_params, connection_mock, mocker): - get_device_id_from_service_tags_mock = mocker.patch( - 'ansible_collections.community.general.plugins.modules.remote_management.dellemc.ome_device_info._get_device_id_from_service_tags' - ) - get_device_id_from_service_tags_mock.return_value = None - res = self.module._get_device_identifier_map(module_params, connection_mock) - assert isinstance(res, dict) - - def test_check_duplicate_device_id(self): - self.module._check_duplicate_device_id([1234], {1234: "MX1234"}) - assert self.module.device_fact_error_report["MX1234"] == "Duplicate report of device_id: 1234" - - @pytest.mark.parametrize("val,expected_res", [(123, True), ("abc", False)]) - def test_is_int(self, val, expected_res): - actual_res = self.module.is_int(val) - assert actual_res == expected_res - - def test_get_device_id_from_service_tags(self, connection_mock, response_mock): - connection_mock.__enter__.return_value = connection_mock - connection_mock.invoke_request.return_value = response_mock - response_mock.json_data = {"value": [{"DeviceServiceTag": "MX1234", "Id": 1234}]} - response_mock.status_code = 200 - response_mock.success = True - self.module._get_device_id_from_service_tags(["MX1234", "INVALID"], connection_mock) - - def test_get_device_id_from_service_tags_error_case(self, connection_mock, response_mock): - connection_mock.__enter__.return_value = connection_mock - connection_mock.invoke_request.side_effect = HTTPError('http://testhost.com', - 400, '', {}, None) - response_mock.json_data = {"value": [{"DeviceServiceTag": "MX1234", "Id": 1234}]} - response_mock.status_code = 200 - response_mock.success = True - with pytest.raises(HTTPError) as ex: - self.module._get_device_id_from_service_tags(["INVALID"], connection_mock) - - def _run_module(self, module_args): - set_module_args(module_args) - with pytest.raises(AnsibleExitJson) as ex: - self.module.main() - return ex.value.args[0] - - def _run_module_with_fail_json(self, module_args): - set_module_args(module_args) - with pytest.raises(AnsibleFailJson) as exc: - self.module.main() - result = exc.value.args[0] - return result