diff --git a/plugins/module_utils/netbox_extras.py b/plugins/module_utils/netbox_extras.py index 7ee0b6560..8a14c4218 100644 --- a/plugins/module_utils/netbox_extras.py +++ b/plugins/module_utils/netbox_extras.py @@ -13,6 +13,7 @@ NB_CONFIG_CONTEXTS = "config_contexts" NB_TAGS = "tags" +NB_CUSTOM_FIELDS = "custom_fields" class NetboxExtrasModule(NetboxModule): diff --git a/plugins/module_utils/netbox_utils.py b/plugins/module_utils/netbox_utils.py index f5df8ae01..b7ef96416 100644 --- a/plugins/module_utils/netbox_utils.py +++ b/plugins/module_utils/netbox_utils.py @@ -74,7 +74,7 @@ "site_groups", "virtual_chassis", ], - extras=["config_contexts", "tags"], + extras=["config_contexts", "tags", "custom_fields"], ipam=[ "aggregates", "ip_addresses", @@ -109,6 +109,7 @@ config_context="name", contact_group="name", contact_role="name", + custom_field="name", device="name", device_role="slug", device_type="slug", @@ -279,6 +280,7 @@ "contacts": "contact", "contact_groups": "contact_group", "contact_roles": "contact_role", + "custom_fields": "custom_field", "device_bays": "device_bay", "device_bay_templates": "device_bay_template", "devices": "device", @@ -360,6 +362,7 @@ "contact": set(["name", "group"]), "contact_group": set(["name"]), "contact_role": set(["name"]), + "custom_field": set(["name"]), "dcim.consoleport": set(["name", "device"]), "dcim.consoleserverport": set(["name", "device"]), "dcim.frontport": set(["name", "device", "rear_port"]), diff --git a/plugins/modules/netbox_custom_field.py b/plugins/modules/netbox_custom_field.py new file mode 100644 index 000000000..cea974d4e --- /dev/null +++ b/plugins/modules/netbox_custom_field.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2022, 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_custom_field +short_description: Creates, updates or deletes custom fields within NetBox +description: + - Creates, updates or removes custom fields from NetBox +notes: + - This should be ran with connection C(local) and hosts C(localhost) +author: + - Martin Rødvand (@rodvand) +requirements: + - pynetbox +version_added: "3.6.0" +extends_documentation_fragment: + - netbox.netbox.common +options: + data: + type: dict + description: + - Defines the custom field + suboptions: + content_types: + description: + - The content type(s) to apply this custom field to + required: false + type: list + elements: raw + type: + description: + - The type of custom field + required: false + type: raw + name: + description: + - Name of the custom field + required: true + type: str + label: + description: + - Label of the custom field + required: false + type: str + description: + description: + - Description of the custom field + required: false + type: str + required: + description: + - Whether the custom field is required + required: false + type: bool + filter_logic: + description: + - Filter logic of the custom field + required: false + type: raw + default: + description: + - Default value of the custom field + required: false + type: raw + weight: + description: + - Fields with higher weights appear lower in a form + required: false + type: int + validation_minimum: + description: + - The minimum allowed value (for numeric fields) + required: false + type: int + validation_maximum: + description: + - The maximum allowed value (for numeric fields) + required: false + type: int + validation_regex: + description: + - The regular expression to enforce on text fields + required: false + type: str + choices: + description: + - List of available choices (for selection fields) + required: false + type: list + elements: str + required: true +""" + +EXAMPLES = r""" +- name: "Test NetBox custom_fields module" + connection: local + hosts: localhost + tasks: + - name: Create a custom field on device and virtual machine + netbox_custom_field: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + content_types: + - dcim.device + - virtualization.virtualmachine + name: A Custom Field + type: text + + - name: Update the custom field to make it required + netbox_custom_field: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + name: A Custom Field + required: yes + + - name: Delete the custom field + netbox_custom_field: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + name: A Custom Field + state: absent +""" + +RETURN = r""" +custom_field: + description: Serialized object as created/existent/updated/deleted within NetBox + returned: always + 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_extras import ( + NetboxExtrasModule, + NB_CUSTOM_FIELDS, +) +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( + content_types=dict(required=False, type="list", elements="raw"), + type=dict(required=False, type="raw"), + name=dict(required=True, type="str"), + label=dict(required=False, type="str"), + description=dict(required=False, type="str"), + required=dict(required=False, type="bool"), + filter_logic=dict(required=False, type="raw"), + default=dict(required=False, type="raw"), + weight=dict(required=False, type="int"), + validation_minimum=dict(required=False, type="int"), + validation_maximum=dict(required=False, type="int"), + validation_regex=dict(required=False, type="str"), + choices=dict(required=False, type="list", elements="str"), + ), + ) + ) + ) + + required_if = [ + ("state", "present", ["content_types", "name"]), + ("state", "absent", ["name"]), + ] + + module = NetboxAnsibleModule( + argument_spec=argument_spec, supports_check_mode=True, required_if=required_if + ) + + netbox_custom_field = NetboxExtrasModule(module, NB_CUSTOM_FIELDS) + netbox_custom_field.run() + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/tests/integration/targets/v3.1/tasks/main.yml b/tests/integration/targets/v3.1/tasks/main.yml index 7880f587f..b342a5266 100644 --- a/tests/integration/targets/v3.1/tasks/main.yml +++ b/tests/integration/targets/v3.1/tasks/main.yml @@ -198,4 +198,13 @@ tags: - netbox_wireless_link tags: - - netbox_wireless_link \ No newline at end of file + - netbox_wireless_link + +- name: "NETBOX_CUSTOM_FIELD TESTS" + include_tasks: + file: "netbox_custom_field.yml" + apply: + tags: + - netbox_custom_field + tags: + - netbox_custom_field \ No newline at end of file diff --git a/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml b/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml new file mode 100644 index 000000000..0db0b5e1a --- /dev/null +++ b/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml @@ -0,0 +1,108 @@ +--- +## +## +### NETBOX_CUSTOM_FIELD +## +## +- name: "CUSTOM_FIELD 1: Necessary info creation" + netbox.netbox.netbox_custom_field: + netbox_url: http://localhost:32768 + netbox_token: 0123456789abcdef0123456789abcdef01234567 + data: + content_types: + - "dcim.device" + name: A_CustomField + type: text + state: present + register: test_one + +- name: "CUSTOM_FIELD 1: ASSERT - Necessary info creation" + assert: + that: + - test_one is changed + - test_one['diff']['before']['state'] == "absent" + - test_one['diff']['after']['state'] == "present" + - test_one['custom_field']['name'] == "A_CustomField" + - test_one['custom_field']['required'] == false + - test_one['custom_field']['content_types'] == ["dcim.device"] + - test_one['custom_field']['type'] == "text" + - test_one['custom_field']['weight'] == 100 + - test_one['msg'] == "custom_field A_CustomField created" + +- name: "CUSTOM_FIELD 2: Create duplicate" + netbox.netbox.netbox_custom_field: + netbox_url: http://localhost:32768 + netbox_token: 0123456789abcdef0123456789abcdef01234567 + data: + content_types: + - "dcim.device" + name: A_CustomField + state: present + register: test_two + +- name: "CUSTOM_FIELD 2: ASSERT - Create duplicate" + assert: + that: + - not test_two['changed'] + - test_two['custom_field']['name'] == "A_CustomField" + - test_two['msg'] == "custom_field A_CustomField already exists" + +- name: "CUSTOM_FIELD 3: Update data and make it required" + netbox.netbox.netbox_custom_field: + netbox_url: http://localhost:32768 + netbox_token: 0123456789abcdef0123456789abcdef01234567 + data: + content_types: + - "dcim.device" + name: "A_CustomField" + description: "Added a description" + required: yes + state: present + register: test_three + +- name: "CUSTOM_FIELD 3: ASSERT - Updated" + assert: + that: + - test_three is changed + - test_three['diff']['after']['description'] == "Added a description" + - test_three['diff']['after']['required'] == true + - test_three['custom_field']['name'] == "A_CustomField" + - test_three['msg'] == "custom_field A_CustomField updated" + +- name: "CUSTOM_FIELD 4: Change content type" + netbox.netbox.netbox_custom_field: + netbox_url: http://localhost:32768 + netbox_token: 0123456789abcdef0123456789abcdef01234567 + data: + content_types: + - "virtualization.virtualmachine" + name: "A_CustomField" + description: "Added a description" + required: yes + state: present + register: test_four + +- name: "CUSTOM_FIELD 4: ASSERT - Change content type" + assert: + that: + - test_four is changed + - test_four['diff']['after']['content_types'] == ["virtualization.virtualmachine"] + - test_four['custom_field']['name'] == "A_CustomField" + - test_four['msg'] == "custom_field A_CustomField updated" + +- name: "CUSTOM_FIELD 5: Delete" + netbox.netbox.netbox_custom_field: + netbox_url: http://localhost:32768 + netbox_token: 0123456789abcdef0123456789abcdef01234567 + data: + name: "A_CustomField" + state: absent + register: test_five + +- name: "CUSTOM_FIELD 5: ASSERT - Deleted" + assert: + that: + - test_five is changed + - test_five['diff']['after']['state'] == "absent" + - test_five['custom_field']['name'] == "A_CustomField" + - test_five['msg'] == "custom_field A_CustomField deleted"