Skip to content

Commit

Permalink
[minor_change] Add nd_rest as a new generic ND REST API module.
Browse files Browse the repository at this point in the history
  • Loading branch information
gmicol committed Dec 17, 2024
1 parent 0d20fed commit 4ecdcb9
Show file tree
Hide file tree
Showing 7 changed files with 907 additions and 0 deletions.
179 changes: 179 additions & 0 deletions plugins/modules/nd_rest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2024, Gaspard Micol (@gmicol) <[email protected]>
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function

__metaclass__ = type

ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"}

DOCUMENTATION = r"""
---
module: nd_rest
short_description: Direct access to the Cisco Nexus Dashboard REST API
description:
- Enables the management of Cisco Nexus Dashboard (ND) through direct access to the Cisco ND REST API.
author:
- Gaspard Micol (@gmicol)
options:
method:
description:
- The HTTP method of the request.
- Using C(delete) is typically used for deleting objects.
- Using C(get) is typically used for querying objects.
- Using C(post) is typically used for modifying objects.
- Using C(put) is typically used for modifying existing objects.
- Using C(patch) is typically also used for modifying existing objects.
type: str
choices: [ delete, get, post, put, patch ]
default: get
aliases: [ action ]
path:
description:
- URI being used to execute API calls.
type: str
required: true
aliases: [ uri ]
content:
description:
- Sets the payload of the API request directly.
- This may be convenient to template simple requests.
- For anything complex use the C(template) lookup plugin (see examples).
type: raw
aliases: [ payload ]
extends_documentation_fragment:
- cisco.nd.modules
notes:
- Some payloads are known not to be idempotent, so be careful when constructing payloads.
"""

EXAMPLES = r"""
- name: Create Security Domain using POST method
cisco.nd.nd_rest:
host: nd
username: admin
password: SomeSecretPassword
path: /nexus/infra/api/aaa/v4/securitydomains
method: post
content:
{
"spec": {
"description": "Security Domain Test for nd_rest module.",
"name": "ansible_security_domain_test"
}
}
- name: Update Security Domain using PUT method
cisco.nd.nd_rest:
host: nd
username: admin
password: SomeSecretPassword
path: /nexus/infra/api/aaa/v4/securitydomains/ansible_security_domain_test
method: put
content:
{
"spec": {
"description": "Updated Security Domain Test for nd_rest module."
}
}
- name: Query Security Domain using GET method
cisco.nd.nd_rest:
host: nd
username: admin
password: SomeSecretPassword
path: /nexus/infra/api/aaa/v4/securitydomains/ansible_security_domain_test
method: get
register: quey_one
- name: Query all Security Domains using GET method
cisco.nd.nd_rest:
host: nd
username: admin
password: SomeSecretPassword
path: /nexus/infra/api/aaa/v4/securitydomains
method: get
register: quey_all
- name: Remove Security Domain using DELETE method
cisco.nd.nd_rest:
host: nd
username: admin
password: SomeSecretPassword
path: /nexus/infra/api/aaa/v4/securitydomains/ansible_security_domain_test
method: delete
"""

RETURN = r"""
"""

# Optional, only used for YAML validation
try:
import yaml

HAS_YAML = True
except Exception:
HAS_YAML = False

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.cisco.nd.plugins.module_utils.nd import NDModule, nd_argument_spec
from ansible.module_utils._text import to_text


def main():
argument_spec = nd_argument_spec()
argument_spec.update(
path=dict(type="str", required=True, aliases=["uri"]),
method=dict(type="str", default="get", choices=["delete", "get", "post", "put", "patch"], aliases=["action"]),
content=dict(type="raw", aliases=["payload"]),
)

module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)

content = module.params.get("content")
path = module.params.get("path")

nd = NDModule(module)

# Validate content/payload
if content and isinstance(content, str) and HAS_YAML:
try:
# Validate YAML/JSON string
content = yaml.safe_load(content)
except Exception as e:
module.fail_json(msg="Failed to parse provided JSON/YAML payload: %s" % to_text(e), exception=to_text(e), payload=content)

method = nd.params.get("method").upper()

