Skip to content

Commit 2e25e1f

Browse files
authored
Merge pull request #1480 from dchiquito/contacts
Add module `netbox_contact_assignment`
2 parents 1f43926 + 368a8cc commit 2e25e1f

File tree

11 files changed

+813
-18
lines changed

11 files changed

+813
-18
lines changed

changelogs/fragments/contacts.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
minor_changes:
2+
- netbox_contact_assignment - New module `#1480 <https://github.com/netbox-community/ansible_modules/pull/1480>`

plugins/module_utils/netbox_tenancy.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,74 @@
1515
NB_TENANTS = "tenants"
1616
NB_TENANT_GROUPS = "tenant_groups"
1717
NB_CONTACTS = "contacts"
18+
NB_CONTACT_ASSIGNMENTS = "contact_assignments"
1819
NB_CONTACT_GROUPS = "contact_groups"
1920
NB_CONTACT_ROLES = "contact_roles"
2021

2122

23+
OBJECT_ENDPOINTS = {
24+
"circuit": "circuits",
25+
"cluster": "clusters",
26+
"cluster_group": "cluster_groups",
27+
"contact": "contacts",
28+
"contact_role": "contact_roles",
29+
"device": "devices",
30+
"location": "locations",
31+
"manufacturer": "manufacturers",
32+
"power_panel": "power_panels",
33+
"provider": "providers",
34+
"rack": "racks",
35+
"region": "regions",
36+
"site": "sites",
37+
"site_group": "site_groups",
38+
"tenant": "tenants",
39+
"virtual_machine": "virtual_machines",
40+
}
41+
# See https://netboxlabs.com/docs/netbox/features/contacts/#contacts-1
42+
OBJECT_TYPES = {
43+
"circuit": "circuits.circuit",
44+
"cluster": "virtualization.cluster",
45+
"cluster_group": "virtualization.clustergroup",
46+
"device": "dcim.device",
47+
"location": "dcim.location",
48+
"manufacturer": "dcim.manufacturer",
49+
"power_panel": "dcim.powerpanel",
50+
"provider": "circuits.provider",
51+
"rack": "dcim.rack",
52+
"region": "dcim.region",
53+
"site": "dcim.site",
54+
"site_group": "dcim.sitegroup",
55+
"tenant": "tenancy.tenant",
56+
"virtual_machine": "virtualization.virtualmachine",
57+
}
58+
OBJECT_NAME_FIELD = {
59+
"circuit": "cid",
60+
# If unspecified, the default is "name"
61+
}
62+
63+
2264
class NetboxTenancyModule(NetboxModule):
2365
def __init__(self, module, endpoint):
2466
super().__init__(module, endpoint)
2567

68+
def get_object_by_name(self, object_type: str, object_name: str):
69+
endpoint = OBJECT_ENDPOINTS[object_type]
70+
app = self._find_app(endpoint)
71+
nb_app = getattr(self.nb, app)
72+
nb_endpoint = getattr(nb_app, endpoint)
73+
74+
name_field = OBJECT_NAME_FIELD.get(object_type)
75+
if name_field is None:
76+
name_field = "name"
77+
78+
query_params = {name_field: object_name}
79+
result = self._nb_endpoint_get(nb_endpoint, query_params, object_name)
80+
if not result:
81+
self._handle_errors(
82+
msg="Could not resolve id of %s: %s" % (object_type, object_name)
83+
)
84+
return result
85+
2686
def run(self):
2787
"""
2888
This function should have all necessary code for endpoints within the application
@@ -31,7 +91,9 @@ def run(self):
3191
- tenants
3292
- tenant groups
3393
- contacts
94+
- contact assignments
3495
- contact groups
96+
- contact roles
3597
"""
3698
# Used to dynamically set key when returning results
3799
endpoint_name = ENDPOINT_NAME_MAPPING[self.endpoint]
@@ -45,6 +107,23 @@ def run(self):
45107

46108
data = self.data
47109

