From b44563efb860e48bab6abd3ba389160540c9cf10 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 29 Oct 2025 19:15:15 -0400 Subject: [PATCH 01/13] Add contact assignment module --- plugins/module_utils/netbox_tenancy.py | 84 ++++++++ plugins/module_utils/netbox_utils.py | 4 + plugins/modules/netbox_contact_assignment.py | 196 +++++++++++++++++++ 3 files changed, 284 insertions(+) create mode 100644 plugins/modules/netbox_contact_assignment.py diff --git a/plugins/module_utils/netbox_tenancy.py b/plugins/module_utils/netbox_tenancy.py index dde8e4819..021e9449c 100644 --- a/plugins/module_utils/netbox_tenancy.py +++ b/plugins/module_utils/netbox_tenancy.py @@ -15,14 +15,73 @@ NB_TENANTS = "tenants" NB_TENANT_GROUPS = "tenant_groups" NB_CONTACTS = "contacts" +NB_CONTACT_ASSIGNMENTS = "contact_assignments" NB_CONTACT_GROUPS = "contact_groups" NB_CONTACT_ROLES = "contact_roles" +OBJECT_ENDPOINTS = { + "circuit": "circuits", + "cluster": "clusters", + "cluster_group": "cluster_groups", + "contact": "contacts", + "contact_role": "contact_roles", + "device": "devices", + "location": "locations", + "manufacturer": "manufacturers", + "power_panel": "power_panels", + "provider": "providers", + "rack": "racks", + "region": "regions", + "site": "sites", + "site_group": "site_groups", + "tenant": "tenants", + "virtual_machine": "virtual_machines", +} +OBJECT_TYPES = { + "circuit": "circuits.circuit", + "cluster": "virtualization.cluster", + "cluster_group": "virtualization.clustergroup", + "device": "dcim.device", + "location": "dcim.location", + "manufacturer": "dcim.manufacturer", + "power_panel": "dcim.powerpanel", + "provider": "circuits.provider", + "rack": "dcim.rack", + "region": "dcim.region", + "site": "dcim.site", + "site_group": "dcim.sitegroup", + "tenant": "tenancy.tenant", + "virtual_machine": "virtualization.virtualmachine", +} +OBJECT_NAME_FIELD = { + "circuit": "cid", + # If unspecified, the default is "name" +} + + class NetboxTenancyModule(NetboxModule): def __init__(self, module, endpoint): super().__init__(module, endpoint) + def get_object_by_name(self, object_type: str, object_name: str): + endpoint = OBJECT_ENDPOINTS[object_type] + app = self._find_app(endpoint) + nb_app = getattr(self.nb, app) + nb_endpoint = getattr(nb_app, endpoint) + + name_field = OBJECT_NAME_FIELD.get(object_type) + if name_field is None: + name_field = "name" + + query_params = {name_field: object_name} + result = self._nb_endpoint_get(nb_endpoint, query_params, object_name) + if not result: + self._handle_errors( + msg="Could not resolve id of %s: %s" % (object_type, object_name) + ) + return result + def run(self): """ This function should have all necessary code for endpoints within the application @@ -31,7 +90,9 @@ def run(self): - tenants - tenant groups - contacts + - contact assignments - contact groups + - contact roles """ # Used to dynamically set key when returning results endpoint_name = ENDPOINT_NAME_MAPPING[self.endpoint] @@ -45,6 +106,23 @@ def run(self): data = self.data + # For ease and consistency of use, the contact assignment module takes the name of the contact, role, and target object rather than an ID or slug. + # We must massage the data a bit by looking up the ID corresponding to the given name so that we can pass the ID to the API. + if self.endpoint == "contact_assignments": + # Not an identifier, just to populate the message field + name = f"{data['contact']} -> {data['object_name']}" + + object_type = data["object_type"] + obj = self.get_object_by_name(object_type, data["object_name"]) + contact = self.get_object_by_name("contact", data["contact"]) + role = self.get_object_by_name("contact_role", data["role"]) + + data["object_type"] = OBJECT_TYPES[object_type] + data["object_id"] = obj.id + del data["object_name"] # object_id replaces object_name + data["contact"] = contact.id + data["role"] = role.id + # Used for msg output if data.get("name"): name = data["name"] @@ -58,6 +136,12 @@ def run(self): object_query_params = self._build_query_params( endpoint_name, data, user_query_params ) + + # For some reason, when creating a new contact assignment, role must be an ID + # But when querying contact assignments, the role must be a slug + if self.endpoint == "contact_assignments": + object_query_params["role"] = role.slug + self.nb_object = self._nb_endpoint_get(nb_endpoint, object_query_params, name) if self.state == "present": diff --git a/plugins/module_utils/netbox_utils.py b/plugins/module_utils/netbox_utils.py index 2d5549849..9c3c85c59 100644 --- a/plugins/module_utils/netbox_utils.py +++ b/plugins/module_utils/netbox_utils.py @@ -114,6 +114,7 @@ "tenants": {}, "tenant_groups": {}, "contacts": {}, + "contact_assignments": {}, "contact_groups": {}, "contact_roles": {}, }, @@ -366,6 +367,7 @@ "console_server_ports": "console_server_port", "console_server_port_templates": "console_server_port_template", "contacts": "contact", + "contact_assignments": "contact_assignment", "contact_groups": "contact_group", "contact_roles": "contact_role", "custom_fields": "custom_field", @@ -476,6 +478,7 @@ "console_server_port": set(["name", "device"]), "console_server_port_template": set(["name", "device_type"]), "contact": set(["name", "group"]), + "contact_assignment": set(["object_type", "object_id", "contact", "role"]), "contact_group": set(["name"]), "contact_role": set(["name"]), "custom_field": set(["name"]), @@ -613,6 +616,7 @@ "console_port_templates": set(["type"]), "console_server_ports": set(["type"]), "console_server_port_templates": set(["type"]), + "contact_assignments": set(["priority"]), "devices": set(["status", "face"]), "device_types": set(["subdevice_role"]), "front_ports": set(["type"]), diff --git a/plugins/modules/netbox_contact_assignment.py b/plugins/modules/netbox_contact_assignment.py new file mode 100644 index 000000000..7297bbc2e --- /dev/null +++ b/plugins/modules/netbox_contact_assignment.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2025, Daniel Chiquito (@dchiquito) +# 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: netbox_contact_assignment +short_description: Creates or removes contact assignments from NetBox +description: + - Creates or removes contact assignments from NetBox +notes: + - Tags should be defined as a YAML list + - This should be ran with connection C(local) and hosts C(localhost) +author: + - Daniel Chiquito (@dchiquito) +requirements: + - pynetbox +version_added: "3.1.0" +extends_documentation_fragment: + - netbox.netbox.common +options: + data: + type: dict + description: + - Defines the contact configuration + suboptions: + object_type: + description: + - The type of the object the contact is assigned to + required: true + type: str + choices: + - circuit + - cluster + - cluster_group + - device + - location + - manufacturer + - power_panel + - provider + - rack + - region + - site + - site_group + - tenant + - virtual_machine + object_name: + description: + - The name of the object the contact is assigned to + required: true + type: str + contact: + description: + - The name of the contact to assign to the object + required: true + type: str + role: + description: + - The name of the role the contact has for this object + required: true + type: str + priority: + description: + - The priority of this contact + required: false + type: str + choices: + - primary + - secondary + - tertiary + - inactive + tags: + description: + - Any tags that the contact may need to be associated with + required: false + type: list + elements: raw + required: true +""" + +EXAMPLES = r""" +- name: "Test NetBox module" + connection: local + hosts: localhost + gather_facts: false + tasks: + - name: Assign a contact to a location with only required information + netbox.netbox.netbox_contact_assignment: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + object_type: location + object_name: My Location + contact: John Doe + role: Supervisor Role + state: present + + - name: Delete contact assignment within netbox + netbox.netbox.netbox_contact_assignment: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + object_type: location + object_name: My Location + contact: John Doe + role: Supervisor Role + state: absent + + - name: Create contact with all parameters + netbox.netbox.netbox_contact: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + object_type: location + object_name: My Location + contact: John Doe + role: Supervisor Role + priority: tertiary + tags: + - tagA + - tagB + - tagC + state: present +""" + +RETURN = r""" +contact_assignment: + description: Serialized object as created or already existent within NetBox + returned: on creation + type: dict +msg: + description: Message indicating failure or info about what has been achieved + returned: always + type: str +""" + +from ansible_collections.netbox.netbox.plugins.module_utils.netbox_utils import ( + NetboxAnsibleModule, + NETBOX_ARG_SPEC, +) +from ansible_collections.netbox.netbox.plugins.module_utils.netbox_tenancy import ( + NetboxTenancyModule, + NB_CONTACT_ASSIGNMENTS, + OBJECT_TYPES, +) +from copy import deepcopy + + +def main(): + """ + Main entry point for module execution + """ + argument_spec = deepcopy(NETBOX_ARG_SPEC) + argument_spec.update( + dict( + data=dict( + type="dict", + required=True, + options=dict( + object_type=dict( + required=True, type="str", choices=list(OBJECT_TYPES.keys()) + ), + object_name=dict(required=True, type="str"), + contact=dict(required=True, type="str"), + role=dict(required=True, type="str"), + priority=dict( + required=False, + type="str", + choices=["primary", "secondary", "tertiary", "inactive"], + ), + tags=dict(required=False, type="list", elements="raw"), + ), + ), + ) + ) + + required_if = [ + ("state", "present", ["object_type", "object_name", "contact", "role"]), + ("state", "absent", ["object_type", "object_name", "contact", "role"]), + ] + + module = NetboxAnsibleModule( + argument_spec=argument_spec, supports_check_mode=True, required_if=required_if + ) + + netbox_contact = NetboxTenancyModule(module, NB_CONTACT_ASSIGNMENTS) + netbox_contact.run() + + +if __name__ == "__main__": # pragma: no cover + main() From 7807cac8c81fc9e2c148f080e53db71a34d2332b Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 29 Oct 2025 19:16:13 -0400 Subject: [PATCH 02/13] Add contact assignment tests --- tests/integration/targets/v4.3/tasks/main.yml | 9 + .../v4.3/tasks/netbox_contact_assignment.yml | 503 ++++++++++++++++++ 2 files changed, 512 insertions(+) create mode 100644 tests/integration/targets/v4.3/tasks/netbox_contact_assignment.yml diff --git a/tests/integration/targets/v4.3/tasks/main.yml b/tests/integration/targets/v4.3/tasks/main.yml index 33ce77bcf..a216a8c7b 100644 --- a/tests/integration/targets/v4.3/tasks/main.yml +++ b/tests/integration/targets/v4.3/tasks/main.yml @@ -71,6 +71,15 @@ tags: - netbox_contact +- name: NETBOX_CONTACT_ASSIGNMENT TESTS + ansible.builtin.include_tasks: + file: netbox_contact_assignment.yml + apply: + tags: + - netbox_contact_assignment + tags: + - netbox_contact_assignment + - name: NETBOX_CONTACT_ROLE TESTS ansible.builtin.include_tasks: file: netbox_contact_role.yml diff --git a/tests/integration/targets/v4.3/tasks/netbox_contact_assignment.yml b/tests/integration/targets/v4.3/tasks/netbox_contact_assignment.yml new file mode 100644 index 000000000..dfb9206ef --- /dev/null +++ b/tests/integration/targets/v4.3/tasks/netbox_contact_assignment.yml @@ -0,0 +1,503 @@ +--- +## +## +### NETBOX_CONTACT_ASSIGNMENT +## +## +- name: Setup - Create contact for assignment + netbox.netbox.netbox_contact: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Contact For Assignment + state: present + +- name: Setup - Create contact role for assignment + netbox.netbox.netbox_contact_role: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Role For Assignment + state: present + +- name: 1 - Setup - Create tenant to assign contact to + netbox.netbox.netbox_tenant: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Tenant With Contact + state: present + +- name: 1 - Assign contact to tenant + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: tenant + object_name: Tenant With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_one + +- name: 1 - ASSERT + ansible.builtin.assert: + that: + - test_one is changed + - test_one['diff']['before']['state'] == "absent" + - test_one['diff']['after']['state'] == "present" + - test_one['contact_assignment']['display'] == "Contact For Assignment -> Tenant With Contact" + - test_one['msg'] == "contact_assignment Contact For Assignment -> Tenant With Contact created" + +- name: Test duplicate contact + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: tenant + object_name: Tenant With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_two + +- name: 2 - ASSERT + ansible.builtin.assert: + that: + - not test_two['changed'] + - test_two['msg'] == "contact_assignment Contact For Assignment -> Tenant With Contact already exists" + +- name: 3 - Test update + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: tenant + object_name: Tenant With Contact + contact: Contact For Assignment + role: Role For Assignment + priority: tertiary + tags: + - tagA + - tagB + register: test_three + +- name: 3 - ASSERT + ansible.builtin.assert: + that: + - test_three is changed + - test_three['diff']['before']['priority'] == None + - test_three['diff']['after']['priority'] == "tertiary" + - test_three['diff']['before']['tags'] == [] + - test_three['diff']['after']['tags'] != [] + - test_three['msg'] == "contact_assignment Contact For Assignment -> Tenant With Contact updated" + +- name: 4 - Test delete + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: tenant + object_name: Tenant With Contact + contact: Contact For Assignment + role: Role For Assignment + state: absent + register: test_four + +- name: 4 - ASSERT + ansible.builtin.assert: + that: + - test_four is changed + - test_four['diff']['before']['state'] == "present" + - test_four['diff']['after']['state'] == "absent" + - test_four['msg'] == "contact_assignment Contact For Assignment -> Tenant With Contact deleted" + +- name: 5 - Setup - Create circuit to assign contact to + netbox.netbox.netbox_circuit: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + cid: Circuit With Contact + provider: Test Provider + circuit_type: Test Circuit Type + state: present + +- name: 5 - Assign contact to circuit + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: circuit + object_name: Circuit With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_five + +- name: 5 - ASSERT + ansible.builtin.assert: + that: + - test_five is changed + - test_five['diff']['before']['state'] == "absent" + - test_five['diff']['after']['state'] == "present" + - test_five['contact_assignment']['display'] == "Contact For Assignment -> Circuit With Contact" + - test_five['msg'] == "contact_assignment Contact For Assignment -> Circuit With Contact created" + +- name: 6 - Setup - Create provider to assign contact to + netbox.netbox.netbox_provider: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Provider With Contact + state: present + +- name: 6 - Assign contact to provider + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: provider + object_name: Provider With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_six + +- name: 6 - ASSERT + ansible.builtin.assert: + that: + - test_six is changed + - test_six['diff']['before']['state'] == "absent" + - test_six['diff']['after']['state'] == "present" + - test_six['contact_assignment']['display'] == "Contact For Assignment -> Provider With Contact" + - test_six['msg'] == "contact_assignment Contact For Assignment -> Provider With Contact created" + +- name: 7 - Setup - Create device to assign contact to + netbox.netbox.netbox_device: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Device With Contact + device_type: + id: "1" + device_role: Core Switch + site: Test Site + status: Staged + state: present + +- name: 7 - Assign contact to device + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: device + object_name: Device With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_seven + +- name: 7 - ASSERT + ansible.builtin.assert: + that: + - test_seven is changed + - test_seven['diff']['before']['state'] == "absent" + - test_seven['diff']['after']['state'] == "present" + - test_seven['contact_assignment']['display'] == "Contact For Assignment -> Device With Contact" + - test_seven['msg'] == "contact_assignment Contact For Assignment -> Device With Contact created" + +- name: 8 - Setup - Create location to assign contact to + netbox.netbox.netbox_location: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Location With Contact + site: Test Site + state: present + +- name: 8 - Assign contact to location + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: location + object_name: Location With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_eight + +- name: 8 - ASSERT + ansible.builtin.assert: + that: + - test_eight is changed + - test_eight['diff']['before']['state'] == "absent" + - test_eight['diff']['after']['state'] == "present" + - test_eight['contact_assignment']['display'] == "Contact For Assignment -> Location With Contact" + - test_eight['msg'] == "contact_assignment Contact For Assignment -> Location With Contact created" + +- name: 9 - Setup - Create manufacturer to assign contact to + netbox.netbox.netbox_manufacturer: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Manufacturer With Contact + state: present + +- name: 9 - Assign contact to manufacturer + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: manufacturer + object_name: Manufacturer With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_nine + +- name: 9 - ASSERT + ansible.builtin.assert: + that: + - test_nine is changed + - test_nine['diff']['before']['state'] == "absent" + - test_nine['diff']['after']['state'] == "present" + - test_nine['contact_assignment']['display'] == "Contact For Assignment -> Manufacturer With Contact" + - test_nine['msg'] == "contact_assignment Contact For Assignment -> Manufacturer With Contact created" + +- name: 10 - Setup - Create power panel to assign contact to + netbox.netbox.netbox_power_panel: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Power Panel With Contact + site: Test Site + state: present + +- name: 10 - Assign contact to power panel + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: power_panel + object_name: Power Panel With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_ten + +- name: 10 - ASSERT + ansible.builtin.assert: + that: + - test_ten is changed + - test_ten['diff']['before']['state'] == "absent" + - test_ten['diff']['after']['state'] == "present" + - test_ten['contact_assignment']['display'] == "Contact For Assignment -> Power Panel With Contact" + - test_ten['msg'] == "contact_assignment Contact For Assignment -> Power Panel With Contact created" + +- name: 11 - Setup - Create rack to assign contact to + netbox.netbox.netbox_rack: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Rack With Contact + site: Test Site + location: Test Rack Group + state: present + +- name: 11 - Assign contact to rack + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: rack + object_name: Rack With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_eleven + +- name: 11 - ASSERT + ansible.builtin.assert: + that: + - test_eleven is changed + - test_eleven['diff']['before']['state'] == "absent" + - test_eleven['diff']['after']['state'] == "present" + - test_eleven['contact_assignment']['display'] == "Contact For Assignment -> Rack With Contact" + - test_eleven['msg'] == "contact_assignment Contact For Assignment -> Rack With Contact created" + +- name: 12 - Setup - Create region to assign contact to + netbox.netbox.netbox_region: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Region With Contact + state: present + +- name: 12 - Assign contact to region + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: region + object_name: Region With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_twelve + +- name: 12 - ASSERT + ansible.builtin.assert: + that: + - test_twelve is changed + - test_twelve['diff']['before']['state'] == "absent" + - test_twelve['diff']['after']['state'] == "present" + - test_twelve['contact_assignment']['display'] == "Contact For Assignment -> Region With Contact" + - test_twelve['msg'] == "contact_assignment Contact For Assignment -> Region With Contact created" + +- name: 13 - Setup - Create site to assign contact to + netbox.netbox.netbox_site: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Site With Contact + state: present + +- name: 13 - Assign contact to site + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: site + object_name: Site With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_thirteen + +- name: 13 - ASSERT + ansible.builtin.assert: + that: + - test_thirteen is changed + - test_thirteen['diff']['before']['state'] == "absent" + - test_thirteen['diff']['after']['state'] == "present" + - test_thirteen['contact_assignment']['display'] == "Contact For Assignment -> Site With Contact" + - test_thirteen['msg'] == "contact_assignment Contact For Assignment -> Site With Contact created" + +- name: 14 - Setup - Create site group to assign contact to + netbox.netbox.netbox_site_group: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Site Group With Contact + state: present + +- name: 14 - Assign contact to site group + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: site_group + object_name: Site Group With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_fourteen + +- name: 14 - ASSERT + ansible.builtin.assert: + that: + - test_fourteen is changed + - test_fourteen['diff']['before']['state'] == "absent" + - test_fourteen['diff']['after']['state'] == "present" + - test_fourteen['contact_assignment']['display'] == "Contact For Assignment -> Site Group With Contact" + - test_fourteen['msg'] == "contact_assignment Contact For Assignment -> Site Group With Contact created" + +- name: 15 - Setup - Create cluster to assign contact to + netbox.netbox.netbox_cluster: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Cluster With Contact + cluster_type: Test Cluster Type + state: present + +- name: 15 - Assign contact to cluster + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: cluster + object_name: Cluster With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_fifteen + +- name: 15 - ASSERT + ansible.builtin.assert: + that: + - test_fifteen is changed + - test_fifteen['diff']['before']['state'] == "absent" + - test_fifteen['diff']['after']['state'] == "present" + - test_fifteen['contact_assignment']['display'] == "Contact For Assignment -> Cluster With Contact" + - test_fifteen['msg'] == "contact_assignment Contact For Assignment -> Cluster With Contact created" + +- name: 16 - Setup - Create cluster group to assign contact to + netbox.netbox.netbox_cluster_group: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Cluster Group With Contact + state: present + +- name: 16 - Assign contact to cluster group + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: cluster_group + object_name: Cluster Group With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_sixteen + +- name: 16 - ASSERT + ansible.builtin.assert: + that: + - test_sixteen is changed + - test_sixteen['diff']['before']['state'] == "absent" + - test_sixteen['diff']['after']['state'] == "present" + - test_sixteen['contact_assignment']['display'] == "Contact For Assignment -> Cluster Group With Contact" + - test_sixteen['msg'] == "contact_assignment Contact For Assignment -> Cluster Group With Contact created" + +- name: 17 - Setup - Create virtual machine to assign contact to + netbox.netbox.netbox_virtual_machine: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: Virtual Machine With Contact + cluster: Test Cluster + state: present + +- name: 17 - Assign contact to virtual machine + netbox.netbox.netbox_contact_assignment: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + object_type: virtual_machine + object_name: Virtual Machine With Contact + contact: Contact For Assignment + role: Role For Assignment + state: present + register: test_seventeen + +- name: 17 - ASSERT + ansible.builtin.assert: + that: + - test_seventeen is changed + - test_seventeen['diff']['before']['state'] == "absent" + - test_seventeen['diff']['after']['state'] == "present" + - test_seventeen['contact_assignment']['display'] == "Contact For Assignment -> Virtual Machine With Contact" + - test_seventeen['msg'] == "contact_assignment Contact For Assignment -> Virtual Machine With Contact created" From bdfeddfad862b8a34a53afa445abbc404ca1b84c Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 29 Oct 2025 19:25:32 -0400 Subject: [PATCH 03/13] Add link to contact assignment documentation --- plugins/module_utils/netbox_tenancy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/module_utils/netbox_tenancy.py b/plugins/module_utils/netbox_tenancy.py index 021e9449c..b593d98bd 100644 --- a/plugins/module_utils/netbox_tenancy.py +++ b/plugins/module_utils/netbox_tenancy.py @@ -38,6 +38,7 @@ "tenant": "tenants", "virtual_machine": "virtual_machines", } +# See https://netboxlabs.com/docs/netbox/features/contacts/#contacts-1 OBJECT_TYPES = { "circuit": "circuits.circuit", "cluster": "virtualization.cluster", From 5fd37b8c5a2b33f0fcf647da177e9d5d49c9f578 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 29 Oct 2025 19:29:24 -0400 Subject: [PATCH 04/13] Fix netbox_contact_assignment.py DOCUMENTATION yaml linting --- plugins/modules/netbox_contact_assignment.py | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/modules/netbox_contact_assignment.py b/plugins/modules/netbox_contact_assignment.py index 7297bbc2e..b1896633d 100644 --- a/plugins/modules/netbox_contact_assignment.py +++ b/plugins/modules/netbox_contact_assignment.py @@ -35,20 +35,20 @@ required: true type: str choices: - - circuit - - cluster - - cluster_group - - device - - location - - manufacturer - - power_panel - - provider - - rack - - region - - site - - site_group - - tenant - - virtual_machine + - circuit + - cluster + - cluster_group + - device + - location + - manufacturer + - power_panel + - provider + - rack + - region + - site + - site_group + - tenant + - virtual_machine object_name: description: - The name of the object the contact is assigned to @@ -62,7 +62,7 @@ role: description: - The name of the role the contact has for this object - required: true + required: true type: str priority: description: @@ -70,10 +70,10 @@ required: false type: str choices: - - primary - - secondary - - tertiary - - inactive + - primary + - secondary + - tertiary + - inactive tags: description: - Any tags that the contact may need to be associated with From 81e4bb3e30888967bfdfe9dbfa62358a621fa9f0 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 29 Oct 2025 19:36:23 -0400 Subject: [PATCH 05/13] Remove trailing space from netbox_contact_assignment.yml --- .../targets/v4.3/tasks/netbox_contact_assignment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/v4.3/tasks/netbox_contact_assignment.yml b/tests/integration/targets/v4.3/tasks/netbox_contact_assignment.yml index dfb9206ef..6928b8f83 100644 --- a/tests/integration/targets/v4.3/tasks/netbox_contact_assignment.yml +++ b/tests/integration/targets/v4.3/tasks/netbox_contact_assignment.yml @@ -481,7 +481,7 @@ cluster: Test Cluster state: present -- name: 17 - Assign contact to virtual machine +- name: 17 - Assign contact to virtual machine netbox.netbox.netbox_contact_assignment: netbox_url: http://localhost:32768 netbox_token: "0123456789abcdef0123456789abcdef01234567" From 583f560e77028343612a2590092057f93e843984 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 29 Oct 2025 22:31:49 -0400 Subject: [PATCH 06/13] Fix power panel integration test --- tests/integration/targets/v4.3/tasks/netbox_power_feed.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/v4.3/tasks/netbox_power_feed.yml b/tests/integration/targets/v4.3/tasks/netbox_power_feed.yml index f4ac71e01..233e0fa4c 100644 --- a/tests/integration/targets/v4.3/tasks/netbox_power_feed.yml +++ b/tests/integration/targets/v4.3/tasks/netbox_power_feed.yml @@ -24,7 +24,6 @@ - test_one['diff']['before']['state'] == "absent" - test_one['diff']['after']['state'] == "present" - test_one['power_feed']['name'] == "Power Feed" - - test_one['power_feed']['power_panel'] == 1 - test_one['msg'] == "power_feed Power Feed created" - name: "POWER_FEED 2: Create duplicate" @@ -42,7 +41,7 @@ that: - not test_two['changed'] - test_two['power_feed']['name'] == "Power Feed" - - test_two['power_feed']['power_panel'] == 1 + - test_two['power_feed']['power_panel'] == test_one['power_feed']['power_panel'] - test_two['msg'] == "power_feed Power Feed already exists" - name: "POWER_FEED 3: Update power_feed with other fields" @@ -76,7 +75,7 @@ - test_three['diff']['after']['max_utilization'] == 25 - test_three['diff']['after']['comments'] == "totally normal power feed" - test_three['power_feed']['name'] == "Power Feed" - - test_three['power_feed']['power_panel'] == 1 + - test_three['power_feed']['power_panel'] == test_one['power_feed']['power_panel'] - test_three['power_feed']['status'] == "offline" - test_three['power_feed']['type'] == "redundant" - test_three['power_feed']['supply'] == "dc" @@ -104,7 +103,7 @@ - test_four['diff']['before']['state'] == "absent" - test_four['diff']['after']['state'] == "present" - test_four['power_feed']['name'] == "Power Feed 2" - - test_four['power_feed']['power_panel'] == 1 + - test_four['power_feed']['power_panel'] == test_one['power_feed']['power_panel'] - test_four['msg'] == "power_feed Power Feed 2 created" - name: "POWER_FEED 5: Delete Power Feed" From 66f893aed7a78e2a98dbaec6bb07e7e8f2885ace Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 29 Oct 2025 22:47:20 -0400 Subject: [PATCH 07/13] Fix power port integration test --- tests/integration/targets/v4.3/tasks/netbox_power_port.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/v4.3/tasks/netbox_power_port.yml b/tests/integration/targets/v4.3/tasks/netbox_power_port.yml index 311820ccd..7b0a7d50d 100644 --- a/tests/integration/targets/v4.3/tasks/netbox_power_port.yml +++ b/tests/integration/targets/v4.3/tasks/netbox_power_port.yml @@ -35,7 +35,6 @@ - test_one['diff']['before']['state'] == "absent" - test_one['diff']['after']['state'] == "present" - test_one['power_port']['name'] == "Power Port" - - test_one['power_port']['device'] == 10 - test_one['msg'] == "power_port Power Port created" - name: "POWER_PORT 2: Create duplicate" @@ -53,7 +52,7 @@ that: - not test_two['changed'] - test_two['power_port']['name'] == "Power Port" - - test_two['power_port']['device'] == 10 + - test_two['power_port']['device'] == test_one['power_port']['device'] - test_two['msg'] == "power_port Power Port already exists" - name: "POWER_FEED 3: Update power_port with other fields" @@ -79,7 +78,7 @@ - test_three['diff']['after']['maximum_draw'] == 20 - test_three['diff']['after']['description'] == "test description" - test_three['power_port']['name'] == "Power Port" - - test_three['power_port']['device'] == 10 + - test_three['power_port']['device'] == test_one['power_port']['device'] - test_three['power_port']['type'] == "ita-e" - test_three['power_port']['allocated_draw'] == 10 - test_three['power_port']['maximum_draw'] == 20 @@ -103,7 +102,7 @@ - test_four['diff']['before']['state'] == "absent" - test_four['diff']['after']['state'] == "present" - test_four['power_port']['name'] == "Power Port 2" - - test_four['power_port']['device'] == 10 + - test_four['power_port']['device'] == test_one['power_port']['device'] - test_four['msg'] == "power_port Power Port 2 created" - name: "POWER_PORT 5: Delete Power Port" From 7431aa23835e5e6ead2cd44d4ef1d22ec7a230b6 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 29 Oct 2025 23:10:45 -0400 Subject: [PATCH 08/13] Fix power outlet integration test --- .../integration/targets/v4.3/tasks/netbox_power_outlet.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/integration/targets/v4.3/tasks/netbox_power_outlet.yml b/tests/integration/targets/v4.3/tasks/netbox_power_outlet.yml index 84f9d3255..e38a8650a 100644 --- a/tests/integration/targets/v4.3/tasks/netbox_power_outlet.yml +++ b/tests/integration/targets/v4.3/tasks/netbox_power_outlet.yml @@ -24,7 +24,6 @@ - test_one['diff']['before']['state'] == "absent" - test_one['diff']['after']['state'] == "present" - test_one['power_outlet']['name'] == "Power Outlet" - - test_one['power_outlet']['device'] == 10 - test_one['msg'] == "power_outlet Power Outlet created" - name: "POWER_OUTLET 2: Create duplicate" @@ -42,7 +41,7 @@ that: - not test_two['changed'] - test_two['power_outlet']['name'] == "Power Outlet" - - test_two['power_outlet']['device'] == 10 + - test_two['power_outlet']['device'] == test_one['power_outlet']['device'] - test_two['msg'] == "power_outlet Power Outlet already exists" - name: "POWER_OUTLET 3: Update power_outlet with other fields" @@ -68,7 +67,7 @@ - test_three['diff']['after']['feed_leg'] == "B" - test_three['diff']['after']['description'] == "test description" - test_three['power_outlet']['name'] == "Power Outlet" - - test_three['power_outlet']['device'] == 10 + - test_three['power_outlet']['device'] == test_one['power_outlet']['device'] - test_three['power_outlet']['type'] == "ita-e" - test_three['power_outlet']['power_port'] == 1 - test_three['power_outlet']['feed_leg'] == "B" @@ -92,7 +91,7 @@ - test_four['diff']['before']['state'] == "absent" - test_four['diff']['after']['state'] == "present" - test_four['power_outlet']['name'] == "Power Outlet 2" - - test_four['power_outlet']['device'] == 10 + - test_four['power_outlet']['device'] == test_one['power_outlet']['device'] - test_four['msg'] == "power_outlet Power Outlet 2 created" - name: "POWER_OUTLET 5: Delete Power Outlet" From a7066c90f5dac1e6081f6bba3ffd8236a1beda93 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Wed, 29 Oct 2025 23:46:01 -0400 Subject: [PATCH 09/13] Fix virtual chassis integration test --- .../integration/targets/v4.3/tasks/netbox_virtual_chassis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml b/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml index 395be8395..b6e8c460c 100644 --- a/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml +++ b/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml @@ -54,7 +54,7 @@ ansible.builtin.assert: that: - not test_two['changed'] - - test_two['virtual_chassis']['master'] == 11 + - test_two['virtual_chassis']['master'] == test_one['virtual_chassis']['master'] - test_two['virtual_chassis']['name'] == "First VC" - test_two['msg'] == "virtual_chassis First VC already exists" @@ -74,7 +74,7 @@ that: - test_three is changed - test_three['diff']['after']['domain'] == "Domain Text" - - test_three['virtual_chassis']['master'] == 11 + - test_three['virtual_chassis']['master'] == test_one['virtual_chassis']['master'] - test_three['virtual_chassis']['domain'] == "Domain Text" - test_three['virtual_chassis']['name'] == "First VC" - test_three['msg'] == "virtual_chassis First VC updated" From 9008d60933a3ee57bda4080c45016c2b3f6418b1 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Thu, 30 Oct 2025 00:01:50 -0400 Subject: [PATCH 10/13] Finish fixing virtual chassis integration test --- tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml b/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml index b6e8c460c..185992769 100644 --- a/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml +++ b/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml @@ -36,7 +36,6 @@ - test_one is changed - test_one['diff']['before']['state'] == "absent" - test_one['diff']['after']['state'] == "present" - - test_one['virtual_chassis']['master'] == 11 - test_one['virtual_chassis']['name'] == "First VC" - test_one['msg'] == "virtual_chassis First VC created" From affbb4280e9cc193a9b7fbfeeba94553876205c8 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Thu, 30 Oct 2025 11:48:43 -0400 Subject: [PATCH 11/13] Finish fixing virtual chassis integration test again --- tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml b/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml index 185992769..6eae6de19 100644 --- a/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml +++ b/tests/integration/targets/v4.3/tasks/netbox_virtual_chassis.yml @@ -107,7 +107,7 @@ - test_four is changed - test_four['diff']['before']['state'] == "absent" - test_four['diff']['after']['state'] == "present" - - test_four['virtual_chassis']['master'] == 12 + - test_four['virtual_chassis']['master'] != test_one['virtual_chassis']['master'] - test_four['virtual_chassis']['name'] == "Second VC" - test_four['msg'] == "virtual_chassis Second VC created" From 099f603b5c9ce7b4e51d255cc864fdb5adb1bf19 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Thu, 30 Oct 2025 12:16:25 -0400 Subject: [PATCH 12/13] Fix lookup integration test --- tests/integration/targets/v4.3/tasks/netbox_lookup.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/targets/v4.3/tasks/netbox_lookup.yml b/tests/integration/targets/v4.3/tasks/netbox_lookup.yml index 046f327eb..3d5ef9efa 100644 --- a/tests/integration/targets/v4.3/tasks/netbox_lookup.yml +++ b/tests/integration/targets/v4.3/tasks/netbox_lookup.yml @@ -4,9 +4,9 @@ ### NETBOX_LOOKUP ## ## -- name: "NETBOX_LOOKUP 1: Lookup returns exactly two sites" +- name: "NETBOX_LOOKUP 1: Lookup returns exactly four sites" ansible.builtin.assert: - that: query_result == "3" + that: query_result == "4" vars: query_result: "{{ query('netbox.netbox.nb_lookup', 'sites', api_endpoint='http://localhost:32768', token='0123456789abcdef0123456789abcdef01234567') | count }}" From 368a8cc73b72071007ffe3291d33d9c9c4366188 Mon Sep 17 00:00:00 2001 From: Daniel Chiquito Date: Fri, 31 Oct 2025 15:05:21 -0400 Subject: [PATCH 13/13] Add changelog fragment --- changelogs/fragments/contacts.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/contacts.yml diff --git a/changelogs/fragments/contacts.yml b/changelogs/fragments/contacts.yml new file mode 100644 index 000000000..5ca638a62 --- /dev/null +++ b/changelogs/fragments/contacts.yml @@ -0,0 +1,2 @@ +minor_changes: + - netbox_contact_assignment - New module `#1480 `