Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[minor_change] Add nd_rest as a new generic ND REST API module. (DCNE-242) #109

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
gmicol marked this conversation as resolved.
Show resolved Hide resolved
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also provide a file path with content?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should add a new attribute for that

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).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps I am missing this example

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgot to add an example for this case, will add it

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.
gmicol marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add link reference to nd api docs and or swagger reference?

"""

EXAMPLES = r"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a PATCH example, but see examples for all the others. Should we add?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should, I will add it.

- 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this module also support query strings in the path of GET requests?
Should we add some examples using parameters such as, filter, orderBy and the pagination options?

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
gmicol marked this conversation as resolved.
Show resolved Hide resolved
- 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
gmicol marked this conversation as resolved.
Show resolved Hide resolved
- 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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also validate json data?

try:
# Validate YAML/JSON string
gmicol marked this conversation as resolved.
Show resolved Hide resolved
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)
gmicol marked this conversation as resolved.
Show resolved Hide resolved

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

# Append previous state of the object
if method in ("PUT", "DELETE"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not do this for PATCH? that should contain previous state?

nd.existing = nd.query_obj(path, ignore_not_found_error=True)
nd.previous = nd.existing
gmicol marked this conversation as resolved.
Show resolved Hide resolved
if method != "GET":
gmicol marked this conversation as resolved.
Show resolved Hide resolved
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"]
gmicol marked this conversation as resolved.
Show resolved Hide resolved

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to add some additional logic for sanitisation prior to comparison, this way the user could control the comparison for those attributes that are not returned in payload

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're right we need to add some additional logic for sanitization prior to comparison

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
Loading