110+
# 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.
111+
# 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.
112+
if self.endpoint == "contact_assignments":
113+
# Not an identifier, just to populate the message field
114+
name = f"{data['contact']} -> {data['object_name']}"
115+
116+
object_type = data["object_type"]
117+
obj = self.get_object_by_name(object_type, data["object_name"])
118+
contact = self.get_object_by_name("contact", data["contact"])
119+
role = self.get_object_by_name("contact_role", data["role"])
120+
121+
data["object_type"] = OBJECT_TYPES[object_type]
122+
data["object_id"] = obj.id
123+
del data["object_name"] # object_id replaces object_name
124+
data["contact"] = contact.id
125+
data["role"] = role.id
126+
48127
# Used for msg output
49128
if data.get("name"):
50129
name = data["name"]
@@ -58,6 +137,12 @@ def run(self):
58137
object_query_params = self._build_query_params(
59138
endpoint_name, data, user_query_params
60139
)
140+
141+
# For some reason, when creating a new contact assignment, role must be an ID
142+
# But when querying contact assignments, the role must be a slug
143+
if self.endpoint == "contact_assignments":
144+
object_query_params["role"] = role.slug
145+
61146
self.nb_object = self._nb_endpoint_get(nb_endpoint, object_query_params, name)
62147

63148
if self.state == "present":

