From 0929aacf4a4b4c77d7eaaf61ec109eba3441dc70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=B8dvand?= Date: Wed, 2 Feb 2022 23:03:43 +0100 Subject: [PATCH 1/8] Initial work on netbox_custom_fields --- plugins/module_utils/netbox_extras.py | 1 + plugins/module_utils/netbox_utils.py | 5 +- plugins/modules/netbox_custom_field.py | 201 +++++++++++++++++++++++++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 plugins/modules/netbox_custom_field.py 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..a6f819bef --- /dev/null +++ b/plugins/modules/netbox_custom_field.py @@ -0,0 +1,201 @@ +#!/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: true + type: list + elements: raw + type: + description: + - The type of custom field + required: true + type: raw + name: + description: + - Name of the custom field + required: true + type: str + label: + description: + - Label of the custom field + required: true + type: str + description: + description: + - Description of the custom field + required: true + 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: str + 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: string + choices: + description: + - List of available choices (for selection fields) + required: false + type: list + elements: str + required: true +""" + +EXAMPLES = r""" +- name: "Test NetBox config_context module" + connection: local + hosts: localhost + gather_facts: False + tasks: + - name: Create config context and apply it to sites euc1-az1, euc1-az2 with the default weight of 1000 + netbox_config_context: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + name: "dns_nameservers-quadnine" + description: "9.9.9.9" + data: "{ \"dns\": { \"nameservers\": [ \"9.9.9.9\" ] } }" + sites: [ euc1-az1, euc1-az2 ] + + - name: Detach config context from euc1-az1, euc1-az2 and attach to euc1-az3 + netbox_config_context: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + name: "dns_nameservers-quadnine" + data: "{ \"dns\": { \"nameservers\": [ \"9.9.9.9\" ] } }" + sites: [ euc1-az3 ] + + - name: Delete config context + netbox_config_context: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + name: "dns_nameservers-quadnine" + data: "{ \"dns\": { \"nameservers\": [ \"9.9.9.9\" ] } }" + 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=True, type="list", elements="raw"), + type=dict(required=True, type="raw"), + name=dict(required=False, 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() From c3c3c523f4a539c4a1b7989e26e081720656d513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=B8dvand?= Date: Wed, 2 Feb 2022 23:41:19 +0100 Subject: [PATCH 2/8] Add tests for netbox_custom_field --- plugins/modules/netbox_custom_field.py | 40 ++++--- tests/integration/targets/v3.1/tasks/main.yml | 11 +- .../v3.1/tasks/netbox_custom_field.yml | 107 ++++++++++++++++++ 3 files changed, 136 insertions(+), 22 deletions(-) create mode 100644 tests/integration/targets/v3.1/tasks/netbox_custom_field.yml diff --git a/plugins/modules/netbox_custom_field.py b/plugins/modules/netbox_custom_field.py index a6f819bef..7f80c31dc 100644 --- a/plugins/modules/netbox_custom_field.py +++ b/plugins/modules/netbox_custom_field.py @@ -31,7 +31,7 @@ content_types: description: - The content type(s) to apply this custom field to - required: true + required: false type: list elements: raw type: @@ -99,37 +99,35 @@ """ EXAMPLES = r""" -- name: "Test NetBox config_context module" +- name: "Test NetBox custom_fields module" connection: local - hosts: localhost - gather_facts: False + hosts: localhost tasks: - - name: Create config context and apply it to sites euc1-az1, euc1-az2 with the default weight of 1000 - netbox_config_context: + - name: Create a custom field on device and virtual machine + netbox_custom_field: netbox_url: http://netbox.local netbox_token: thisIsMyToken data: - name: "dns_nameservers-quadnine" - description: "9.9.9.9" - data: "{ \"dns\": { \"nameservers\": [ \"9.9.9.9\" ] } }" - sites: [ euc1-az1, euc1-az2 ] + content_types: + - dcim.device + - virtualization.virtualmachine + name: A Custom Field + type: text - - name: Detach config context from euc1-az1, euc1-az2 and attach to euc1-az3 - netbox_config_context: + - name: Update the custom field to make it required + netbox_custom_field: netbox_url: http://netbox.local netbox_token: thisIsMyToken data: - name: "dns_nameservers-quadnine" - data: "{ \"dns\": { \"nameservers\": [ \"9.9.9.9\" ] } }" - sites: [ euc1-az3 ] + name: A Custom Field + required: yes - - name: Delete config context - netbox_config_context: + - name: Delete the custom field + netbox_custom_field: netbox_url: http://netbox.local netbox_token: thisIsMyToken data: - name: "dns_nameservers-quadnine" - data: "{ \"dns\": { \"nameservers\": [ \"9.9.9.9\" ] } }" + name: A Custom Field state: absent """ @@ -166,8 +164,8 @@ def main(): type="dict", required=True, options=dict( - content_types=dict(required=True, type="list", elements="raw"), - type=dict(required=True, type="raw"), + content_types=dict(required=False, type="list", elements="raw"), + type=dict(required=False, type="raw"), name=dict(required=False, type="str"), label=dict(required=False, type="str"), description=dict(required=False, type="str"), 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..3fef1141a --- /dev/null +++ b/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml @@ -0,0 +1,107 @@ +--- +## +## +### 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 + 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'] == "test_context" + - 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: "CONFIG_CONTEXT 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" From adc545bc9ac84c363996b5840d57edfa02c6e1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=B8dvand?= Date: Wed, 2 Feb 2022 23:46:55 +0100 Subject: [PATCH 3/8] Update so doc and argspec matches --- plugins/modules/netbox_custom_field.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/modules/netbox_custom_field.py b/plugins/modules/netbox_custom_field.py index 7f80c31dc..7b53733c8 100644 --- a/plugins/modules/netbox_custom_field.py +++ b/plugins/modules/netbox_custom_field.py @@ -47,12 +47,12 @@ label: description: - Label of the custom field - required: true + required: false type: str description: description: - Description of the custom field - required: true + required: false type: str required: description: @@ -63,7 +63,7 @@ description: - Filter logic of the custom field required: false - type: str + type: raw default: description: - Default value of the custom field @@ -165,8 +165,8 @@ def main(): required=True, options=dict( content_types=dict(required=False, type="list", elements="raw"), - type=dict(required=False, type="raw"), - name=dict(required=False, type="str"), + type=dict(required=True, 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"), From 5cfaf03f9c4cfd1bf284674fdf08deae8f40e448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=B8dvand?= Date: Wed, 2 Feb 2022 23:57:21 +0100 Subject: [PATCH 4/8] Documentation fix --- plugins/modules/netbox_custom_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/netbox_custom_field.py b/plugins/modules/netbox_custom_field.py index 7b53733c8..a57c29c58 100644 --- a/plugins/modules/netbox_custom_field.py +++ b/plugins/modules/netbox_custom_field.py @@ -88,7 +88,7 @@ description: - The regular expression to enforce on text fields required: false - type: string + type: str choices: description: - List of available choices (for selection fields) From 4cd97bf8d4c1b0ef5d9ba0fef6a5d8e62b56f131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=B8dvand?= Date: Thu, 3 Feb 2022 00:17:49 +0100 Subject: [PATCH 5/8] Fix formatting in test --- .../integration/targets/v3.1/tasks/netbox_custom_field.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml b/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml index 3fef1141a..b4b335aa9 100644 --- a/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml +++ b/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml @@ -75,9 +75,9 @@ data: content_types: - "virtualization.virtualmachine" - name: "A_CustomField" - description: "Added a description" - required: yes + name: "A_CustomField" + description: "Added a description" + required: yes state: present register: test_four From f6c9d626c56e8105054c34808791d38259e9fa5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=B8dvand?= Date: Thu, 3 Feb 2022 00:35:21 +0100 Subject: [PATCH 6/8] Adjust the type option in argspec --- plugins/modules/netbox_custom_field.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/netbox_custom_field.py b/plugins/modules/netbox_custom_field.py index a57c29c58..317da4b49 100644 --- a/plugins/modules/netbox_custom_field.py +++ b/plugins/modules/netbox_custom_field.py @@ -36,8 +36,8 @@ elements: raw type: description: - - The type of custom field - required: true + - The type of custom field (defaults to text) + required: false type: raw name: description: @@ -165,7 +165,7 @@ def main(): required=True, options=dict( content_types=dict(required=False, type="list", elements="raw"), - type=dict(required=True, type="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"), From ef5548e66387f8b357e9ad1af243f260832f751b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=B8dvand?= Date: Thu, 3 Feb 2022 07:14:41 +0100 Subject: [PATCH 7/8] Test adjustment --- tests/integration/targets/v3.1/tasks/netbox_custom_field.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml b/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml index b4b335aa9..fa92ca057 100644 --- a/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml +++ b/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml @@ -12,6 +12,7 @@ content_types: - "dcim.device" name: A_CustomField + type: text state: present register: test_one From 6c8f9914c0fb4fa5bab914b93bc0deb842cd1c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20R=C3=B8dvand?= Date: Thu, 3 Feb 2022 07:29:37 +0100 Subject: [PATCH 8/8] Test, test, test! --- plugins/modules/netbox_custom_field.py | 2 +- tests/integration/targets/v3.1/tasks/netbox_custom_field.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/modules/netbox_custom_field.py b/plugins/modules/netbox_custom_field.py index 317da4b49..cea974d4e 100644 --- a/plugins/modules/netbox_custom_field.py +++ b/plugins/modules/netbox_custom_field.py @@ -36,7 +36,7 @@ elements: raw type: description: - - The type of custom field (defaults to text) + - The type of custom field required: false type: raw name: diff --git a/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml b/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml index fa92ca057..0db0b5e1a 100644 --- a/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml +++ b/tests/integration/targets/v3.1/tasks/netbox_custom_field.yml @@ -66,7 +66,7 @@ - test_three is changed - test_three['diff']['after']['description'] == "Added a description" - test_three['diff']['after']['required'] == true - - test_three['custom_field']['name'] == "test_context" + - test_three['custom_field']['name'] == "A_CustomField" - test_three['msg'] == "custom_field A_CustomField updated" - name: "CUSTOM_FIELD 4: Change content type" @@ -82,7 +82,7 @@ state: present register: test_four -- name: "CONFIG_CONTEXT 4: ASSERT - Change content type" +- name: "CUSTOM_FIELD 4: ASSERT - Change content type" assert: that: - test_four is changed