intersight_rest_api module and integration tests. (#52430)

* intersight_rest_api module and integration tests.
Fix intersight module_utils issues when using POST/PATCH/DELETE.

* Update json returns based on code review
This commit is contained in:
David Soper 2019-02-19 09:40:40 -06:00 committed by Dag Wieers
parent 008313b8cc
commit 63ea76d174
4 changed files with 412 additions and 2 deletions

View file

@ -186,11 +186,13 @@ class IntersightModule():
try:
response, info = self.intersight_call(**options)
if not re.match(r'2..', str(info['status'])):
raise RuntimeError(info['status'], info['msg'])
raise RuntimeError(info['status'], info['msg'], info['body'])
except Exception as e:
self.module.fail_json(msg="API error: %s " % str(e))
return json.loads(response.read())
if response.length > 0:
return json.loads(response.read())
return {}
def intersight_call(self, http_method="", resource_path="", query_params=None, body=None, moid=None, name=None):
"""
@ -278,6 +280,7 @@ class IntersightModule():
# Generate the HTTP requests header
request_header = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Host': '{0}'.format(target_host),
'Date': '{0}'.format(cdate),
'Digest': 'SHA-256={0}'.format(b64_body_digest.decode('ascii')),

View file

@ -0,0 +1,252 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = r'''
---
module: intersight_rest_api
short_description: REST API configuration for Cisco Intersight
description:
- Direct REST API configuration for Cisco Intersight.
- All REST API resources and properties must be specified.
- For more information see L(Cisco Intersight,https://intersight.com/apidocs).
extends_documentation_fragment: intersight
options:
resource_path:
description:
- Resource URI being configured related to api_uri.
type: str
required: yes
query_params:
description:
- Query parameters for the Intersight API query languange.
type: dict
update_method:
description:
- The HTTP method used for update operations.
- Some Intersight resources require POST operations for modifications.
type: str
choices: [ patch, post ]
default: patch
api_body:
description:
- The paylod for API requests used to modify resources.
type: dict
state:
description:
- If C(present), will verify the resource is present and will create if needed.
- If C(absent), will verify the resource is absent and will delete if needed.
choices: [present, absent]
default: present
author:
- David Soper (@dsoper2)
- CiscoUcs (@CiscoUcs)
version_added: '2.8'
'''
EXAMPLES = r'''
- name: Configure Boot Policy
intersight_rest_api:
api_private_key: "{{ api_private_key }}"
api_key_id: "{{ api_key_id }}"
api_key_uri: "{{ api_key_uri }}"
validate_certs: "{{ validate_certs }}"
resource_path: /boot/PrecisionPolicies
query_params:
$filter: "Name eq 'vmedia-localdisk'"
api_body: {
"Name": "vmedia-hdd",
"ConfiguredBootMode": "Legacy",
"BootDevices": [
{
"ObjectType": "boot.VirtualMedia",
"Enabled": true,
"Name": "remote-vmedia",
"Subtype": "cimc-mapped-dvd"
},
{
"ObjectType": "boot.LocalDisk",
"Enabled": true,
"Name": "localdisk",
"Slot": "MRAID",
"Bootloader": null
}
],
}
state: present
- name: Delete Boot Policy
intersight_rest_api:
api_private_key: "{{ api_private_key }}"
api_key_id: "{{ api_key_id }}"
api_key_uri: "{{ api_key_uri }}"
validate_certs: "{{ validate_certs }}"
resource_path: /boot/PrecisionPolicies
query_params:
$filter: "Name eq 'vmedia-localdisk'"
state: absent
'''
RETURN = r'''
api_repsonse:
description: The API response output returned by the specified resource.
returned: always
type: dict
sample:
"api_response": {
"BootDevices": [
{
"Enabled": true,
"Name": "remote-vmedia",
"ObjectType": "boot.VirtualMedia",
"Subtype": "cimc-mapped-dvd"
},
{
"Bootloader": null,
"Enabled": true,
"Name": "boot-lun",
"ObjectType": "boot.LocalDisk",
"Slot": "MRAID"
}
],
"ConfiguredBootMode": "Legacy",
"Name": "vmedia-localdisk",
"ObjectType": "boot.PrecisionPolicy",
}
'''
import re
from ansible.module_utils.remote_management.intersight import IntersightModule, intersight_argument_spec
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
def get_resource(intersight):
'''
GET a resource and return the 1st element found
'''
options = {
'http_method': 'get',
'resource_path': intersight.module.params['resource_path'],
'query_params': intersight.module.params['query_params'],
}
response_dict = intersight.call_api(**options)
if response_dict.get('Results'):
# return the 1st list element
response_dict = response_dict['Results'][0]
return response_dict
def compare_values(expected, actual):
try:
for (key, value) in iteritems(expected):
if re.search(r'P(ass)?w(or)?d', key) or not actual.get(key):
# do not compare any password related attributes or attributes that are not in the actual resource
continue
if not compare_values(value, actual[key]):
return False
# loop complete with all items matching
return True
except (AttributeError, TypeError):
if expected and actual != expected:
return False
return True
def configure_resource(intersight, moid):
if not intersight.module.check_mode:
if moid:
# update the resource - user has to specify all the props they want updated
options = {
'http_method': intersight.module.params['update_method'],
'resource_path': intersight.module.params['resource_path'],
'body': intersight.module.params['api_body'],
'moid': moid,
}
response_dict = intersight.call_api(**options)
if response_dict.get('Results'):
# return the 1st element in the results list
intersight.result['api_response'] = response_dict['Results'][0]
else:
# create the resource
options = {
'http_method': 'post',
'resource_path': intersight.module.params['resource_path'],
'body': intersight.module.params['api_body'],
}
intersight.call_api(**options)
intersight.result['api_response'] = get_resource(intersight)
intersight.result['changed'] = True
def delete_resource(intersight, moid):
# delete resource and create empty api_response
if not intersight.module.check_mode:
options = {
'http_method': 'delete',
'resource_path': intersight.module.params['resource_path'],
'moid': moid,
}
intersight.call_api(**options)
intersight.result['api_response'] = {}
intersight.result['changed'] = True
def main():
argument_spec = intersight_argument_spec
argument_spec.update(
resource_path=dict(type='str', required=True),
query_params=dict(type='dict', default={}),
update_method=dict(type='str', choices=['patch', 'post'], default='patch'),
api_body=dict(type='dict', default={}),
state=dict(type='str', choices=['absent', 'present'], default='present'),
)
module = AnsibleModule(
argument_spec,
supports_check_mode=True,
)
intersight = IntersightModule(module)
intersight.result['api_response'] = {}
# get the current state of the resource
intersight.result['api_response'] = get_resource(intersight)
# determine requested operation (config, delete, or neither (get resource only))
if module.params['state'] == 'present':
request_delete = False
# api_body implies resource configuration through post/patch
request_config = bool(module.params['api_body'])
else: # state == 'absent'
request_delete = True
request_config = False
moid = None
resource_values_match = False
if (request_config or request_delete) and intersight.result['api_response'].get('Moid'):
# resource exists and moid was returned
moid = intersight.result['api_response']['Moid']
if request_config:
resource_values_match = compare_values(module.params['api_body'], intersight.result['api_response'])
else: # request_delete
delete_resource(intersight, moid)
if request_config and not resource_values_match:
configure_resource(intersight, moid)
module.exit_json(**intersight.result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,3 @@
# Not enabled, but can be used with Intersight by specifying API keys.
# See tasks/main.yml for examples.
unsupported

View file

@ -0,0 +1,152 @@
---
# Test code for the Cisco Intersight modules
# Copyright 2019, David Soper (@dsoper2)
- name: Setup API access variables
debug: msg="Setup API keys"
vars:
api_info: &api_info
api_private_key: "{{ api_private_key | default('~/Downloads/SSOSecretKey.txt') }}"
api_key_id: "{{ api_key_id | default('596cc79e5d91b400010d15ad/596cc7945d91b400010d154e/5b6275df3437357030a7795f') }}"
# Setup (clean environment)
- name: Boot policy Absent
intersight_rest_api: &boot_policy_absent
<<: *api_info
resource_path: /boot/PrecisionPolicies
query_params:
$filter: "Name eq 'vmedia-localdisk'"
state: absent
# Test present (check_mode)
- name: Boot policy present (check_mode)
intersight_rest_api: &boot_policy_present
<<: *api_info
resource_path: /boot/PrecisionPolicies
query_params:
$filter: "Name eq 'vmedia-localdisk'"
api_body: {
"Name": "vmedia-localdisk",
"ConfiguredBootMode": "Legacy",
"BootDevices": [
{
"ObjectType": "boot.VirtualMedia",
"Enabled": true,
"Name": "remote-vmedia",
"Subtype": "cimc-mapped-dvd"
},
{
"ObjectType": "boot.LocalDisk",
"Enabled": true,
"Name": "localdisk",
"Slot": "MRAID",
"Bootloader": null
}
],
}
check_mode: true
register: cm_boot_policy_present
# Present (normal mode)
- name: Boot policy present (normal mode)
intersight_rest_api: *boot_policy_present
register: nm_boot_policy_present
# Test present again (idempotent)
- name: Boot policy present again (check_mode)
intersight_rest_api: *boot_policy_present
check_mode: true
register: cm_boot_policy_present_again
# Present again (normal mode)
- name: Boot policy present again (normal mode)
intersight_rest_api: *boot_policy_present
register: nm_boot_policy_present_again
# Verfiy present
- name: Verify Boot policy present results
assert:
that:
- cm_boot_policy_present.changed == nm_boot_policy_present.changed == true
- cm_boot_policy_present_again.changed == nm_boot_policy_present_again.changed == false
# Test change (check_mode)
- name: Boot policy change (check_mode)
intersight_rest_api: &boot_policy_change
<<: *api_info
resource_path: /boot/PrecisionPolicies
query_params:
$filter: "Name eq 'vmedia-localdisk'"
api_body: {
"Name": "vmedia-localdisk",
"ConfiguredBootMode": "Legacy",
"BootDevices": [
{
"ObjectType": "boot.VirtualMedia",
"Enabled": true,
"Name": "remote-vmedia",
"Subtype": "cimc-mapped-dvd"
},
{
"ObjectType": "boot.LocalDisk",
"Enabled": true,
"Name": "localdisk",
"Slot": "HBA",
"Bootloader": null
}
],
}
check_mode: true
register: cm_boot_policy_change
# Change (normal mode)
- name: Boot policy change (normal mode)
intersight_rest_api: *boot_policy_change
register: nm_boot_policy_change
# Test change again (idempotent)
- name: Boot policy again (check_mode)
intersight_rest_api: *boot_policy_change
check_mode: true
register: cm_boot_policy_change_again
# Change again (normal mode)
- name: Boot policy change again (normal mode)
intersight_rest_api: *boot_policy_change
register: nm_boot_policy_change_again
# Verfiy change
- name: Verify Boot policy change results
assert:
that:
- cm_boot_policy_change.changed == nm_boot_policy_change.changed == true
- cm_boot_policy_change_again.changed == nm_boot_policy_change_again.changed == false
# Teardown (clean environment)
- name: Boot policy absent (check_mode)
intersight_rest_api: *boot_policy_absent
check_mode: true
register: cm_boot_policy_absent
# Absent (normal mode)
- name: Boot policy absent (normal mode)
intersight_rest_api: *boot_policy_absent
register: nm_boot_policy_absent
# Test absent again (idempotent)
- name: Boot policy absent again (check_mode)
intersight_rest_api: *boot_policy_absent
check_mode: true
register: cm_boot_policy_absent_again
# Absent again (normal mode)
- name: Boot policy absent again (normal mode)
intersight_rest_api: *boot_policy_absent
register: nm_boot_policy_absent_again
# Verfiy absent
- name: Verify Boot policy absent results
assert:
that:
- cm_boot_policy_absent.changed == nm_boot_policy_absent.changed == true
- cm_boot_policy_absent_again.changed == nm_boot_policy_absent_again.changed == false