plugins/module_utils/netbox_utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
"tenants": {},
118118
"tenant_groups": {},
119119
"contacts": {},
120+
"contact_assignments": {},
120121
"contact_groups": {},
121122
"contact_roles": {},
122123
},
@@ -369,6 +370,7 @@
369370
"console_server_ports": "console_server_port",
370371
"console_server_port_templates": "console_server_port_template",
371372
"contacts": "contact",
373+
"contact_assignments": "contact_assignment",
372374
"contact_groups": "contact_group",
373375
"contact_roles": "contact_role",
374376
"custom_fields": "custom_field",
@@ -480,6 +482,7 @@
480482
"console_server_port": set(["name", "device"]),
481483
"console_server_port_template": set(["name", "device_type"]),
482484
"contact": set(["name", "group"]),
485+
"contact_assignment": set(["object_type", "object_id", "contact", "role"]),
483486
"contact_group": set(["name"]),
484487
"contact_role": set(["name"]),
485488
"custom_field": set(["name"]),
@@ -618,6 +621,7 @@
618621
"console_port_templates": set(["type"]),
619622
"console_server_ports": set(["type"]),
620623
"console_server_port_templates": set(["type"]),
624+
"contact_assignments": set(["priority"]),
621625
"devices": set(["status", "face"]),
622626
"device_types": set(["subdevice_role"]),
623627
"front_ports": set(["type"]),
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
# Copyright: (c) 2025, Daniel Chiquito (@dchiquito)
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
from __future__ import absolute_import, division, print_function
7+
8+
__metaclass__ = type
9+
10+
DOCUMENTATION = r"""
11+
---
12+
module: netbox_contact_assignment
13+
short_description: Creates or removes contact assignments from NetBox
14+
description:
15+
- Creates or removes contact assignments from NetBox
16+
notes:
17+
- Tags should be defined as a YAML list
18+
- This should be ran with connection C(local) and hosts C(localhost)
19+
author:
20+
- Daniel Chiquito (@dchiquito)
21+
requirements:
22+
- pynetbox
23+
version_added: "3.1.0"
24+
extends_documentation_fragment:
25+
- netbox.netbox.common
26+
options:
27+
data:
28+
type: dict
29+
description:
30+
- Defines the contact configuration
31+
suboptions:
32+
object_type:
33+
description:
34+
- The type of the object the contact is assigned to
35+
required: true
36+
type: str
37+
choices:
38+
- circuit
39+
- cluster
40+
- cluster_group
41+
- device
42+
- location
43+
- manufacturer
44+
- power_panel
45+
- provider
46+
- rack
47+
- region
48+
- site
49+
- site_group
50+
- tenant
51+
- virtual_machine
52+
object_name:
53+
description:
54+
- The name of the object the contact is assigned to
55+
required: true
56+
type: str
57+
contact:
58+
description:
59+
- The name of the contact to assign to the object
60+
required: true
61+
type: str
62+
role:
63+
description:
64+
- The name of the role the contact has for this object
65+
required: true
66+
type: str
67+
priority:
68+
description:
69+
- The priority of this contact
70+
required: false
71+
type: str
72+
choices:
73+
- primary
74+
- secondary
75+
- tertiary
76+
- inactive
77+
tags:
78+
description:
79+
- Any tags that the contact may need to be associated with
80+
required: false
81+
type: list
82+
elements: raw
83+
required: true
84+
"""
85+
86+
EXAMPLES = r"""
87+
- name: "Test NetBox module"
88+
connection: local
89+
hosts: localhost
90+
gather_facts: false
91+
tasks:
92+
- name: Assign a contact to a location with only required information
93+
netbox.netbox.netbox_contact_assignment:
94+
netbox_url: http://netbox.local
95+
netbox_token: thisIsMyToken
96+
data:
97+
object_type: location
98+
object_name: My Location
99+
contact: John Doe
100+
role: Supervisor Role
101+
state: present
102+
103+
- name: Delete contact assignment within netbox
104+
netbox.netbox.netbox_contact_assignment:
105+
netbox_url: http://netbox.local
106+
netbox_token: thisIsMyToken
107+
data:
108+
object_type: location
109+
object_name: My Location
110+
contact: John Doe
111+
role: Supervisor Role
112+
state: absent
113+
114+
- name: Create contact with all parameters
115+
netbox.netbox.netbox_contact:
116+
netbox_url: http://netbox.local
117+
netbox_token: thisIsMyToken
118+
data:
119+
object_type: location
120+
object_name: My Location
121+
contact: John Doe
122+
role: Supervisor Role
123+
priority: tertiary
124+
tags:
125+
- tagA
126+
- tagB
127+
- tagC
128+
state: present
129+
"""
130+
131+
RETURN = r"""
132+
contact_assignment:
133+
description: Serialized object as created or already existent within NetBox
134+
returned: on creation
135+
type: dict
136+
msg:
137+
description: Message indicating failure or info about what has been achieved
138+
returned: always
139+
type: str
140+
"""
141+
142+
from ansible_collections.netbox.netbox.plugins.module_utils.netbox_utils import (
143+
NetboxAnsibleModule,
144+
NETBOX_ARG_SPEC,
145+
)
146+
from ansible_collections.netbox.netbox.plugins.module_utils.netbox_tenancy import (
147+
NetboxTenancyModule,
148+
NB_CONTACT_ASSIGNMENTS,
149+
OBJECT_TYPES,
150+
)
151+
from copy import deepcopy
152+
153+
154+
def main():
155+
"""
156+
Main entry point for module execution
157+
"""
158+
argument_spec = deepcopy(NETBOX_ARG_SPEC)
159+
argument_spec.update(
160+
dict(
161+
data=dict(
162+
type="dict",
163+
required=True,
164+
options=dict(
165+
object_type=dict(
166+
required=True, type="str", choices=list(OBJECT_TYPES.keys())
167+
),
168+
object_name=dict(required=True, type="str"),
169+
contact=dict(required=True, type="str"),
170+
role=dict(required=True, type="str"),
171+
priority=dict(
172+
required=False,
173+
type="str",
174+
choices=["primary", "secondary", "tertiary", "inactive"],
175+
),
176+
tags=dict(required=False, type="list", elements="raw"),
177+
),
178+
),
179+
)
180+
)
181+
182+
required_if = [
183+
("state", "present", ["object_type", "object_name", "contact", "role"]),
184+
("state", "absent", ["object_type", "object_name", "contact", "role"]),
185+
]
186+
187+
module = NetboxAnsibleModule(
188+
argument_spec=argument_spec, supports_check_mode=True, required_if=required_if
189+
)
190+
191+
netbox_contact = NetboxTenancyModule(module, NB_CONTACT_ASSIGNMENTS)
192+
netbox_contact.run()
193+
194+
195+
if __name__ == "__main__": # pragma: no cover
196+
main()

tests/integration/targets/v4.3/tasks/main.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@
7171
tags:
7272
- netbox_contact
7373

74+
- name: NETBOX_CONTACT_ASSIGNMENT TESTS
75+
ansible.builtin.include_tasks:
76+
file: netbox_contact_assignment.yml
77+
apply:
78+
tags:
79+
- netbox_contact_assignment
80+
tags:
81+
- netbox_contact_assignment
82+
7483
- name: NETBOX_CONTACT_ROLE TESTS
7584
ansible.builtin.include_tasks:
7685
file: netbox_contact_role.yml

0 commit comments

Comments
 (0)