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 support for annotation in aci_rest module (#437) #497

Merged
merged 9 commits into from
Dec 21, 2023
832 changes: 832 additions & 0 deletions plugins/module_utils/annotation_unsupported.py

Large diffs are not rendered by default.

48 changes: 45 additions & 3 deletions plugins/modules/aci_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# Copyright: (c) 2017, Dag Wieers (@dagwieers) <[email protected]>
# Copyright: (c) 2020, Cindy Zhao (@cizhao) <[email protected]>
# Copyright: (c) 2023, Samita Bhattacharjee (@samitab) <[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
Expand Down Expand Up @@ -62,6 +63,7 @@
default: false
extends_documentation_fragment:
- cisco.aci.aci
- cisco.aci.annotation

notes:
- Certain payloads are known not to be idempotent, so be careful when constructing payloads,
Expand All @@ -73,6 +75,7 @@
- XML payloads require the C(lxml) and C(xmljson) python libraries. For JSON payloads nothing special is needed.
- If you do not have any attributes, it may be necessary to add the "attributes" key with an empty dictionnary "{}" for value
as the APIC does expect the entry to precede any children.
- Annotation set directly in c(src) or C(content) will take precedent over the C(annotation) parameter.
seealso:
- module: cisco.aci.aci_tenant
- name: Cisco APIC REST API Configuration Guide
Expand All @@ -81,6 +84,7 @@
author:
- Dag Wieers (@dagwieers)
- Cindy Zhao (@cizhao)
- Samita Bhattacharjee (@samitab)
"""

EXAMPLES = r"""
Expand Down Expand Up @@ -284,8 +288,11 @@
HAS_YAML = False

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.cisco.aci.plugins.module_utils.aci import ACIModule, aci_argument_spec
from ansible_collections.cisco.aci.plugins.module_utils.aci import ACIModule, aci_argument_spec, aci_annotation_spec
from ansible.module_utils._text import to_text
from ansible_collections.cisco.aci.plugins.module_utils.annotation_unsupported import (
ANNOTATION_UNSUPPORTED,
)


def update_qsl(url, params):
Expand All @@ -303,6 +310,33 @@
return url + "?" + "&".join(["%s=%s" % (k, v) for k, v in params.items()])


def add_annotation(annotation, payload):
samiib marked this conversation as resolved.
Show resolved Hide resolved
"""Add annotation to payload only if it has not already been added"""
if annotation and isinstance(payload, dict):
for key, val in payload.items():
if key in ANNOTATION_UNSUPPORTED:
continue
att = val.get("attributes", {})

Check warning on line 319 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L318-L319

Added lines #L318 - L319 were not covered by tests
if "annotation" not in att.keys():
att["annotation"] = annotation

Check warning on line 321 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L321

Added line #L321 was not covered by tests
# Recursively add annotation to children
children = val.get("children", None)

Check warning on line 323 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L323

Added line #L323 was not covered by tests
if children:
for child in children:
add_annotation(annotation, child)

Check warning on line 326 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L326

Added line #L326 was not covered by tests


def add_annotation_xml(annotation, tree):
"""Add annotation to payload xml only if it has not already been added"""
if annotation:
for element in tree.iter():
if element.tag in ANNOTATION_UNSUPPORTED:
continue
ann = element.get("annotation")

Check warning on line 335 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L334-L335

Added lines #L334 - L335 were not covered by tests
if ann is None:
element.set("annotation", annotation)

Check warning on line 337 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L337

Added line #L337 was not covered by tests


class ACIRESTModule(ACIModule):
def changed(self, d):
"""Check ACI response for changes"""
Expand Down Expand Up @@ -335,6 +369,7 @@

def main():
argument_spec = aci_argument_spec()
argument_spec.update(aci_annotation_spec())
argument_spec.update(
path=dict(type="str", required=True, aliases=["uri"]),
method=dict(type="str", default="get", choices=["delete", "get", "post"], aliases=["action"]),
Expand All @@ -353,6 +388,7 @@
path = module.params.get("path")
src = module.params.get("src")
rsp_subtree_preserve = module.params.get("rsp_subtree_preserve")
annotation = module.params.get("annotation")

Check warning on line 391 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L391

Added line #L391 was not covered by tests

# Report missing file
file_exists = False
Expand Down Expand Up @@ -388,21 +424,27 @@
if rest_type == "json":
if content and isinstance(content, dict):
# Validate inline YAML/JSON
add_annotation(annotation, payload)

Check warning on line 427 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L427

Added line #L427 was not covered by tests
payload = json.dumps(payload)
elif payload and isinstance(payload, str) and HAS_YAML:
try:
# Validate YAML/JSON string
payload = json.dumps(yaml.safe_load(payload))
payload = yaml.safe_load(payload)
add_annotation(annotation, payload)
payload = json.dumps(payload)

Check warning on line 434 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L432-L434

Added lines #L432 - L434 were not covered by tests
except Exception as e:
module.fail_json(msg="Failed to parse provided JSON/YAML payload: {0}".format(to_text(e)), exception=to_text(e), payload=payload)
elif rest_type == "xml" and HAS_LXML_ETREE:
if content and isinstance(content, dict) and HAS_XMLJSON_COBRA:
# Validate inline YAML/JSON
add_annotation(annotation, payload)

Check warning on line 440 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L440

Added line #L440 was not covered by tests
payload = etree.tostring(cobra.etree(payload)[0], encoding="unicode")
elif payload and isinstance(payload, str):
try:
# Validate XML string
payload = etree.tostring(etree.fromstring(payload), encoding="unicode")
payload = etree.fromstring(payload)
add_annotation_xml(annotation, payload)
payload = etree.tostring(payload, encoding="unicode")

Check warning on line 447 in plugins/modules/aci_rest.py

View check run for this annotation

Codecov / codecov/patch

plugins/modules/aci_rest.py#L445-L447

Added lines #L445 - L447 were not covered by tests
except Exception as e:
module.fail_json(msg="Failed to parse provided XML payload: {0}".format(to_text(e)), payload=payload)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
that:
- error_on_input_validation is failed
- error_on_input_validation.method == 'POST'
- "error_on_input_validation.msg == 'APIC Error 801: property descr of tn-ansible_test failed validation for value \\'This is an [invalid] description\\''"
- "error_on_input_validation.msg is ansible.builtin.regex('APIC Error 801: property descr of.*tn-ansible_test failed validation for value \\'This is an \\[invalid\\] description\\'')"
- 'error_on_input_validation.response == "HTTP Error 400: Bad Request"'
- error_on_input_validation.status == 400
- "'current' not in error_on_input_validation"
Expand Down
159 changes: 159 additions & 0 deletions tests/integration/targets/aci_rest/tasks/json_inline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
that:
- cm_add_tenant is changed
- cm_add_tenant.proposed.fvTenant.attributes.name == "ansible_test"
- cm_add_tenant.proposed.fvTenant.attributes.annotation == "orchestrator:ansible"
- cm_verify_checkmode_tenant.current == []

- name: Add tenant (normal mode)
Expand All @@ -72,6 +73,7 @@
ansible.builtin.assert:
that:
- nm_add_tenant is changed
- nm_add_tenant.imdata.0.fvTenant.attributes.annotation == "orchestrator:ansible"
- nm_add_tenant_again is not changed

# CHANGE TENANT
Expand Down Expand Up @@ -179,3 +181,160 @@
ansible.builtin.assert:
that:
- nm_query_non_tenant is not changed

# VERIFY ANNOTATION SUPPORT
- name: Add tenant with annotation option
cisco.aci.aci_rest:
host: '{{ aci_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
validate_certs: '{{ aci_validate_certs | default(false) }}'
use_ssl: '{{ aci_use_ssl | default(true) }}'
use_proxy: '{{ aci_use_proxy | default(true) }}'
output_level: '{{ aci_output_level | default("info") }}'
path: /api/mo/uni.json
method: post
annotation: test:inoption
content:
{
"fvTenant": {
"attributes": {
"descr": "Ansible test tenant",
"name": "ansible_test"
}
}
}
register: nm_add_tenant_annotation_option

- name: Add tenant with annotation in content
cisco.aci.aci_rest:
host: '{{ aci_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
validate_certs: '{{ aci_validate_certs | default(false) }}'
use_ssl: '{{ aci_use_ssl | default(true) }}'
use_proxy: '{{ aci_use_proxy | default(true) }}'
output_level: '{{ aci_output_level | default("info") }}'
path: /api/mo/uni.json
method: post
content:
{
"fvTenant": {
"attributes": {
"descr": "Ansible test tenant",
"name": "ansible_test",
"annotation": "test:incontent"
}
}
}
register: nm_add_tenant_annotation_content

- name: Add tenant with annotation in content and option
cisco.aci.aci_rest:
host: '{{ aci_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
validate_certs: '{{ aci_validate_certs | default(false) }}'
use_ssl: '{{ aci_use_ssl | default(true) }}'
use_proxy: '{{ aci_use_proxy | default(true) }}'
output_level: '{{ aci_output_level | default("info") }}'
path: /api/mo/uni.json
method: post
annotation: test:inoption
content:
{
"fvTenant": {
"attributes": {
"descr": "Ansible test tenant",
"name": "ansible_test",
"annotation": "test:optionincontent"
}
}
}
register: nm_add_tenant_annotation_option_content

- name: Add tag to tenant with annotation unsupported
cisco.aci.aci_rest:
host: '{{ aci_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
validate_certs: '{{ aci_validate_certs | default(false) }}'
use_ssl: '{{ aci_use_ssl | default(true) }}'
use_proxy: '{{ aci_use_proxy | default(true) }}'
output_level: '{{ aci_output_level | default("info") }}'
path: /api/mo/uni/tn-ansible_test/tagKey-foo.json
method: post
annotation: test:inoption
content:
{
"tagTag": {
"attributes": {
"value": "bar"
}
}
}
register: nm_add_tag_no_annotation

- name: Remove tenant
cisco.aci.aci_rest: *tenant_absent

- name: Add tenant with children objects including annotation
cisco.aci.aci_rest:
host: '{{ aci_hostname }}'
username: '{{ aci_username }}'
password: '{{ aci_password }}'
validate_certs: '{{ aci_validate_certs | default(false) }}'
use_ssl: '{{ aci_use_ssl | default(true) }}'
use_proxy: '{{ aci_use_proxy | default(true) }}'
output_level: '{{ aci_output_level | default("info") }}'
path: /api/mo/uni.json
method: post
annotation: test:inoption
content:
{
"fvTenant": {
"attributes": {
"descr": "Ansible test tenant",
"name": "ansible_test"
},
"children": [
{
"fvCtx": {
"attributes": {
"name": "VRF1"
}
}
},
{
"fvAp": {
"attributes": {
"name": "Application1"
},
"children": [
{
"fvAEPg": {
"attributes": {
"name": "WebTier",
"annotation": "test:inchild"
}
}
}
]
}
}
]
}
}
register: nm_add_tenant_annotation_children

- name: Verify annotation support
assert:
that:
- nm_add_tenant_annotation_option.imdata.0.fvTenant.attributes.annotation == "test:inoption"
- nm_add_tenant_annotation_content.imdata.0.fvTenant.attributes.annotation == "test:incontent"
- nm_add_tenant_annotation_option_content.imdata.0.fvTenant.attributes.annotation == "test:optionincontent"
- nm_add_tag_no_annotation.imdata.0.tagTag.attributes.annotation is undefined
- nm_add_tenant_annotation_children.imdata.0.fvTenant.attributes.annotation == "test:inoption"
- nm_add_tenant_annotation_children.imdata.0.fvTenant.children.0.fvAp.attributes.annotation == "test:inoption"
- nm_add_tenant_annotation_children.imdata.0.fvTenant.children.0.fvAp.children.0.fvAEPg.attributes.annotation == "test:inchild"
- nm_add_tenant_annotation_children.imdata.0.fvTenant.children.2.fvCtx.attributes.annotation == "test:inoption"
Loading
Loading