Adding dell ome device_info module (#53438)

* Adding device_facts module for contribution

* changes added for pylint error

* Updated code to solve ansible-test compile error

* Changes to avoide comile error added

* Review Comments changes update

* Avoided blank line

* pylint error changes

* Removed ansible_facts return in error case

* Updated description

* modules renamed

* changing from ansible_facts to device_info

* avoide pep8 error

* Updated sample output

* version changed to 2.9

* Changed Copyright license to BSD

* Changed 3-clause BSD license to 2-clause BSD

* Added unit test support for ome_device_info

* version change

* removed pylint error in unit test modules

* Avoid Sanity error for unit test modules

* updated version
This commit is contained in:
Sajna Shetty 2019-06-10 16:11:48 +05:30 committed by ansibot
parent ef2d70a806
commit e25269e1b3
9 changed files with 866 additions and 0 deletions

View file

@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
# Dell EMC OpenManage Ansible Modules
# Version 1.3
# Copyright (C) 2019 Dell Inc. or its subsidiaries. All Rights Reserved.
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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

View file

@ -0,0 +1,418 @@
#!/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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: ome_device_info
short_description: Retrieves the information about Device.
version_added: "2.9"
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.
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.
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.
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.
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.
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.
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.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()

View file

@ -0,0 +1,78 @@
# -*- 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
import pytest
from ansible.module_utils.urls import ConnectionError, SSLValidationError
from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
from ansible.module_utils.remote_management.dellemc.ome import RestOME
from units.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.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.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.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.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")

View file

@ -0,0 +1,189 @@
# -*- 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
import pytest
from units.modules.utils import set_module_args, exit_json, \
fail_json, AnsibleFailJson, AnsibleExitJson
from ansible.module_utils import basic
from ansible.modules.remote_management.dellemc.ome 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.modules.remote_management.dellemc.ome.ome_device_info.RestOME')
return connection_class_mock.return_value
@pytest.fixture
def response_mock(self, mocker):
response_class_mock = mocker.patch('ansible.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.modules.remote_management.dellemc.ome.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.modules.remote_management.dellemc.ome.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.modules.remote_management.dellemc.ome.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.modules.remote_management.dellemc.ome.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.modules.remote_management.dellemc.ome.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