diff --git a/changelogs/fragments/netbox_mac_address.yml b/changelogs/fragments/netbox_mac_address.yml new file mode 100644 index 000000000..86740c3d0 --- /dev/null +++ b/changelogs/fragments/netbox_mac_address.yml @@ -0,0 +1,3 @@ +minor_changes: + - netbox_device_interface - Add primary_mac_address option for NetBox 4.2+ + - netbox_vm_interface - Add primary_mac_address option for NetBox 4.2+ diff --git a/meta/runtime.yml b/meta/runtime.yml index a20a9c404..863ab2802 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -51,6 +51,7 @@ action_groups: - netbox_l2vpn - netbox_l2vpn_termination - netbox_location + - netbox_mac_address - netbox_manufacturer - netbox_module - netbox_module_bay diff --git a/plugins/module_utils/netbox_dcim.py b/plugins/module_utils/netbox_dcim.py index d3ede9c73..ccf1b0fc1 100644 --- a/plugins/module_utils/netbox_dcim.py +++ b/plugins/module_utils/netbox_dcim.py @@ -51,6 +51,7 @@ NB_SITES = "sites" NB_SITE_GROUPS = "site_groups" NB_VIRTUAL_CHASSIS = "virtual_chassis" +NB_MAC_ADDRESSES = "mac_addresses" try: from packaging.version import Version @@ -141,6 +142,8 @@ def run(self): name = self.module.params["data"]["master"] elif data.get("slug"): name = data["slug"] + elif data.get("mac_address"): + name = data["mac_address"] elif endpoint_name == "cable": if self.module.params["data"]["termination_a"].get("name"): termination_a_name = self.module.params["data"]["termination_a"]["name"] diff --git a/plugins/module_utils/netbox_utils.py b/plugins/module_utils/netbox_utils.py index ea76f43c0..08a756234 100644 --- a/plugins/module_utils/netbox_utils.py +++ b/plugins/module_utils/netbox_utils.py @@ -78,6 +78,7 @@ "sites": {}, "site_groups": {}, "virtual_chassis": {}, + "mac_addresses": {}, }, extras={ "config_contexts": {}, @@ -226,6 +227,7 @@ webhook="name", wireless_lan="ssid", wireless_lan_group="slug", + mac_address="mac_address", ) # Specifies keys within data that need to be converted to ID and the endpoint to be used when queried @@ -275,6 +277,7 @@ "ipsec_profile": "ipsec_profiles", "location": "locations", "lag": "interfaces", + "primary_mac_address": "mac_addresses", "manufacturer": "manufacturers", "master": "devices", "module": "modules", @@ -434,6 +437,7 @@ "wireless_lans": "wireless_lan", "wireless_lan_groups": "wireless_lan_group", "wireless_links": "wireless_link", + "mac_addresses": "mac_address", } ALLOWED_QUERY_PARAMS = { @@ -516,6 +520,7 @@ ), "lag": set(["name"]), "location": set(["name", "slug", "site"]), + "mac_address": set(["mac_address"]), "module": set(["device", "module_bay", "module_type"]), "module_bay": set(["device", "name"]), "module_type": set(["model"]), @@ -1414,7 +1419,7 @@ def _normalize_data(self, data): # We need to assign the correct type for the assigned object so the user doesn't have to worry about this. # We determine it by whether or not they pass in a device or virtual_machine - if data.get("assigned_object"): + if data.get("assigned_object") and isinstance(data["assigned_object"], dict): if data["assigned_object"].get("device"): data["assigned_object_type"] = "dcim.interface" if data["assigned_object"].get("virtual_machine"): diff --git a/plugins/modules/netbox_device_interface.py b/plugins/modules/netbox_device_interface.py index 63980f922..42d6e73b8 100644 --- a/plugins/modules/netbox_device_interface.py +++ b/plugins/modules/netbox_device_interface.py @@ -86,6 +86,11 @@ - The MAC address of the interface required: false type: str + primary_mac_address: + description: + - The primary MAC address of the interface (NetBox 4.2 and later) + required: false + type: str wwn: description: - The WWN of the interface @@ -335,6 +340,7 @@ def main(): bridge=dict(required=False, type="raw"), mtu=dict(required=False, type="int"), mac_address=dict(required=False, type="str"), + primary_mac_address=dict(required=False, type="str"), wwn=dict(required=False, type="str"), mgmt_only=dict(required=False, type="bool"), poe_type=dict(required=False, type="raw"), diff --git a/plugins/modules/netbox_mac_address.py b/plugins/modules/netbox_mac_address.py new file mode 100644 index 000000000..7d70b8269 --- /dev/null +++ b/plugins/modules/netbox_mac_address.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2025, Martin Rødvand (@rodvand) +# 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_mac_address +short_description: Create, update or delete MAC addresses within NetBox +description: + - Creates, updates or removes MAC addresses from NetBox +notes: + - Tags should be defined as a YAML list + - This should be ran with connection C(local) and hosts C(localhost) +author: + - Martin Rødvand (@rodvand) +requirements: + - pynetbox +version_added: "3.21.0" +extends_documentation_fragment: + - netbox.netbox.common +options: + data: + type: dict + description: + - Defines the MAC address configuration + suboptions: + mac_address: + description: + - The MAC address + required: true + type: str + assigned_object: + description: + - The object to assign this MAC address to + required: false + type: dict + description: + description: + - Description of the MAC address + required: false + type: str + comments: + description: + - Comments for the MAC address + required: false + type: str + tags: + description: + - Any tags that the MAC address may need to be associated with + required: false + type: list + elements: raw + custom_fields: + description: + - Must exist in NetBox and in key/value format + required: false + type: dict + required: true +""" + +EXAMPLES = r""" +- name: "Test NetBox MAC address module" + connection: local + hosts: localhost + gather_facts: false + + tasks: + - name: Create MAC address within NetBox with only required information + netbox.netbox.netbox_mac_address: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + mac_address: "00:11:22:33:44:55" + state: present + + - name: Create MAC address with interface assignment + netbox.netbox.netbox_mac_address: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + mac_address: "AA:BB:CC:DD:EE:FF" + assigned_object: + device: Test Nexus One + name: Ethernet1/1 + description: "MAC address for eth1/1" + tags: + - Network + state: present + + - name: Delete MAC address within netbox + netbox.netbox.netbox_mac_address: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + mac_address: "00:11:22:33:44:55" + state: absent +""" + +RETURN = r""" +mac_address: + description: Serialized object as created or already existent within NetBox + 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.netbox.netbox.plugins.module_utils.netbox_utils import ( + NetboxAnsibleModule, + NETBOX_ARG_SPEC, +) +from ansible_collections.netbox.netbox.plugins.module_utils.netbox_dcim import ( + NetboxDcimModule, + NB_MAC_ADDRESSES, +) +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( + mac_address=dict(required=True, type="str"), + assigned_object=dict(required=False, type="dict"), + description=dict(required=False, type="str"), + comments=dict(required=False, type="str"), + tags=dict(required=False, type="list", elements="raw"), + custom_fields=dict(required=False, type="dict"), + ), + ), + ) + ) + + required_if = [ + ("state", "present", ["mac_address"]), + ("state", "absent", ["mac_address"]), + ] + + module = NetboxAnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=required_if, + ) + + netbox_mac_address = NetboxDcimModule(module, NB_MAC_ADDRESSES) + netbox_mac_address.run() + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/plugins/modules/netbox_vm_interface.py b/plugins/modules/netbox_vm_interface.py index d4bf739f2..133bfee21 100644 --- a/plugins/modules/netbox_vm_interface.py +++ b/plugins/modules/netbox_vm_interface.py @@ -53,6 +53,11 @@ - The MAC address of the interface required: false type: str + primary_mac_address: + description: + - The primary MAC address of the interface (NetBox 4.2 and later) + required: false + type: str description: description: - The description of the interface @@ -209,6 +214,7 @@ def main(): enabled=dict(required=False, type="bool"), mtu=dict(required=False, type="int"), mac_address=dict(required=False, type="str"), + primary_mac_address=dict(required=False, type="str"), description=dict(required=False, type="str"), mode=dict(required=False, type="raw"), vm_bridge=dict(required=False, type="raw"), diff --git a/tests/integration/netbox-deploy.py b/tests/integration/netbox-deploy.py index 68f28cb2e..af5a7b5af 100755 --- a/tests/integration/netbox-deploy.py +++ b/tests/integration/netbox-deploy.py @@ -372,6 +372,24 @@ def make_netbox_calls(endpoint, payload): ] created_interfaces = make_netbox_calls(nb.dcim.interfaces, dev_interfaces) +# Create MAC addresses +if nb_version >= version.parse("4.2"): + test100_gi1 = nb.dcim.interfaces.get(name="GigabitEthernet1", device_id=1) + test100_gi2 = nb.dcim.interfaces.get(name="GigabitEthernet2", device_id=1) + mac_addresses = [ + { + "mac_address": "AA:BB:CC:DD:EE:FF", + "assigned_object_id": test100_gi1.id, + "assigned_object_type": "dcim.interface", + }, + { + "mac_address": "AA:AB:CC:DD:EE:FF", + "assigned_object_id": test100_gi2.id, + "assigned_object_type": "dcim.interface", + }, + ] + created_mac_addresses = make_netbox_calls(nb.dcim.mac_addresses, mac_addresses) + # Wireless Interfaces if nb_version >= version.parse("3.1"): wlink_interfaces = [ diff --git a/tests/integration/targets/v4.2/tasks/main.yml b/tests/integration/targets/v4.2/tasks/main.yml index 9b5b5c21a..33ce77bcf 100644 --- a/tests/integration/targets/v4.2/tasks/main.yml +++ b/tests/integration/targets/v4.2/tasks/main.yml @@ -332,15 +332,6 @@ tags: - netbox_circuit -- name: NETBOX_CIRCUIT_TERMINATION TESTS - ansible.builtin.include_tasks: - file: netbox_circuit_termination.yml - apply: - tags: - - netbox_circuit_termination - tags: - - netbox_circuit_termination - - name: NETBOX_REAR_PORT TESTS ansible.builtin.include_tasks: file: netbox_rear_port.yml @@ -732,3 +723,21 @@ - netbox_tunnel_group tags: - netbox_tunnel_group + +- name: NETBOX_MAC_ADDRESS TESTS + ansible.builtin.include_tasks: + file: netbox_mac_address.yml + apply: + tags: + - netbox_mac_address + tags: + - netbox_mac_address + +- name: NETBOX_CIRCUIT_TERMINATION TESTS + ansible.builtin.include_tasks: + file: netbox_circuit_termination.yml + apply: + tags: + - netbox_circuit_termination + tags: + - netbox_circuit_termination diff --git a/tests/integration/targets/v4.2/tasks/netbox_device_interface.yml b/tests/integration/targets/v4.2/tasks/netbox_device_interface.yml index dd3c2383b..5013b6edc 100644 --- a/tests/integration/targets/v4.2/tasks/netbox_device_interface.yml +++ b/tests/integration/targets/v4.2/tasks/netbox_device_interface.yml @@ -310,3 +310,23 @@ - test_twelve['interface']['name'] == "GigabitEthernet5" - test_twelve['interface']['device'] == 1 - test_twelve['interface']['mark_connected'] == true + +- name: "13 - Update interface primary MAC address" + netbox.netbox.netbox_device_interface: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + device: test100 + name: GigabitEthernet2 + primary_mac_address: "AA:AB:CC:DD:EE:FF" + state: present + register: test_thirteen + +- name: "13 - ASSERT" + ansible.builtin.assert: + that: + - test_thirteen is changed + - test_thirteen['msg'] == "interface GigabitEthernet2 updated" + - test_thirteen['interface']['name'] == "GigabitEthernet2" + - test_thirteen['interface']['device'] == 1 + - test_thirteen['interface']['primary_mac_address'] == 2 diff --git a/tests/integration/targets/v4.2/tasks/netbox_mac_address.yml b/tests/integration/targets/v4.2/tasks/netbox_mac_address.yml new file mode 100644 index 000000000..88192d197 --- /dev/null +++ b/tests/integration/targets/v4.2/tasks/netbox_mac_address.yml @@ -0,0 +1,70 @@ +--- +## +## +### NETBOX_MAC_ADDRESS +## +## +- name: "MAC 1: Create MAC address with required parameters" + netbox.netbox.netbox_mac_address: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + mac_address: "00:11:22:33:44:55" + state: present + register: test_one + +- name: "MAC 1: ASSERT - Create MAC address" + ansible.builtin.assert: + that: + - test_one is changed + - test_one['diff']['before']['state'] == "absent" + - test_one['diff']['after']['state'] == "present" + - test_one['mac_address']['mac_address'] == "00:11:22:33:44:55" + - test_one['msg'] == "mac_address 00:11:22:33:44:55 created" + +- name: "MAC 2: Create MAC address with all parameters" + netbox.netbox.netbox_mac_address: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + mac_address: "AA:BB:CC:DD:EE:F0" + assigned_object: + device: Test Nexus One + name: Ethernet1/1 + description: "Test MAC address" + comments: "Test MAC address comment" + tags: + - Schnozzberry + state: present + register: test_two + +- name: "MAC 2: ASSERT - Create MAC address with all parameters" + ansible.builtin.assert: + that: + - test_two is changed + - test_two['diff']['before']['state'] == "absent" + - test_two['diff']['after']['state'] == "present" + - test_two['mac_address']['mac_address'] == "AA:BB:CC:DD:EE:F0" + - test_two['mac_address']['assigned_object_type'] == "dcim.interface" + - test_two['mac_address']['assigned_object_id'] == 1 + - test_two['mac_address']['description'] == "Test MAC address" + - test_two['mac_address']['comments'] == "Test MAC address comment" + - test_two['mac_address']['tags'][0] == 4 + - test_two['msg'] == "mac_address AA:BB:CC:DD:EE:F0 created" + +- name: "MAC 3: Delete MAC address" + netbox.netbox.netbox_mac_address: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + mac_address: "00:11:22:33:44:55" + state: absent + register: test_three + +- name: "MAC 3: ASSERT - Delete MAC address" + ansible.builtin.assert: + that: + - test_three is changed + - test_three['diff']['before']['state'] == "present" + - test_three['diff']['after']['state'] == "absent" + - test_three['msg'] == "mac_address 00:11:22:33:44:55 deleted"