Skip to content

Commit ed70e69

Browse files
rodvandk-304
authored andcommitted
Add module netbox_mac_address (netbox-community#1371)
1 parent 3306e62 commit ed70e69

File tree

11 files changed

+315
-10
lines changed

11 files changed

+315
-10
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
minor_changes:
2+
- netbox_device_interface - Add primary_mac_address option for NetBox 4.2+
3+
- netbox_vm_interface - Add primary_mac_address option for NetBox 4.2+

meta/runtime.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ action_groups:
5151
- netbox_l2vpn
5252
- netbox_l2vpn_termination
5353
- netbox_location
54+
- netbox_mac_address
5455
- netbox_manufacturer
5556
- netbox_module
5657
- netbox_module_bay

plugins/module_utils/netbox_dcim.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
NB_SITES = "sites"
5252
NB_SITE_GROUPS = "site_groups"
5353
NB_VIRTUAL_CHASSIS = "virtual_chassis"
54+
NB_MAC_ADDRESSES = "mac_addresses"
5455

5556
try:
5657
from packaging.version import Version
@@ -141,6 +142,8 @@ def run(self):
141142
name = self.module.params["data"]["master"]
142143
elif data.get("slug"):
143144
name = data["slug"]
145+
elif data.get("mac_address"):
146+
name = data["mac_address"]
144147
elif endpoint_name == "cable":
145148
if self.module.params["data"]["termination_a"].get("name"):
146149
termination_a_name = self.module.params["data"]["termination_a"]["name"]

plugins/module_utils/netbox_utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"sites": {},
7979
"site_groups": {},
8080
"virtual_chassis": {},
81+
"mac_addresses": {},
8182
},
8283
extras={
8384
"config_contexts": {},
@@ -226,6 +227,7 @@
226227
webhook="name",
227228
wireless_lan="ssid",
228229
wireless_lan_group="slug",
230+
mac_address="mac_address",
229231
)
230232

231233
# Specifies keys within data that need to be converted to ID and the endpoint to be used when queried
@@ -275,6 +277,7 @@
275277
"ipsec_profile": "ipsec_profiles",
276278
"location": "locations",
277279
"lag": "interfaces",
280+
"primary_mac_address": "mac_addresses",
278281
"manufacturer": "manufacturers",
279282
"master": "devices",
280283
"module": "modules",
@@ -434,6 +437,7 @@
434437
"wireless_lans": "wireless_lan",
435438
"wireless_lan_groups": "wireless_lan_group",
436439
"wireless_links": "wireless_link",
440+
"mac_addresses": "mac_address",
437441
}
438442

439443
ALLOWED_QUERY_PARAMS = {
@@ -516,6 +520,7 @@
516520
),
517521
"lag": set(["name"]),
518522
"location": set(["name", "slug", "site"]),
523+
"mac_address": set(["mac_address"]),
519524
"module": set(["device", "module_bay", "module_type"]),
520525
"module_bay": set(["device", "name"]),
521526
"module_type": set(["model"]),
@@ -1414,7 +1419,7 @@ def _normalize_data(self, data):
14141419

14151420
# We need to assign the correct type for the assigned object so the user doesn't have to worry about this.
14161421
# We determine it by whether or not they pass in a device or virtual_machine
1417-
if data.get("assigned_object"):
1422+
if data.get("assigned_object") and isinstance(data["assigned_object"], dict):
14181423
if data["assigned_object"].get("device"):
14191424
data["assigned_object_type"] = "dcim.interface"
14201425
if data["assigned_object"].get("virtual_machine"):

plugins/modules/netbox_device_interface.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@
8686
- The MAC address of the interface
8787
required: false
8888
type: str
89+
primary_mac_address:
90+
description:
91+
- The primary MAC address of the interface (NetBox 4.2 and later)
92+
required: false
93+
type: str
8994
wwn:
9095
description:
9196
- The WWN of the interface
@@ -335,6 +340,7 @@ def main():
335340
bridge=dict(required=False, type="raw"),
336341
mtu=dict(required=False, type="int"),
337342
mac_address=dict(required=False, type="str"),
343+
primary_mac_address=dict(required=False, type="str"),
338344
wwn=dict(required=False, type="str"),
339345
mgmt_only=dict(required=False, type="bool"),
340346
poe_type=dict(required=False, type="raw"),