# Append previous state of the object
if method in ("PUT", "DELETE"):
nd.existing = nd.query_obj(path, ignore_not_found_error=True)
nd.previous = nd.existing
if method != "GET":
nd.result["previous"] = nd.previous

# Perform request
if module.check_mode:
nd.result["jsondata"] = content
else:
nd.result["jsondata"] = nd.request(path, method=method, data=content)
nd.existing = nd.result["jsondata"]

# Report changes for idempotency depending on methods
nd.result["status"] = nd.status
if nd.result["jsondata"] != nd.previous and method != "GET":
nd.result["changed"] = True

# Report success
nd.exit_json(**nd.result)


if __name__ == "__main__":
main()
112 changes: 112 additions & 0 deletions tests/integration/targets/nd_rest/tasks/error_handling.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Test code for the ND modules
# Copyright: (c) 2024, Gaspard Micol (@gmciol) <[email protected]>

# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)

- name: Test that we have a Nexus Dashboard host, username and password
ansible.builtin.fail:
msg: 'Please define the following variables: ansible_host, ansible_user and ansible_password.'
when: ansible_host is not defined or ansible_user is not defined or ansible_password is not defined

- name: Set vars
ansible.builtin.set_fact:
nd_info: &nd_info
host: '{{ ansible_host }}'
username: '{{ ansible_user }}'
password: '{{ ansible_password }}'
validate_certs: '{{ ansible_httpapi_validate_certs | default(false) }}'
use_ssl: '{{ ansible_httpapi_use_ssl | default(true) }}'
use_proxy: '{{ ansible_httpapi_use_proxy | default(true) }}'
output_level: '{{ mso_output_level | default("info") }}'
timeout: 90

- name: Error when required parameter is missing
cisco.nd.nd_rest:
<<: *nd_info
method: post
content:
{
"spec": {
"name": "ansible_security_domain_test"
}
}
ignore_errors: true
register: error_missing_path

- name: Assert missing required parameter error
ansible.builtin.assert:
that:
- error_missing_path is failed
- error_missing_path.msg == "missing required arguments: path"

- name: Error when required attribute is missing
cisco.nd.nd_rest:
<<: *nd_info
path: /api/config/v2/addsite/
method: post
content:
{
"name": "ansible_error_site",
"siteType": "ACI",
"verifySecure": false,
"useProxy": false,
"aci": {
"userName": '{{ site_username }}',
"password": '{{ site_password }}',
},
}
ignore_errors: true
register: error_missing_site_address

- name: Assert missing required attribute error
ansible.builtin.assert:
that:
- error_missing_site_address is failed
- error_missing_site_address.info.body.error == "controller URL/IP required"
- error_missing_site_address.payload.error == "controller URL/IP required"
- error_missing_site_address.status == 400

- name: Error when input does not validate
cisco.nd.nd_rest:
<<: *nd_info
path: /nexus/infra/api/aaa/v4/securitydomains
method: post
content:
{
"spec": {
"name": "[invalid] name"
}
}
ignore_errors: true
register: error_invalid_name

- name: Assert invalid input error
ansible.builtin.assert:
that:
- error_invalid_name is failed
- error_invalid_name.info.body.errors == ["Invalid domain name"]
- error_invalid_name.payload.errors == ["Invalid domain name"]
- error_invalid_name.status == 500

- name: Error on name resolution
cisco.nd.nd_rest:
<<: *nd_info
host: foo.bar.cisco.com
path: /nexus/infra/api/aaa/v4/securitydomains
method: post
content:
{
"spec": {
"description": "Security Domain Test for nd_rest module.",
"name": "ansible_security_domain_test"
}
}
ignore_errors: true
register: error_name_resolution

- name: Assert name resolution error
ansible.builtin.assert:
that:
- error_name_resolution is failed
- '"Could not connect to https://foo.bar.cisco.com:443/nexus/infra/api/aaa/v4/securitydomains" in error_name_resolution.msg'
- '"Name or service not known" in error_name_resolution.msg'
Loading

0 comments on commit 4ecdcb9

Please sign in to comment.