diff --git a/changelogs/fragments/1459-add-netbox-data-sources.yml b/changelogs/fragments/1459-add-netbox-data-sources.yml new file mode 100644 index 000000000..17edb4042 --- /dev/null +++ b/changelogs/fragments/1459-add-netbox-data-sources.yml @@ -0,0 +1,2 @@ +minor_changes: + - netbox_data_source - New module `#1459 ` diff --git a/plugins/module_utils/netbox_core.py b/plugins/module_utils/netbox_core.py new file mode 100644 index 000000000..5361d8d50 --- /dev/null +++ b/plugins/module_utils/netbox_core.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2025, Daniel Chiquito (@dchiquito) +# 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 + +from ansible_collections.netbox.netbox.plugins.module_utils.netbox_utils import ( + NetboxModule, + ENDPOINT_NAME_MAPPING, + SLUG_REQUIRED, +) + +NB_DATA_SOURCES = "data_sources" + + +class NetboxCoreModule(NetboxModule): + def __init__(self, module, endpoint): + super().__init__(module, endpoint) + + def _handle_state_new(self, nb_app, nb_endpoint, endpoint_name, data): + if self.state == "new": + self.nb_object, diff = self._create_netbox_object(nb_endpoint, data) + self.result["msg"] = "%s created" % (endpoint_name) + self.result["changed"] = True + self.result["diff"] = diff + + def run(self): + """ + This function should have all necessary code for endpoints within the application + to create/update/delete the endpoint objects + Supported endpoints: + - data_sources + """ + # Used to dynamically set key when returning results + endpoint_name = ENDPOINT_NAME_MAPPING[self.endpoint] + + self.result = {"changed": False} + + application = self._find_app(self.endpoint) + nb_app = getattr(self.nb, application) + nb_endpoint = getattr(nb_app, self.endpoint) + user_query_params = self.module.params.get("query_params") + + data = self.data + + # Used for msg output + if data.get("name"): + name = data["name"] + elif data.get("slug"): + name = data["slug"] + + if self.endpoint in SLUG_REQUIRED: + if not data.get("slug"): + data["slug"] = self._to_slug(name) + + # Make color params lowercase + if data.get("color"): + data["color"] = data["color"].lower() + + # Handle journal entry + if self.state == "new" and endpoint_name == "journal_entry": + self._handle_state_new(nb_app, nb_endpoint, endpoint_name, data) + else: + object_query_params = self._build_query_params( + endpoint_name, data, user_query_params + ) + self.nb_object = self._nb_endpoint_get( + nb_endpoint, object_query_params, name + ) + + if self.state == "present": + self._ensure_object_exists(nb_endpoint, endpoint_name, name, data) + elif self.state == "absent": + self._ensure_object_absent(endpoint_name, name) + + try: + serialized_object = self.nb_object.serialize() + except AttributeError: + serialized_object = self.nb_object + + self.result.update({endpoint_name: serialized_object}) + + self.module.exit_json(**self.result) diff --git a/plugins/module_utils/netbox_utils.py b/plugins/module_utils/netbox_utils.py index 2d5549849..3fd869f06 100644 --- a/plugins/module_utils/netbox_utils.py +++ b/plugins/module_utils/netbox_utils.py @@ -39,6 +39,9 @@ "providers": {}, "provider_networks": {}, }, + core={ + "data_sources": {}, + }, dcim={ "cables": {}, "console_ports": {}, @@ -371,6 +374,7 @@ "custom_fields": "custom_field", "custom_field_choice_sets": "choice_set", "custom_links": "custom_link", + "data_sources": "data_source", "device_bays": "device_bay", "device_bay_templates": "device_bay_template", "devices": "device", @@ -482,6 +486,7 @@ "custom_field_choice_set": set(["name"]), "choice_set": set(["name"]), "custom_link": set(["name"]), + "data_source": 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_data_source.py b/plugins/modules/netbox_data_source.py new file mode 100644 index 000000000..9923e2927 --- /dev/null +++ b/plugins/modules/netbox_data_source.py @@ -0,0 +1,190 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2025, Daniel Chiquito (@dchiquito) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: netbox_data_source +short_description: Creates or removes data sources from NetBox +description: + - Creates or removes data sources from NetBox +author: + - Daniel Chiquito (@dchiquito) +requirements: + - pynetbox +version_added: "3.22.0" +extends_documentation_fragment: + - netbox.netbox.common +options: + data: + type: dict + description: + - Defines the data source configuration + suboptions: + name: + description: + - Name of the data source + required: true + type: str + type: + description: + - The origin of the data source + choices: + - local + - git + - amazon-s3 + required: false + type: str + source_url: + description: + - URL of the data source to be created + required: false + type: str + enabled: + description: + - Whether or not this data source can be synced + required: false + type: bool + description: + description: + - Description of the data source + required: false + type: str + ignore_rules: + description: + - Patterns (one per line) matching files to ignore when syncing + required: false + type: str + sync_interval: + description: + - The interval in seconds between syncs + required: false + choices: + - 1 + - 60 + - 720 + - 1440 + - 10080 + - 43200 + type: int + comments: + description: + - Comments about the data source + required: false + type: str + required: true +""" + +EXAMPLES = r""" +- name: "Test NetBox modules" + connection: local + hosts: localhost + gather_facts: false + + tasks: + - name: "Create a new data source with only required information" + netbox.netbox.netbox_data_source: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + name: "Data Source 1" + type: "local" + source_url: "/tmp/data-source.txt" + enabled: true + state: present + - name: "Update that data source with other fields" + netbox.netbox.netbox_data_source: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + name: "Data Source 1" + type: "amazon-s3" + source_url: "path/to/bucket" + enabled: false + description: "My first data source" + ignore_rules: ".*\nfoo.txt\n*.yml" + sync_interval: 1440 + comments: "Some commentary on this data source" + state: present + - name: "Delete the data source" + netbox.netbox.netbox_data_source: + netbox_url: http://netbox.local + netbox_token: thisIsMyToken + data: + name: "Data Source 1" + state: absent +""" + +RETURN = r""" +data_source: + description: Serialized object as created or already existent within NetBox + returned: on creation + 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_core import ( + NetboxCoreModule, + NB_DATA_SOURCES, +) +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( + name=dict(required=True, type="str"), + type=dict( + required=False, + choices=["local", "git", "amazon-s3"], + type="str", + ), + source_url=dict(required=False, type="str"), + enabled=dict(required=False, type="bool"), + description=dict(required=False, type="str"), + ignore_rules=dict(required=False, type="str"), + sync_interval=dict( + required=False, + choices=[1, 60, 60 * 12, 60 * 24, 60 * 24 * 7, 60 * 24 * 30], + type="int", + ), + comments=dict(required=False, type="str"), + ), + ), + ) + ) + + required_if = [ + ("state", "present", ["name", "type", "source_url", "enabled"]), + ("state", "absent", ["name"]), + ] + + module = NetboxAnsibleModule( + argument_spec=argument_spec, supports_check_mode=True, required_if=required_if + ) + + netbox_data_source = NetboxCoreModule(module, NB_DATA_SOURCES) + netbox_data_source.run() + + +if __name__ == "__main__": # pragma: no cover + main() diff --git a/tests/integration/targets/v4.0/tasks/netbox_data_source.yml b/tests/integration/targets/v4.0/tasks/netbox_data_source.yml new file mode 100644 index 000000000..9fd954cba --- /dev/null +++ b/tests/integration/targets/v4.0/tasks/netbox_data_source.yml @@ -0,0 +1,75 @@ +--- +## +## +### NETBOX_DATA_SOURCE +## +## +- name: "DATA SOURCE 1: Create" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + type: "local" + source_url: "/tmp/data-source.txt" + enabled: true + state: present + register: test_one + +- name: "DATA SOURCE 1: Assert - Create" + ansible.builtin.assert: + that: + - test_one is changed + - test_one['diff']['before']['state'] == "absent" + - test_one['diff']['after']['state'] == "present" + - test_one['data_source']['name'] == "Data Source 1" + - test_one['data_source']['type'] == "local" + - test_one['data_source']['source_url'] == "/tmp/data-source.txt" + - test_one['data_source']['enabled'] == true + - test_one['msg'] == "data_source Data Source 1 created" + +- name: "DATA SOURCE 2: Update" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + type: "amazon-s3" + source_url: "path/to/bucket" + enabled: false + description: "My first data source" + ignore_rules: ".*\nfoo.txt\n*.yml" + sync_interval: 1440 + comments: "Some commentary on this data source" + state: present + register: test_two + +- name: "DATA SOURCE 2: Assert - Update" + ansible.builtin.assert: + that: + - test_two is changed + - test_two['data_source']['name'] == "Data Source 1" + - test_two['data_source']['type'] == "amazon-s3" + - test_two['data_source']['source_url'] == "path/to/bucket" + - test_two['data_source']['enabled'] == false + - test_two['data_source']['description'] == "My first data source" + - test_two['data_source']['ignore_rules'] == ".*\nfoo.txt\n*.yml" + - test_two['data_source']['sync_interval'] == 1440 + - test_two['data_source']['comments'] == "Some commentary on this data source" + - test_two['msg'] == "data_source Data Source 1 updated" + +- name: "DATA SOURCE 3: Delete" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + state: absent + register: test_three + +- name: "DATA SOURCE 3: Assert - Delete" + ansible.builtin.assert: + that: + - test_three is changed + - test_three['diff']['before']['state'] == "present" + - test_three['diff']['after']['state'] == "absent" diff --git a/tests/integration/targets/v4.1/tasks/netbox_data_source.yml b/tests/integration/targets/v4.1/tasks/netbox_data_source.yml new file mode 100644 index 000000000..9fd954cba --- /dev/null +++ b/tests/integration/targets/v4.1/tasks/netbox_data_source.yml @@ -0,0 +1,75 @@ +--- +## +## +### NETBOX_DATA_SOURCE +## +## +- name: "DATA SOURCE 1: Create" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + type: "local" + source_url: "/tmp/data-source.txt" + enabled: true + state: present + register: test_one + +- name: "DATA SOURCE 1: Assert - Create" + ansible.builtin.assert: + that: + - test_one is changed + - test_one['diff']['before']['state'] == "absent" + - test_one['diff']['after']['state'] == "present" + - test_one['data_source']['name'] == "Data Source 1" + - test_one['data_source']['type'] == "local" + - test_one['data_source']['source_url'] == "/tmp/data-source.txt" + - test_one['data_source']['enabled'] == true + - test_one['msg'] == "data_source Data Source 1 created" + +- name: "DATA SOURCE 2: Update" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + type: "amazon-s3" + source_url: "path/to/bucket" + enabled: false + description: "My first data source" + ignore_rules: ".*\nfoo.txt\n*.yml" + sync_interval: 1440 + comments: "Some commentary on this data source" + state: present + register: test_two + +- name: "DATA SOURCE 2: Assert - Update" + ansible.builtin.assert: + that: + - test_two is changed + - test_two['data_source']['name'] == "Data Source 1" + - test_two['data_source']['type'] == "amazon-s3" + - test_two['data_source']['source_url'] == "path/to/bucket" + - test_two['data_source']['enabled'] == false + - test_two['data_source']['description'] == "My first data source" + - test_two['data_source']['ignore_rules'] == ".*\nfoo.txt\n*.yml" + - test_two['data_source']['sync_interval'] == 1440 + - test_two['data_source']['comments'] == "Some commentary on this data source" + - test_two['msg'] == "data_source Data Source 1 updated" + +- name: "DATA SOURCE 3: Delete" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + state: absent + register: test_three + +- name: "DATA SOURCE 3: Assert - Delete" + ansible.builtin.assert: + that: + - test_three is changed + - test_three['diff']['before']['state'] == "present" + - test_three['diff']['after']['state'] == "absent" diff --git a/tests/integration/targets/v4.2/tasks/netbox_data_source.yml b/tests/integration/targets/v4.2/tasks/netbox_data_source.yml new file mode 100644 index 000000000..9fd954cba --- /dev/null +++ b/tests/integration/targets/v4.2/tasks/netbox_data_source.yml @@ -0,0 +1,75 @@ +--- +## +## +### NETBOX_DATA_SOURCE +## +## +- name: "DATA SOURCE 1: Create" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + type: "local" + source_url: "/tmp/data-source.txt" + enabled: true + state: present + register: test_one + +- name: "DATA SOURCE 1: Assert - Create" + ansible.builtin.assert: + that: + - test_one is changed + - test_one['diff']['before']['state'] == "absent" + - test_one['diff']['after']['state'] == "present" + - test_one['data_source']['name'] == "Data Source 1" + - test_one['data_source']['type'] == "local" + - test_one['data_source']['source_url'] == "/tmp/data-source.txt" + - test_one['data_source']['enabled'] == true + - test_one['msg'] == "data_source Data Source 1 created" + +- name: "DATA SOURCE 2: Update" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + type: "amazon-s3" + source_url: "path/to/bucket" + enabled: false + description: "My first data source" + ignore_rules: ".*\nfoo.txt\n*.yml" + sync_interval: 1440 + comments: "Some commentary on this data source" + state: present + register: test_two + +- name: "DATA SOURCE 2: Assert - Update" + ansible.builtin.assert: + that: + - test_two is changed + - test_two['data_source']['name'] == "Data Source 1" + - test_two['data_source']['type'] == "amazon-s3" + - test_two['data_source']['source_url'] == "path/to/bucket" + - test_two['data_source']['enabled'] == false + - test_two['data_source']['description'] == "My first data source" + - test_two['data_source']['ignore_rules'] == ".*\nfoo.txt\n*.yml" + - test_two['data_source']['sync_interval'] == 1440 + - test_two['data_source']['comments'] == "Some commentary on this data source" + - test_two['msg'] == "data_source Data Source 1 updated" + +- name: "DATA SOURCE 3: Delete" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + state: absent + register: test_three + +- name: "DATA SOURCE 3: Assert - Delete" + ansible.builtin.assert: + that: + - test_three is changed + - test_three['diff']['before']['state'] == "present" + - test_three['diff']['after']['state'] == "absent" diff --git a/tests/integration/targets/v4.3/tasks/netbox_data_source.yml b/tests/integration/targets/v4.3/tasks/netbox_data_source.yml new file mode 100644 index 000000000..9fd954cba --- /dev/null +++ b/tests/integration/targets/v4.3/tasks/netbox_data_source.yml @@ -0,0 +1,75 @@ +--- +## +## +### NETBOX_DATA_SOURCE +## +## +- name: "DATA SOURCE 1: Create" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + type: "local" + source_url: "/tmp/data-source.txt" + enabled: true + state: present + register: test_one + +- name: "DATA SOURCE 1: Assert - Create" + ansible.builtin.assert: + that: + - test_one is changed + - test_one['diff']['before']['state'] == "absent" + - test_one['diff']['after']['state'] == "present" + - test_one['data_source']['name'] == "Data Source 1" + - test_one['data_source']['type'] == "local" + - test_one['data_source']['source_url'] == "/tmp/data-source.txt" + - test_one['data_source']['enabled'] == true + - test_one['msg'] == "data_source Data Source 1 created" + +- name: "DATA SOURCE 2: Update" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + type: "amazon-s3" + source_url: "path/to/bucket" + enabled: false + description: "My first data source" + ignore_rules: ".*\nfoo.txt\n*.yml" + sync_interval: 1440 + comments: "Some commentary on this data source" + state: present + register: test_two + +- name: "DATA SOURCE 2: Assert - Update" + ansible.builtin.assert: + that: + - test_two is changed + - test_two['data_source']['name'] == "Data Source 1" + - test_two['data_source']['type'] == "amazon-s3" + - test_two['data_source']['source_url'] == "path/to/bucket" + - test_two['data_source']['enabled'] == false + - test_two['data_source']['description'] == "My first data source" + - test_two['data_source']['ignore_rules'] == ".*\nfoo.txt\n*.yml" + - test_two['data_source']['sync_interval'] == 1440 + - test_two['data_source']['comments'] == "Some commentary on this data source" + - test_two['msg'] == "data_source Data Source 1 updated" + +- name: "DATA SOURCE 3: Delete" + netbox.netbox.netbox_data_source: + netbox_url: http://localhost:32768 + netbox_token: "0123456789abcdef0123456789abcdef01234567" + data: + name: "Data Source 1" + state: absent + register: test_three + +- name: "DATA SOURCE 3: Assert - Delete" + ansible.builtin.assert: + that: + - test_three is changed + - test_three['diff']['before']['state'] == "present" + - test_three['diff']['after']['state'] == "absent"