plugins/modules/netbox_mac_address.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
# Copyright: (c) 2025, Martin Rødvand (@rodvand) <[email protected]>
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_mac_address
13+
short_description: Create, update or delete MAC addresses within NetBox
14+
description:
15+
- Creates, updates or removes MAC addresses 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+
- Martin Rødvand (@rodvand)
21+
requirements:
22+
- pynetbox
23+
version_added: "3.21.0"
24+
extends_documentation_fragment:
25+
- netbox.netbox.common
26+
options:
27+
data:
28+
type: dict
29+
description:
30+
- Defines the MAC address configuration
31+
suboptions:
32+
mac_address:
33+
description:
34+
- The MAC address
35+
required: true
36+
type: str
37+
assigned_object:
38+
description:
39+
- The object to assign this MAC address to
40+
required: false
41+
type: dict
42+
description:
43+
description:
44+
- Description of the MAC address
45+
required: false
46+
type: str
47+
comments:
48+
description:
49+
- Comments for the MAC address
50+
required: false
51+
type: str
52+
tags:
53+
description:
54+
- Any tags that the MAC address may need to be associated with
55+
required: false
56+
type: list
57+
elements: raw
58+
custom_fields:
59+
description:
60+
- Must exist in NetBox and in key/value format
61+
required: false
62+
type: dict
63+
required: true
64+
"""
65+
66+
EXAMPLES = r"""
67+
- name: "Test NetBox MAC address module"
68+
connection: local
69+
hosts: localhost
70+
gather_facts: false
71+
72+
tasks:
73+
- name: Create MAC address within NetBox with only required information
74+
netbox.netbox.netbox_mac_address:
75+
netbox_url: http://netbox.local
76+
netbox_token: thisIsMyToken
77+
data:
78+
mac_address: "00:11:22:33:44:55"
79+
state: present
80+
81+
- name: Create MAC address with interface assignment
82+
netbox.netbox.netbox_mac_address:
83+
netbox_url: http://netbox.local
84+
netbox_token: thisIsMyToken
85+
data:
86+
mac_address: "AA:BB:CC:DD:EE:FF"
87+
assigned_object:
88+
device: Test Nexus One
89+
name: Ethernet1/1
90+
description: "MAC address for eth1/1"
91+
tags:
92+
- Network
93+
state: present
94+
95+
- name: Delete MAC address within netbox
96+
netbox.netbox.netbox_mac_address:
97+
netbox_url: http://netbox.local
98+
netbox_token: thisIsMyToken
99+
data:
100+
mac_address: "00:11:22:33:44:55"
101+
state: absent
102+
"""
103+
104+
RETURN = r"""
105+
mac_address:
106+
description: Serialized object as created or already existent within NetBox
107+
returned: success (when I(state=present))
108+
type: dict
109+
msg:
110+
description: Message indicating failure or info about what has been achieved
111+
returned: always
112+
type: str
113+
"""
114+
115+
from ansible_collections.netbox.netbox.plugins.module_utils.netbox_utils import (
116+
NetboxAnsibleModule,
117+
NETBOX_ARG_SPEC,
118+
)
119+
from ansible_collections.netbox.netbox.plugins.module_utils.netbox_dcim import (
120+
NetboxDcimModule,
121+
NB_MAC_ADDRESSES,
122+
)
123+
from copy import deepcopy
124+
125+
126+
def main():
127+
"""
128+
Main entry point for module execution
129+
"""
130+
argument_spec = deepcopy(NETBOX_ARG_SPEC)
131+
argument_spec.update(
132+
dict(
133+
data=dict(
134+
type="dict",
135+
required=True,
136+
options=dict(
137+
mac_address=dict(required=True, type="str"),
138+
assigned_object=dict(required=False, type="dict"),
139+
description=dict(required=False, type="str"),
140+
comments=dict(required=False, type="str"),
141+
tags=dict(required=False, type="list", elements="raw"),
142+
custom_fields=dict(required=False, type="dict"),
143+
),
144+
),
145+
)
146+
)
147+
148+
required_if = [
149+
("state", "present", ["mac_address"]),
150+
("state", "absent", ["mac_address"]),
151+
]
152+
153+
module = NetboxAnsibleModule(
154+
argument_spec=argument_spec,
155+
supports_check_mode=True,
156+
required_if=required_if,
157+
)
158+
159+
netbox_mac_address = NetboxDcimModule(module, NB_MAC_ADDRESSES)
160+
netbox_mac_address.run()
161+
162+
163+
if __name__ == "__main__": # pragma: no cover
164+
main()

plugins/modules/netbox_vm_interface.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
- The MAC address of the interface
5454
required: false
5555
type: str
56+
primary_mac_address:
57+
description:
58+
- The primary MAC address of the interface (NetBox 4.2 and later)
59+
required: false
60+
type: str
5661
description:
5762
description:
5863
- The description of the interface
@@ -209,6 +214,7 @@ def main():
209214
enabled=dict(required=False, type="bool"),
210215
mtu=dict(required=False, type="int"),
211216
mac_address=dict(required=False, type="str"),
217+
primary_mac_address=dict(required=False, type="str"),
212218
description=dict(required=False, type="str"),
213219
mode=dict(required=False, type="raw"),
214220
vm_bridge=dict(required=False, type="raw"),

tests/integration/netbox-deploy.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,24 @@ def make_netbox_calls(endpoint, payload):
372372
]
373373
created_interfaces = make_netbox_calls(nb.dcim.interfaces, dev_interfaces)
374374

375+
# Create MAC addresses
376+
if nb_version >= version.parse("4.2"):
377+
test100_gi1 = nb.dcim.interfaces.get(name="GigabitEthernet1", device_id=1)
378+
test100_gi2 = nb.dcim.interfaces.get(name="GigabitEthernet2", device_id=1)
379+
mac_addresses = [
380+
{
381+
"mac_address": "AA:BB:CC:DD:EE:FF",
382+
"assigned_object_id": test100_gi1.id,
383+
"assigned_object_type": "dcim.interface",
384+
},
385+
{
386+
"mac_address": "AA:AB:CC:DD:EE:FF",
387+
"assigned_object_id": test100_gi2.id,
388+
"assigned_object_type": "dcim.interface",
389+
},
390+
]
391+
created_mac_addresses = make_netbox_calls(nb.dcim.mac_addresses, mac_addresses)
392+
375393
# Wireless Interfaces
376394
if nb_version >= version.parse("3.1"):
377395
wlink_interfaces = [

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -332,15 +332,6 @@
332332
tags:
333333
- netbox_circuit
334334

335-
- name: NETBOX_CIRCUIT_TERMINATION TESTS
336-
ansible.builtin.include_tasks:
337-
file: netbox_circuit_termination.yml
338-
apply:
339-
tags:
340-
- netbox_circuit_termination
341-
tags:
342-
- netbox_circuit_termination
343-
344335
- name: NETBOX_REAR_PORT TESTS
345336
ansible.builtin.include_tasks:
346337
file: netbox_rear_port.yml
@@ -732,3 +723,21 @@
732723
- netbox_tunnel_group
733724
tags:
734725
- netbox_tunnel_group
726+
727+
- name: NETBOX_MAC_ADDRESS TESTS
728+
ansible.builtin.include_tasks:
729+
file: netbox_mac_address.yml
730+
apply:
731+
tags:
732+
- netbox_mac_address
733+
tags:
734+
- netbox_mac_address
735+
736+
- name: NETBOX_CIRCUIT_TERMINATION TESTS
737+
ansible.builtin.include_tasks:
738+
file: netbox_circuit_termination.yml
739+
apply:
740+
tags:
741+
- netbox_circuit_termination
742+
tags:
743+
- netbox_circuit_termination

tests/integration/targets/v4.2/tasks/netbox_device_interface.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,23 @@
310310
- test_twelve['interface']['name'] == "GigabitEthernet5"
311311
- test_twelve['interface']['device'] == 1
312312
- test_twelve['interface']['mark_connected'] == true
313+
314+
- name: "13 - Update interface primary MAC address"
315+
netbox.netbox.netbox_device_interface:
316+
netbox_url: http://localhost:32768
317+
netbox_token: "0123456789abcdef0123456789abcdef01234567"
318+
data:
319+
device: test100
320+
name: GigabitEthernet2
321+
primary_mac_address: "AA:AB:CC:DD:EE:FF"
322+
state: present
323+
register: test_thirteen
324+
325+
- name: "13 - ASSERT"
326+
ansible.builtin.assert:
327+
that:
328+
- test_thirteen is changed
329+
- test_thirteen['msg'] == "interface GigabitEthernet2 updated"
330+
- test_thirteen['interface']['name'] == "GigabitEthernet2"
331+
- test_thirteen['interface']['device'] == 1
332+
- test_thirteen['interface']['primary_mac_address'] == 2

0 commit comments

Comments
 (0)