mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Migrating DellEMC collections from community.general collections (#1699)
* migration modification for dellemc collections * removing dellemc collections * Update changelogs/fragments/948-dellemc-migration-removal.yml Co-authored-by: Felix Fontein <felix@fontein.de> * Update 948-dellemc-migration-removal.yml * Update 948-dellemc-migration-removal.yml * Update runtime.yml * Update meta/runtime.yml Co-authored-by: Felix Fontein <felix@fontein.de> * Update runtime.yml * deleted symlink entries * Update 948-dellemc-migration-removal.yml * Update changelogs/fragments/948-dellemc-migration-removal.yml Co-authored-by: Felix Fontein <felix@fontein.de> * Update changelogs/fragments/948-dellemc-migration-removal.yml Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
6af3c96d8e
commit
8afdd23be4
19 changed files with 23 additions and 1433 deletions
7
.github/BOTMETA.yml
vendored
7
.github/BOTMETA.yml
vendored
|
@ -139,7 +139,6 @@ files:
|
||||||
$module_utils/redfish_utils.py:
|
$module_utils/redfish_utils.py:
|
||||||
maintainers: $team_redfish
|
maintainers: $team_redfish
|
||||||
labels: redfish_utils
|
labels: redfish_utils
|
||||||
$module_utils/remote_management/dellemc/: rajeevarakkal
|
|
||||||
$module_utils/remote_management/lxca/common.py: navalkp prabhosa
|
$module_utils/remote_management/lxca/common.py: navalkp prabhosa
|
||||||
$module_utils/scaleway.py:
|
$module_utils/scaleway.py:
|
||||||
maintainers: $team_scaleway
|
maintainers: $team_scaleway
|
||||||
|
@ -691,12 +690,6 @@ files:
|
||||||
maintainers: matze
|
maintainers: matze
|
||||||
$modules/remote_management/cobbler/:
|
$modules/remote_management/cobbler/:
|
||||||
maintainers: dagwieers
|
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/:
|
$modules/remote_management/hpilo/:
|
||||||
maintainers: haad
|
maintainers: haad
|
||||||
ignore: dagwieers
|
ignore: dagwieers
|
||||||
|
|
13
changelogs/fragments/948-dellemc-migration-removal.yml
Normal file
13
changelogs/fragments/948-dellemc-migration-removal.yml
Normal file
|
@ -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 <https://galaxy.ansible.com/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.
|
|
@ -199,10 +199,14 @@ plugin_routing:
|
||||||
deprecation:
|
deprecation:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
warning_text: see plugin documentation for details
|
warning_text: see plugin documentation for details
|
||||||
|
idrac_firmware:
|
||||||
|
redirect: dellemc.openmanage.idrac_firmware
|
||||||
idrac_redfish_facts:
|
idrac_redfish_facts:
|
||||||
deprecation:
|
deprecation:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
warning_text: see plugin documentation for details
|
warning_text: see plugin documentation for details
|
||||||
|
idrac_server_config_profile:
|
||||||
|
redirect: dellemc.openmanage.idrac_server_config_profile
|
||||||
jenkins_job_facts:
|
jenkins_job_facts:
|
||||||
deprecation:
|
deprecation:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
|
@ -283,6 +287,8 @@ plugin_routing:
|
||||||
deprecation:
|
deprecation:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
warning_text: see plugin documentation for details
|
warning_text: see plugin documentation for details
|
||||||
|
ome_device_info:
|
||||||
|
redirect: dellemc.openmanage.ome_device_info
|
||||||
one_image_facts:
|
one_image_facts:
|
||||||
deprecation:
|
deprecation:
|
||||||
removal_version: 3.0.0
|
removal_version: 3.0.0
|
||||||
|
@ -565,6 +571,8 @@ plugin_routing:
|
||||||
postgresql:
|
postgresql:
|
||||||
redirect: community.postgresql.postgresql
|
redirect: community.postgresql.postgresql
|
||||||
module_utils:
|
module_utils:
|
||||||
|
remote_management.dellemc.dellemc_idrac:
|
||||||
|
redirect: dellemc.openmanage.dellemc_idrac
|
||||||
docker.common:
|
docker.common:
|
||||||
redirect: community.docker.common
|
redirect: community.docker.common
|
||||||
docker.swarm:
|
docker.swarm:
|
||||||
|
@ -579,6 +587,8 @@ plugin_routing:
|
||||||
redirect: community.hrobot.robot
|
redirect: community.hrobot.robot
|
||||||
kubevirt:
|
kubevirt:
|
||||||
redirect: community.kubevirt.kubevirt
|
redirect: community.kubevirt.kubevirt
|
||||||
|
remote_management.dellemc.ome:
|
||||||
|
redirect: dellemc.openmanage.ome
|
||||||
postgresql:
|
postgresql:
|
||||||
redirect: community.postgresql.postgresql
|
redirect: community.postgresql.postgresql
|
||||||
callback:
|
callback:
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1 +0,0 @@
|
||||||
./remote_management/dellemc/idrac_firmware.py
|
|
|
@ -1 +0,0 @@
|
||||||
./remote_management/dellemc/idrac_server_config_profile.py
|
|
|
@ -1 +0,0 @@
|
||||||
./remote_management/dellemc/ome_device_info.py
|
|
|
@ -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()
|
|
|
@ -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()
|
|
|
@ -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()
|
|
|
@ -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/slackpkg.py validate-modules:parameter-invalid
|
||||||
plugins/modules/packaging/os/urpmi.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/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_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/hpilo_info.py validate-modules:parameter-type-not-in-doc
|
||||||
plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc
|
||||||
|
|
|
@ -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/slackpkg.py validate-modules:parameter-invalid
|
||||||
plugins/modules/packaging/os/urpmi.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/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_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/hpilo_info.py validate-modules:parameter-type-not-in-doc
|
||||||
plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc
|
||||||
|
|
|
@ -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/slackpkg.py validate-modules:parameter-invalid
|
||||||
plugins/modules/packaging/os/urpmi.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/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_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/hpilo_info.py validate-modules:parameter-type-not-in-doc
|
||||||
plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc
|
plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc
|
||||||
|
|
|
@ -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")
|
|
|
@ -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
|
|
Loading…
Reference in a new issue