From f8b0821f562ef756e9998231c10a1133a07efdc1 Mon Sep 17 00:00:00 2001 From: Joe Wesch Date: Thu, 11 Jul 2024 21:21:02 -0500 Subject: [PATCH] Adds Contacts and Teams modules --- plugins/lookup/lookup.py | 2 + plugins/module_utils/extras.py | 2 + plugins/module_utils/utils.py | 10 ++ plugins/modules/contact.py | 135 +++++++++++++++ plugins/modules/team.py | 135 +++++++++++++++ tests/integration/nautobot-populate.py | 12 ++ .../targets/latest/tasks/contact.yml | 150 +++++++++++++++++ .../integration/targets/latest/tasks/main.yml | 18 ++ .../integration/targets/latest/tasks/team.yml | 154 ++++++++++++++++++ 9 files changed, 618 insertions(+) create mode 100644 plugins/modules/contact.py create mode 100644 plugins/modules/team.py create mode 100644 tests/integration/targets/latest/tasks/contact.yml create mode 100644 tests/integration/targets/latest/tasks/team.yml diff --git a/plugins/lookup/lookup.py b/plugins/lookup/lookup.py index eccfa545..92c169a8 100644 --- a/plugins/lookup/lookup.py +++ b/plugins/lookup/lookup.py @@ -178,6 +178,7 @@ def get_endpoint(nautobot, term): "console-ports": {"endpoint": nautobot.dcim.console_ports}, "console-server-port-templates": {"endpoint": nautobot.dcim.console_server_port_templates}, "console-server-ports": {"endpoint": nautobot.dcim.console_server_ports}, + "contacts": {"endpoint": nautobot.extras.contacts}, "custom-fields": {"endpoint": nautobot.extras.custom_fields}, "custom-field-choices": {"endpoint": nautobot.extras.custom_field_choices}, "device-bay-templates": {"endpoint": nautobot.dcim.device_bay_templates}, @@ -225,6 +226,7 @@ def get_endpoint(nautobot, term): "services": {"endpoint": nautobot.ipam.services}, "statuses": {"endpoint": nautobot.extras.statuses}, "tags": {"endpoint": nautobot.extras.tags}, + "teams": {"endpoint": nautobot.extras.teams}, "tenant-groups": {"endpoint": nautobot.tenancy.tenant_groups}, "tenants": {"endpoint": nautobot.tenancy.tenants}, "topology-maps": {"endpoint": nautobot.extras.topology_maps}, diff --git a/plugins/module_utils/extras.py b/plugins/module_utils/extras.py index 13e8505f..a104c7ac 100644 --- a/plugins/module_utils/extras.py +++ b/plugins/module_utils/extras.py @@ -15,6 +15,8 @@ NB_RELATIONSHIP_ASSOCIATIONS = "relationship_associations" NB_CUSTOM_FIELDS = "custom_fields" NB_CUSTOM_FIELD_CHOICES = "custom_field_choices" +NB_CONTACT = "contacts" +NB_TEAM = "teams" class NautobotExtrasModule(NautobotModule): diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py index bf20947a..36700a60 100644 --- a/plugins/module_utils/utils.py +++ b/plugins/module_utils/utils.py @@ -66,12 +66,14 @@ "virtual_chassis", ], extras=[ + "contacts", "custom_fields", "custom_field_choices", "relationship_associations", "roles", "statuses", "tags", + "teams", ], ipam=[ "ip_addresses", @@ -150,6 +152,7 @@ "cluster": "clusters", "cluster_group": "cluster_groups", "cluster_type": "cluster_types", + "contacts": "contacts", "dcim.consoleport": "console_ports", "dcim.consoleserverport": "console_server_ports", "dcim.frontport": "front_ports", @@ -198,6 +201,7 @@ "status": "statuses", "tags": "tags", "tagged_vlans": "vlans", + "teams": "teams", "tenant": "tenants", "tenant_group": "tenant_groups", "termination_a": "interfaces", @@ -223,6 +227,7 @@ "console_port_templates": "console_port_template", "console_server_ports": "console_server_port", "console_server_port_templates": "console_server_port_template", + "contacts": "contact", "custom_fields": "custom_field", "custom_field_choices": "custom_field_choice", "device_bays": "device_bay", @@ -261,6 +266,7 @@ "services": "services", "statuses": "statuses", "tags": "tags", + "teams": "team", "tenants": "tenant", "tenant_groups": "tenant_group", "virtual_chassis": "virtual_chassis", @@ -284,6 +290,8 @@ "console_port_template": set(["name", "device_type"]), "console_server_port": set(["name", "device"]), "console_server_port_template": set(["name", "device_type"]), + "contact": set(["name", "phone", "email"]), + "contacts": set(["name", "phone", "email"]), "custom_field": set(["label"]), "custom_field_choice": set(["value", "custom_field"]), "dcim.consoleport": set(["name", "device"]), @@ -341,6 +349,8 @@ "statuses": set(["name"]), "tags": set(["name"]), "tagged_vlans": set(["group", "name", "location", "vid", "vlan_group", "tenant"]), + "team": set(["name", "phone", "email"]), + "teams": set(["name", "phone", "email"]), "tenant": set(["name"]), "tenant_group": set(["name"]), "termination_a": set(["name", "device", "virtual_machine"]), diff --git a/plugins/modules/contact.py b/plugins/modules/contact.py new file mode 100644 index 00000000..9dfe1450 --- /dev/null +++ b/plugins/modules/contact.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2024, Network to Code (@networktocode) +# 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 + +DOCUMENTATION = r""" +--- +module: contact +short_description: Creates or removes contacts from Nautobot +description: + - Creates or removes contacts from Nautobot +notes: + - Tags should be defined as a YAML list + - This should be ran with connection C(local) and hosts C(localhost) +author: + - Joe Wesch (@joewesch) +requirements: + - pynautobot +version_added: "5.3.0" +extends_documentation_fragment: + - networktocode.nautobot.fragments.base + - networktocode.nautobot.fragments.tags + - networktocode.nautobot.fragments.custom_fields +options: + name: + description: + - The name of the contact + required: true + type: str + phone: + description: + - The phone number of the contact + required: false + type: str + email: + description: + - The email of the contact + required: false + type: str + address: + description: + - The address of the contact + required: false + type: str + teams: + description: + - The teams the contact is associated with + required: false + type: list + elements: raw + comments: + description: + - Comments about the contact + required: false + type: str +""" + +EXAMPLES = r""" +--- +- name: Create a contact + networktocode.nautobot.contact: + url: http://nautobot.local + token: thisIsMyToken + name: My Contact + phone: 123-456-7890 + email: user@example.com + address: 1234 Main St + teams: + - name: team1 + - name: team2 + comments: My Comments + tags: + - tag1 + - tag2 + custom_fields: + my_custom_field: my_value + state: present + +- name: Delete a contact + networktocode.nautobot.contact: + url: http://nautobot.local + token: thisIsMyToken + name: My Contact + state: absent +""" + +RETURN = r""" +contact: + description: Serialized object as created or already existent within Nautobot + returned: success (when I(state=present)) + type: dict +msg: + description: Message indicating failure or info about what has been achieved + returned: always + type: str +""" + +from ansible_collections.networktocode.nautobot.plugins.module_utils.utils import NAUTOBOT_ARG_SPEC +from ansible_collections.networktocode.nautobot.plugins.module_utils.extras import ( + NautobotExtrasModule, + NB_CONTACT, +) +from ansible.module_utils.basic import AnsibleModule +from copy import deepcopy + + +def main(): + """ + Main entry point for module execution + """ + argument_spec = deepcopy(NAUTOBOT_ARG_SPEC) + argument_spec.update( + dict( + name=dict(required=True, type="str"), + phone=dict(required=False, type="str"), + email=dict(required=False, type="str"), + address=dict(required=False, type="str"), + teams=dict(required=False, type="list", elements="raw"), + comments=dict(required=False, type="str"), + tags=dict(required=False, type="list", elements="raw"), + custom_fields=dict(required=False, type="dict"), + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + contact = NautobotExtrasModule(module, NB_CONTACT) + contact.run() + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/plugins/modules/team.py b/plugins/modules/team.py new file mode 100644 index 00000000..fc886703 --- /dev/null +++ b/plugins/modules/team.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2024, Network to Code (@networktocode) +# 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 + +DOCUMENTATION = r""" +--- +module: team +short_description: Creates or removes teams from Nautobot +description: + - Creates or removes teams from Nautobot +notes: + - Tags should be defined as a YAML list + - This should be ran with connection C(local) and hosts C(localhost) +author: + - Joe Wesch (@joewesch) +requirements: + - pynautobot +version_added: "5.3.0" +extends_documentation_fragment: + - networktocode.nautobot.fragments.base + - networktocode.nautobot.fragments.tags + - networktocode.nautobot.fragments.custom_fields +options: + name: + description: + - The name of the team + required: true + type: str + phone: + description: + - The phone number of the team + required: false + type: str + email: + description: + - The email of the team + required: false + type: str + address: + description: + - The address of the team + required: false + type: str + contacts: + description: + - The contacts the team is associated with + required: false + type: list + elements: raw + comments: + description: + - Comments about the team + required: false + type: str +""" + +EXAMPLES = r""" +--- +- name: Create a team + networktocode.nautobot.team: + url: http://nautobot.local + token: thisIsMyToken + name: My Team + phone: 123-456-7890 + email: user@example.com + address: 1234 Main St + contacts: + - name: contact1 + - name: contact2 + comments: My Comments + tags: + - tag1 + - tag2 + custom_fields: + my_custom_field: my_value + state: present + +- name: Delete a team + networktocode.nautobot.team: + url: http://nautobot.local + token: thisIsMyToken + name: My Team + state: absent +""" + +RETURN = r""" +team: + description: Serialized object as created or already existent within Nautobot + returned: success (when I(state=present)) + type: dict +msg: + description: Message indicating failure or info about what has been achieved + returned: always + type: str +""" + +from ansible_collections.networktocode.nautobot.plugins.module_utils.utils import NAUTOBOT_ARG_SPEC +from ansible_collections.networktocode.nautobot.plugins.module_utils.extras import ( + NautobotExtrasModule, + NB_TEAM, +) +from ansible.module_utils.basic import AnsibleModule +from copy import deepcopy + + +def main(): + """ + Main entry point for module execution + """ + argument_spec = deepcopy(NAUTOBOT_ARG_SPEC) + argument_spec.update( + dict( + name=dict(required=True, type="str"), + phone=dict(required=False, type="str"), + email=dict(required=False, type="str"), + address=dict(required=False, type="str"), + contacts=dict(required=False, type="list", elements="raw"), + comments=dict(required=False, type="str"), + tags=dict(required=False, type="list", elements="raw"), + custom_fields=dict(required=False, type="dict"), + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + team = NautobotExtrasModule(module, NB_TEAM) + team.run() + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/tests/integration/nautobot-populate.py b/tests/integration/nautobot-populate.py index c3b2778c..c4fdc671 100755 --- a/tests/integration/nautobot-populate.py +++ b/tests/integration/nautobot-populate.py @@ -597,5 +597,17 @@ def make_nautobot_calls(endpoint, payload): ] created_custom_fields = make_nautobot_calls(nb.extras.custom_fields, custom_fields) +############### +# v2.2+ items # +############### +if nautobot_version > version.parse("2.1"): + # Create Teams + teams = [{"name": "My Test Team"}] + created_teams = make_nautobot_calls(nb.extras.teams, teams) + + # Create Contacts + contacts = [{"name": "My Contact"}, {"name": "My Contact 2"}] + created_contacts = make_nautobot_calls(nb.extras.contacts, contacts) + if ERRORS: sys.exit("Errors have occurred when creating objects, and should have been printed out. Check previous output.") diff --git a/tests/integration/targets/latest/tasks/contact.yml b/tests/integration/targets/latest/tasks/contact.yml new file mode 100644 index 00000000..9f6babb4 --- /dev/null +++ b/tests/integration/targets/latest/tasks/contact.yml @@ -0,0 +1,150 @@ +--- +## +## +### PYNAUTOBOT_CONTACT +## +## +- block: + - set_fact: + test_team: "{{ lookup('networktocode.nautobot.lookup', 'teams', api_endpoint=nautobot_url, token=nautobot_token, api_filter='name=\"My Test Team\"') }}" + + - name: "1 - Create contact within Nautobot with only required information" + networktocode.nautobot.contact: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Contact + register: test_create_min + + - name: "1 - ASSERT" + assert: + that: + - test_create_min is changed + - test_create_min['diff']['before']['state'] == "absent" + - test_create_min['diff']['after']['state'] == "present" + - test_create_min['contact']['name'] == "Test Contact" + - test_create_min['msg'] == "contact Test Contact created" + + - name: "2 - Duplicate" + networktocode.nautobot.contact: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Contact + + register: test_create_idem + - name: "2 - ASSERT" + assert: + that: + - not test_create_idem['changed'] + - test_create_idem['msg'] == "contact Test Contact already exists" + - test_create_idem['contact']['name'] == "Test Contact" + + - name: "3 - Update contact" + networktocode.nautobot.contact: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Contact + comments: Test Comments + register: test_update + + - name: "3 - ASSERT" + assert: + that: + - test_update is changed + - test_update['diff']['before']['comments'] == "" + - test_update['diff']['after']['comments'] == "Test Comments" + + - name: "4 - Update idempotent" + networktocode.nautobot.contact: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Contact + comments: Test Comments + register: test_update_idem + + - name: "4 - ASSERT" + assert: + that: + - not test_update_idem['changed'] + - test_update_idem['msg'] == "contact Test Contact already exists" + - test_update_idem['contact']['name'] == "Test Contact" + + - name: "5 - Create contact with all parameters" + networktocode.nautobot.contact: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Contact 2 + phone: "123456789" + email: user@example.com + address: Test Address + teams: + - name: My Test Team + comments: Test Comments + register: test_create_max + + - name: "5 - ASSERT" + assert: + that: + - test_create_max is changed + - test_create_max['diff']['before']['state'] == "absent" + - test_create_max['diff']['after']['state'] == "present" + - test_create_max['contact']['name'] == "Test Contact 2" + - test_create_max['contact']['phone'] == "123456789" + - test_create_max['contact']['email'] == "user@example.com" + - test_create_max['contact']['address'] == "Test Address" + - test_create_max['contact']['teams'][0] == test_team['key'] + - test_create_max['contact']['comments'] == "Test Comments" + + - name: "6 - Duplicate create with all parameters" + networktocode.nautobot.contact: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Contact 2 + phone: "123456789" + email: user@example.com + address: Test Address + teams: + - name: My Test Team + comments: Test Comments + register: test_create_max_idem + + - name: "6 - ASSERT" + assert: + that: + - not test_create_max_idem['changed'] + - test_create_max_idem['msg'] == "contact Test Contact 2 already exists" + - test_create_max_idem['contact']['name'] == "Test Contact 2" + + - name: "7 - Delete contact" + networktocode.nautobot.contact: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Contact 2 + state: absent + register: test_delete + + - name: "7 - ASSERT" + assert: + that: + - test_delete is changed + - test_delete['diff']['before']['state'] == "present" + - test_delete['diff']['after']['state'] == "absent" + - test_delete['contact']['name'] == "Test Contact 2" + - "'deleted' in test_delete['msg']" + + - name: "8 - Delete idempotent" + networktocode.nautobot.contact: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Contact 2 + state: absent + register: test_delete_idem + + - name: "8 - ASSERT" + assert: + that: + - not test_delete_idem['changed'] + - "'already absent' in test_delete_idem['msg']" + + when: + # Contacts are only available on Nautobot 2.2+ + - "nautobot_version is version('2.2', '>=')" diff --git a/tests/integration/targets/latest/tasks/main.yml b/tests/integration/targets/latest/tasks/main.yml index d497f62d..53cf0740 100644 --- a/tests/integration/targets/latest/tasks/main.yml +++ b/tests/integration/targets/latest/tasks/main.yml @@ -529,3 +529,21 @@ - vlan_location tags: - vlan_location + +- name: "PYNAUTOBOT_CONTACT TESTS" + include_tasks: + file: "contact.yml" + apply: + tags: + - contact + tags: + - contact + +- name: "PYNAUTOBOT_TEAM TESTS" + include_tasks: + file: "team.yml" + apply: + tags: + - team + tags: + - team diff --git a/tests/integration/targets/latest/tasks/team.yml b/tests/integration/targets/latest/tasks/team.yml new file mode 100644 index 00000000..c9f0f88a --- /dev/null +++ b/tests/integration/targets/latest/tasks/team.yml @@ -0,0 +1,154 @@ +--- +## +## +### PYNAUTOBOT_TEAM +## +## +- block: + - set_fact: + test_contact: "{{ lookup('networktocode.nautobot.lookup', 'contacts', api_endpoint=nautobot_url, token=nautobot_token, api_filter='name=\"My Contact\"') }}" + test_contact2: "{{ lookup('networktocode.nautobot.lookup', 'contacts', api_endpoint=nautobot_url, token=nautobot_token, api_filter='name=\"My Contact 2\"') }}" + + - name: "1 - Create team within Nautobot with only required information" + networktocode.nautobot.team: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Team + register: test_create_min + + - name: "1 - ASSERT" + assert: + that: + - test_create_min is changed + - test_create_min['diff']['before']['state'] == "absent" + - test_create_min['diff']['after']['state'] == "present" + - test_create_min['team']['name'] == "Test Team" + - test_create_min['msg'] == "team Test Team created" + + - name: "2 - Duplicate" + networktocode.nautobot.team: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Team + + register: test_create_idem + - name: "2 - ASSERT" + assert: + that: + - not test_create_idem['changed'] + - test_create_idem['msg'] == "team Test Team already exists" + - test_create_idem['team']['name'] == "Test Team" + + - name: "3 - Update team" + networktocode.nautobot.team: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Team + comments: Test Comments + register: test_update + + - name: "3 - ASSERT" + assert: + that: + - test_update is changed + - test_update['diff']['before']['comments'] == "" + - test_update['diff']['after']['comments'] == "Test Comments" + + - name: "4 - Update idempotent" + networktocode.nautobot.team: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Team + comments: Test Comments + register: test_update_idem + + - name: "4 - ASSERT" + assert: + that: + - not test_update_idem['changed'] + - test_update_idem['msg'] == "team Test Team already exists" + - test_update_idem['team']['name'] == "Test Team" + + - name: "5 - Create team with all parameters" + networktocode.nautobot.team: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Team 2 + phone: "123456789" + email: user@example.com + address: Test Address + contacts: + - name: My Contact + - name: My Contact 2 + comments: Test Comments + register: test_create_max + + - name: "5 - ASSERT" + assert: + that: + - test_create_max is changed + - test_create_max['diff']['before']['state'] == "absent" + - test_create_max['diff']['after']['state'] == "present" + - test_create_max['team']['name'] == "Test Team 2" + - test_create_max['team']['phone'] == "123456789" + - test_create_max['team']['email'] == "user@example.com" + - test_create_max['team']['address'] == "Test Address" + - test_create_max['team']['contacts'][0] == test_contact['key'] + - test_create_max['team']['contacts'][1] == test_contact2['key'] + - test_create_max['team']['comments'] == "Test Comments" + + - name: "6 - Duplicate create with all parameters" + networktocode.nautobot.team: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Team 2 + phone: "123456789" + email: user@example.com + address: Test Address + contacts: + - name: My Contact + - name: My Contact 2 + comments: Test Comments + register: test_create_max_idem + + - name: "6 - ASSERT" + assert: + that: + - not test_create_max_idem['changed'] + - test_create_max_idem['msg'] == "team Test Team 2 already exists" + - test_create_max_idem['team']['name'] == "Test Team 2" + + - name: "7 - Delete team" + networktocode.nautobot.team: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Team 2 + state: absent + register: test_delete + + - name: "7 - ASSERT" + assert: + that: + - test_delete is changed + - test_delete['diff']['before']['state'] == "present" + - test_delete['diff']['after']['state'] == "absent" + - test_delete['team']['name'] == "Test Team 2" + - "'deleted' in test_delete['msg']" + + - name: "8 - Delete idempotent" + networktocode.nautobot.team: + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + name: Test Team 2 + state: absent + register: test_delete_idem + + - name: "8 - ASSERT" + assert: + that: + - not test_delete_idem['changed'] + - "'already absent' in test_delete_idem['msg']" + + when: + # Teams are only available on Nautobot 2.2+ + - "nautobot_version is version('2.2', '>=')"