From 8ccb9efa1d8892a487e4ccf3f1539beaf2ca49f1 Mon Sep 17 00:00:00 2001 From: Ansible Core Team Date: Mon, 9 Mar 2020 09:40:33 +0000 Subject: [PATCH] Migrated to netbox.netbox --- .../net_tools/netbox/netbox_utils.py | 357 ------------ .../modules/net_tools/netbox/netbox_device.py | 312 ----------- .../net_tools/netbox/netbox_interface.py | 351 ------------ .../net_tools/netbox/netbox_ip_address.py | 517 ------------------ .../modules/net_tools/netbox/netbox_prefix.py | 461 ---------------- .../modules/net_tools/netbox/netbox_site.py | 348 ------------ lib/ansible/plugins/inventory/netbox.py | 499 ----------------- test/sanity/ignore.txt | 9 - .../net_tools/netbox/test_netbox_utils.py | 152 ----- 9 files changed, 3006 deletions(-) delete mode 100644 lib/ansible/module_utils/net_tools/netbox/netbox_utils.py delete mode 100644 lib/ansible/modules/net_tools/netbox/netbox_device.py delete mode 100644 lib/ansible/modules/net_tools/netbox/netbox_interface.py delete mode 100644 lib/ansible/modules/net_tools/netbox/netbox_ip_address.py delete mode 100644 lib/ansible/modules/net_tools/netbox/netbox_prefix.py delete mode 100644 lib/ansible/modules/net_tools/netbox/netbox_site.py delete mode 100644 lib/ansible/plugins/inventory/netbox.py delete mode 100644 test/units/module_utils/net_tools/netbox/test_netbox_utils.py diff --git a/lib/ansible/module_utils/net_tools/netbox/netbox_utils.py b/lib/ansible/module_utils/net_tools/netbox/netbox_utils.py deleted file mode 100644 index 6c37eded415..00000000000 --- a/lib/ansible/module_utils/net_tools/netbox/netbox_utils.py +++ /dev/null @@ -1,357 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: (c) 2018, Mikhail Yohman (@fragmentedpacket) -# Copyright: (c) 2018, David Gomez (@amb1s1) -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -__metaclass__ = type - -API_APPS_ENDPOINTS = dict( - circuits=[], - dcim=[ - "devices", - "device_roles", - "device_types", - "devices", - "interfaces", - "platforms", - "racks", - "regions", - "sites", - ], - extras=[], - ipam=["ip_addresses", "prefixes", "roles", "vlans", "vlan_groups", "vrfs"], - secrets=[], - tenancy=["tenants", "tenant_groups"], - virtualization=["clusters"], -) - -QUERY_TYPES = dict( - cluster="name", - devices="name", - device_role="slug", - device_type="slug", - manufacturer="slug", - nat_inside="address", - nat_outside="address", - platform="slug", - primary_ip="address", - primary_ip4="address", - primary_ip6="address", - rack="slug", - region="slug", - role="slug", - site="slug", - tenant="name", - tenant_group="slug", - time_zone="timezone", - vlan="name", - vlan_group="slug", - vrf="name", -) - -CONVERT_TO_ID = dict( - cluster="clusters", - device="devices", - device_role="device_roles", - device_type="device_types", - interface="interfaces", - lag="interfaces", - nat_inside="ip_addresses", - nat_outside="ip_addresses", - platform="platforms", - primary_ip="ip_addresses", - primary_ip4="ip_addresses", - primary_ip6="ip_addresses", - rack="racks", - region="regions", - role="roles", - site="sites", - tagged_vlans="vlans", - tenant="tenants", - tenant_group="tenant_groups", - untagged_vlan="vlans", - vlan="vlans", - vlan_group="vlan_groups", - vrf="vrfs", -) - -FACE_ID = dict(front=0, rear=1) - -NO_DEFAULT_ID = set( - [ - "device", - "lag", - "primary_ip", - "primary_ip4", - "primary_ip6", - "role", - "vlan", - "vrf", - "nat_inside", - "nat_outside", - "region", - "untagged_vlan", - "tagged_vlans", - "tenant", - ] -) - -DEVICE_STATUS = dict(offline=0, active=1, planned=2, staged=3, failed=4, inventory=5) - -IP_ADDRESS_STATUS = dict(active=1, reserved=2, deprecated=3, dhcp=5) - -IP_ADDRESS_ROLE = dict( - loopback=10, secondary=20, anycast=30, vip=40, vrrp=41, hsrp=42, glbp=43, carp=44 -) - -PREFIX_STATUS = dict(container=0, active=1, reserved=2, deprecated=3) - -VLAN_STATUS = dict(active=1, reserved=2, deprecated=3) - -SITE_STATUS = dict(active=1, planned=2, retired=4) - -INTF_FORM_FACTOR = { - "virtual": 0, - "link aggregation group (lag)": 200, - "100base-tx (10/100me)": 800, - "1000base-t (1ge)": 1000, - "10gbase-t (10ge)": 1150, - "10gbase-cx4 (10ge)": 1170, - "gbic (1ge)": 1050, - "sfp (1ge)": 1100, - "sfp+ (10ge)": 1200, - "xfp (10ge)": 1300, - "xenpak (10ge)": 1310, - "x2 (10ge)": 1320, - "sfp28 (25ge)": 1350, - "qsfp+ (40ge)": 1400, - "cfp (100ge)": 1500, - "cfp2 (100ge)": 1510, - "cfp2 (200ge)": 1650, - "cfp4 (100ge)": 1520, - "cisco cpak (100ge)": 1550, - "qsfp28 (100ge)": 1600, - "qsfp56 (200ge)": 1700, - "qsfp-dd (400ge)": 1750, - "ieee 802.11a": 2600, - "ieee 802.11b/g": 2610, - "ieee 802.11n": 2620, - "ieee 802.11ac": 2630, - "ieee 802.11ad": 2640, - "gsm": 2810, - "cdma": 2820, - "lte": 2830, - "oc-3/stm-1": 6100, - "oc-12/stm-4": 6200, - "oc-48/stm-16": 6300, - "oc-192/stm-64": 6400, - "oc-768/stm-256": 6500, - "oc-1920/stm-640": 6600, - "oc-3840/stm-1234": 6700, - "sfp (1gfc)": 3010, - "sfp (2gfc)": 3020, - "sfp (4gfc)": 3040, - "sfp+ (8gfc)": 3080, - "sfp+ (16gfc)": 3160, - "sfp28 (32gfc)": 3320, - "qsfp28 (128gfc)": 3400, - "t1 (1.544 mbps)": 4000, - "e1 (2.048 mbps)": 4010, - "t3 (45 mbps)": 4040, - "e3 (34 mbps)": 4050, - "cisco stackwise": 5000, - "cisco stackwise plus": 5050, - "cisco flexstack": 5100, - "cisco flexstack plus": 5150, - "juniper vcp": 5200, - "extreme summitstack": 5300, - "extreme summitstack-128": 5310, - "extreme summitstack-256": 5320, - "extreme summitstack-512": 5330, - "other": 32767, -} - -INTF_MODE = {"access": 100, "tagged": 200, "tagged all": 300} - -ALLOWED_QUERY_PARAMS = { - "interface": set(["name", "device"]), - "lag": set(["name"]), - "nat_inside": set(["vrf", "address"]), - "vlan": set(["name", "site", "vlan_group", "tenant"]), - "untagged_vlan": set(["name", "site", "vlan_group", "tenant"]), - "tagged_vlans": set(["name", "site", "vlan_group", "tenant"]), -} - -QUERY_PARAMS_IDS = set(["vrf", "site", "vlan_group", "tenant"]) - - -def _build_diff(before=None, after=None): - return {"before": before, "after": after} - - -def create_netbox_object(nb_endpoint, data, check_mode): - """Create a Netbox object. - :returns tuple(serialized_nb_obj, diff): tuple of the serialized created - Netbox object and the Ansible diff. - """ - if check_mode: - serialized_nb_obj = data - else: - nb_obj = nb_endpoint.create(data) - try: - serialized_nb_obj = nb_obj.serialize() - except AttributeError: - serialized_nb_obj = nb_obj - - diff = _build_diff(before={"state": "absent"}, after={"state": "present"}) - return serialized_nb_obj, diff - - -def delete_netbox_object(nb_obj, check_mode): - """Delete a Netbox object. - :returns tuple(serialized_nb_obj, diff): tuple of the serialized deleted - Netbox object and the Ansible diff. - """ - if not check_mode: - nb_obj.delete() - - diff = _build_diff(before={"state": "present"}, after={"state": "absent"}) - return nb_obj.serialize(), diff - - -def update_netbox_object(nb_obj, data, check_mode): - """Update a Netbox object. - :returns tuple(serialized_nb_obj, diff): tuple of the serialized updated - Netbox object and the Ansible diff. - """ - serialized_nb_obj = nb_obj.serialize() - updated_obj = serialized_nb_obj.copy() - updated_obj.update(data) - if serialized_nb_obj == updated_obj: - return serialized_nb_obj, None - else: - data_before, data_after = {}, {} - for key in data: - if serialized_nb_obj[key] != updated_obj[key]: - data_before[key] = serialized_nb_obj[key] - data_after[key] = updated_obj[key] - - if not check_mode: - nb_obj.update(data) - updated_obj = nb_obj.serialize() - - diff = _build_diff(before=data_before, after=data_after) - return updated_obj, diff - - -def _get_query_param_id(nb, match, child): - endpoint = CONVERT_TO_ID[match] - app = find_app(endpoint) - nb_app = getattr(nb, app) - nb_endpoint = getattr(nb_app, endpoint) - result = nb_endpoint.get(**{QUERY_TYPES.get(match): child[match]}) - if result: - return result.id - else: - return child - - -def find_app(endpoint): - for k, v in API_APPS_ENDPOINTS.items(): - if endpoint in v: - nb_app = k - return nb_app - - -def build_query_params(nb, parent, module_data, child): - query_dict = dict() - query_params = ALLOWED_QUERY_PARAMS.get(parent) - matches = query_params.intersection(set(child.keys())) - for match in matches: - if match in QUERY_PARAMS_IDS: - value = _get_query_param_id(nb, match, child) - query_dict.update({match + "_id": value}) - else: - value = child.get(match) - query_dict.update({match: value}) - - if parent == "lag": - query_dict.update({"form_factor": 200}) - if isinstance(module_data["device"], int): - query_dict.update({"device_id": module_data["device"]}) - else: - query_dict.update({"device": module_data["device"]}) - - return query_dict - - -def find_ids(nb, data): - for k, v in data.items(): - if k in CONVERT_TO_ID: - endpoint = CONVERT_TO_ID[k] - search = v - app = find_app(endpoint) - nb_app = getattr(nb, app) - nb_endpoint = getattr(nb_app, endpoint) - - if isinstance(v, dict): - query_params = build_query_params(nb, k, data, v) - query_id = nb_endpoint.get(**query_params) - - elif isinstance(v, list): - id_list = list() - for index in v: - norm_data = normalize_data(index) - temp_dict = build_query_params(nb, k, data, norm_data) - query_id = nb_endpoint.get(**temp_dict) - if query_id: - id_list.append(query_id.id) - else: - return ValueError("%s not found" % (index)) - - else: - try: - query_id = nb_endpoint.get(**{QUERY_TYPES.get(k, "q"): search}) - except ValueError: - raise ValueError( - "Multiple results found while searching for key: %s" % (k) - ) - - if isinstance(v, list): - data[k] = id_list - elif query_id: - data[k] = query_id.id - elif k in NO_DEFAULT_ID: - pass - else: - raise ValueError("Could not resolve id of %s: %s" % (k, v)) - - return data - - -def normalize_data(data): - for k, v in data.items(): - if isinstance(v, dict): - for subk, subv in v.items(): - sub_data_type = QUERY_TYPES.get(subk, "q") - if sub_data_type == "slug": - if "-" in subv: - data[k][subk] = subv.replace(" ", "").lower() - elif " " in subv: - data[k][subk] = subv.replace(" ", "-").lower() - else: - data[k][subk] = subv.lower() - else: - data_type = QUERY_TYPES.get(k, "q") - if data_type == "slug": - if "-" in v: - data[k] = v.replace(" ", "").lower() - elif " " in v: - data[k] = v.replace(" ", "-").lower() - else: - data[k] = v.lower() - elif data_type == "timezone": - if " " in v: - data[k] = v.replace(" ", "_") - - return data diff --git a/lib/ansible/modules/net_tools/netbox/netbox_device.py b/lib/ansible/modules/net_tools/netbox/netbox_device.py deleted file mode 100644 index d74dac6af9f..00000000000 --- a/lib/ansible/modules/net_tools/netbox/netbox_device.py +++ /dev/null @@ -1,312 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) -# Copyright: (c) 2018, David Gomez (@amb1s1) -# 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: netbox_device -short_description: Create, update or delete devices within Netbox -description: - - Creates, updates or removes devices from Netbox -notes: - - Tags should be defined as a YAML list - - This should be ran with connection C(local) and hosts C(localhost) -author: - - Mikhail Yohman (@FragmentedPacket) - - David Gomez (@amb1s1) -requirements: - - pynetbox -version_added: '2.8' -options: - netbox_url: - description: - - URL of the Netbox instance resolvable by Ansible control host - required: true - netbox_token: - description: - - The token created within Netbox to authorize API access - required: true - data: - description: - - Defines the device configuration - suboptions: - name: - description: - - The name of the device - required: true - device_type: - description: - - Required if I(state=present) and the device does not exist yet - device_role: - description: - - Required if I(state=present) and the device does not exist yet - tenant: - description: - - The tenant that the device will be assigned to - platform: - description: - - The platform of the device - serial: - description: - - Serial number of the device - asset_tag: - description: - - Asset tag that is associated to the device - site: - description: - - Required if I(state=present) and the device does not exist yet - rack: - description: - - The name of the rack to assign the device to - position: - description: - - The position of the device in the rack defined above - face: - description: - - Required if I(rack) is defined - status: - description: - - The status of the device - choices: - - Active - - Offline - - Planned - - Staged - - Failed - - Inventory - cluster: - description: - - Cluster that the device will be assigned to - comments: - description: - - Comments that may include additional information in regards to the device - tags: - description: - - Any tags that the device may need to be associated with - custom_fields: - description: - - must exist in Netbox - required: true - state: - description: - - Use C(present) or C(absent) for adding or removing. - choices: [ absent, present ] - default: present - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. - default: 'yes' - type: bool -''' - -EXAMPLES = r''' -- name: "Test Netbox modules" - connection: local - hosts: localhost - gather_facts: False - - tasks: - - name: Create device within Netbox with only required information - netbox_device: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - name: Test Device - device_type: C9410R - device_role: Core Switch - site: Main - state: present - - - name: Delete device within netbox - netbox_device: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - name: Test Device - state: absent - - - name: Create device with tags - netbox_device: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - name: Another Test Device - device_type: C9410R - device_role: Core Switch - site: Main - tags: - - Schnozzberry - state: present - - - name: Update the rack and position of an existing device - netbox_device: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - name: Test Device - rack: Test Rack - position: 10 - face: Front - state: present -''' - -RETURN = r''' -device: - 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 -''' - -import json -import traceback - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.net_tools.netbox.netbox_utils import ( - find_ids, - normalize_data, - create_netbox_object, - delete_netbox_object, - update_netbox_object, - DEVICE_STATUS, - FACE_ID -) - -PYNETBOX_IMP_ERR = None -try: - import pynetbox - HAS_PYNETBOX = True -except ImportError: - PYNETBOX_IMP_ERR = traceback.format_exc() - HAS_PYNETBOX = False - - -def main(): - ''' - Main entry point for module execution - ''' - argument_spec = dict( - netbox_url=dict(type="str", required=True), - netbox_token=dict(type="str", required=True, no_log=True), - data=dict(type="dict", required=True), - state=dict(required=False, default='present', choices=['present', 'absent']), - validate_certs=dict(type="bool", default=True) - ) - - global module - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - - # Fail module if pynetbox is not installed - if not HAS_PYNETBOX: - module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR) - - # Fail if device name is not given - if not module.params["data"].get("name"): - module.fail_json(msg="missing device name") - - # Assign variables to be used with module - app = 'dcim' - endpoint = 'devices' - url = module.params["netbox_url"] - token = module.params["netbox_token"] - data = module.params["data"] - state = module.params["state"] - validate_certs = module.params["validate_certs"] - - # Attempt to create Netbox API object - try: - nb = pynetbox.api(url, token=token, ssl_verify=validate_certs) - except Exception: - module.fail_json(msg="Failed to establish connection to Netbox API") - try: - nb_app = getattr(nb, app) - except AttributeError: - module.fail_json(msg="Incorrect application specified: %s" % (app)) - - nb_endpoint = getattr(nb_app, endpoint) - norm_data = normalize_data(data) - try: - if 'present' in state: - result = ensure_device_present(nb, nb_endpoint, norm_data) - else: - result = ensure_device_absent(nb_endpoint, norm_data) - return module.exit_json(**result) - except pynetbox.RequestError as e: - return module.fail_json(msg=json.loads(e.error)) - except ValueError as e: - return module.fail_json(msg=str(e)) - - -def _find_ids(nb, data): - if data.get("status"): - data["status"] = DEVICE_STATUS.get(data["status"].lower()) - if data.get("face"): - data["face"] = FACE_ID.get(data["face"].lower()) - return find_ids(nb, data) - - -def ensure_device_present(nb, nb_endpoint, normalized_data): - ''' - :returns dict(device, msg, changed, diff): dictionary resulting of the request, - where `device` is the serialized device fetched or newly created in - Netbox - ''' - data = _find_ids(nb, normalized_data) - nb_device = nb_endpoint.get(name=data["name"]) - result = {} - if not nb_device: - device, diff = create_netbox_object(nb_endpoint, data, module.check_mode) - msg = "Device %s created" % (data["name"]) - changed = True - result["diff"] = diff - else: - device, diff = update_netbox_object(nb_device, data, module.check_mode) - if device is False: - module.fail_json( - msg="Request failed, couldn't update device: %s" % data["name"] - ) - if diff: - msg = "Device %s updated" % (data["name"]) - changed = True - result["diff"] = diff - else: - msg = "Device %s already exists" % (data["name"]) - changed = False - result.update({"device": device, "changed": changed, "msg": msg}) - return result - - -def ensure_device_absent(nb_endpoint, data): - ''' - :returns dict(msg, changed, diff) - ''' - nb_device = nb_endpoint.get(name=data["name"]) - result = {} - if nb_device: - dummy, diff = delete_netbox_object(nb_device, module.check_mode) - msg = 'Device %s deleted' % (data["name"]) - changed = True - result["diff"] = diff - else: - msg = 'Device %s already absent' % (data["name"]) - changed = False - - result.update({"changed": changed, "msg": msg}) - return result - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/net_tools/netbox/netbox_interface.py b/lib/ansible/modules/net_tools/netbox/netbox_interface.py deleted file mode 100644 index ae6189416b1..00000000000 --- a/lib/ansible/modules/net_tools/netbox/netbox_interface.py +++ /dev/null @@ -1,351 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) -# 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 - -ANSIBLE_METADATA = {"metadata_version": "1.1", - "status": ["preview"], - "supported_by": "community"} - -DOCUMENTATION = r""" ---- -module: netbox_interface -short_description: Creates or removes interfaces from Netbox -description: - - Creates or removes interfaces from Netbox -notes: - - Tags should be defined as a YAML list - - This should be ran with connection C(local) and hosts C(localhost) -author: - - Mikhail Yohman (@FragmentedPacket) -requirements: - - pynetbox -version_added: "2.8" -options: - netbox_url: - description: - - URL of the Netbox instance resolvable by Ansible control host - required: true - type: str - netbox_token: - description: - - The token created within Netbox to authorize API access - required: true - type: str - data: - description: - - Defines the prefix configuration - suboptions: - device: - description: - - Name of the device the interface will be associated with (case-sensitive) - required: true - type: str - name: - description: - - Name of the interface to be created - required: true - type: str - form_factor: - description: - - | - Form factor of the interface: - ex. 1000Base-T (1GE), Virtual, 10GBASE-T (10GE) - This has to be specified exactly as what is found within UI - type: str - enabled: - description: - - Sets whether interface shows enabled or disabled - type: bool - lag: - description: - - Parent LAG interface will be a member of - type: dict - mtu: - description: - - The MTU of the interface - type: str - mac_address: - description: - - The MAC address of the interface - type: str - mgmt_only: - description: - - This interface is used only for out-of-band management - type: bool - description: - description: - - The description of the prefix - type: str - mode: - description: - - The mode of the interface - choices: - - Access - - Tagged - - Tagged All - type: str - untagged_vlan: - description: - - The untagged VLAN to be assigned to interface - type: dict - tagged_vlans: - description: - - A list of tagged VLANS to be assigned to interface. Mode must be set to either C(Tagged) or C(Tagged All) - type: list - tags: - description: - - Any tags that the prefix may need to be associated with - type: list - required: true - state: - description: - - Use C(present) or C(absent) for adding or removing. - choices: [ absent, present ] - default: present - type: str - validate_certs: - description: - - | - If C(no), SSL certificates will not be validated. - This should only be used on personally controlled sites using self-signed certificates. - default: "yes" - type: bool -""" - -EXAMPLES = r""" -- name: "Test Netbox interface module" - connection: local - hosts: localhost - gather_facts: False - tasks: - - name: Create interface within Netbox with only required information - netbox_interface: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - device: test100 - name: GigabitEthernet1 - state: present - - name: Delete interface within netbox - netbox_interface: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - device: test100 - name: GigabitEthernet1 - state: absent - - name: Create LAG with several specified options - netbox_interface: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - device: test100 - name: port-channel1 - form_factor: Link Aggregation Group (LAG) - mtu: 1600 - mgmt_only: false - mode: Access - state: present - - name: Create interface and assign it to parent LAG - netbox_interface: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - device: test100 - name: GigabitEthernet1 - enabled: false - form_factor: 1000Base-t (1GE) - lag: - name: port-channel1 - mtu: 1600 - mgmt_only: false - mode: Access - state: present - - name: Create interface as a trunk port - netbox_interface: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - device: test100 - name: GigabitEthernet25 - enabled: false - form_factor: 1000Base-t (1GE) - untagged_vlan: - name: Wireless - site: Test Site - tagged_vlans: - - name: Data - site: Test Site - - name: VoIP - site: Test Site - mtu: 1600 - mgmt_only: true - mode: Tagged - state: present -""" - -RETURN = r""" -interface: - 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 -""" - -import json -import traceback - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.net_tools.netbox.netbox_utils import ( - find_ids, - normalize_data, - create_netbox_object, - delete_netbox_object, - update_netbox_object, - INTF_FORM_FACTOR, - INTF_MODE, -) -from ansible.module_utils.compat import ipaddress -from ansible.module_utils._text import to_text - - -PYNETBOX_IMP_ERR = None -try: - import pynetbox - HAS_PYNETBOX = True -except ImportError: - PYNETBOX_IMP_ERR = traceback.format_exc() - HAS_PYNETBOX = False - - -def main(): - """ - Main entry point for module execution - """ - argument_spec = dict( - netbox_url=dict(type="str", required=True), - netbox_token=dict(type="str", required=True, no_log=True), - data=dict(type="dict", required=True), - state=dict(required=False, default="present", choices=["present", "absent"]), - validate_certs=dict(type="bool", default=True) - ) - - global module - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - - # Fail module if pynetbox is not installed - if not HAS_PYNETBOX: - module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR) - # Assign variables to be used with module - app = "dcim" - endpoint = "interfaces" - url = module.params["netbox_url"] - token = module.params["netbox_token"] - data = module.params["data"] - state = module.params["state"] - validate_certs = module.params["validate_certs"] - # Attempt to create Netbox API object - try: - nb = pynetbox.api(url, token=token, ssl_verify=validate_certs) - except Exception: - module.fail_json(msg="Failed to establish connection to Netbox API") - try: - nb_app = getattr(nb, app) - except AttributeError: - module.fail_json(msg="Incorrect application specified: %s" % (app)) - nb_endpoint = getattr(nb_app, endpoint) - norm_data = normalize_data(data) - try: - norm_data = _check_and_adapt_data(nb, norm_data) - - if "present" in state: - return module.exit_json( - **ensure_interface_present(nb, nb_endpoint, norm_data) - ) - else: - return module.exit_json( - **ensure_interface_absent(nb, nb_endpoint, norm_data) - ) - except pynetbox.RequestError as e: - return module.fail_json(msg=json.loads(e.error)) - except ValueError as e: - return module.fail_json(msg=str(e)) - except AttributeError as e: - return module.fail_json(msg=str(e)) - - -def _check_and_adapt_data(nb, data): - data = find_ids(nb, data) - - if data.get("form_factor"): - data["form_factor"] = INTF_FORM_FACTOR.get(data["form_factor"].lower()) - if data.get("mode"): - data["mode"] = INTF_MODE.get(data["mode"].lower()) - - return data - - -def ensure_interface_present(nb, nb_endpoint, data): - """ - :returns dict(interface, msg, changed): dictionary resulting of the request, - where 'interface' is the serialized interface fetched or newly created in Netbox - """ - - if not isinstance(data, dict): - changed = False - return {"msg": data, "changed": changed} - - nb_intf = nb_endpoint.get(name=data["name"], device_id=data["device"]) - result = dict() - - if not nb_intf: - intf, diff = create_netbox_object(nb_endpoint, data, module.check_mode) - changed = True - msg = "Interface %s created" % (data["name"]) - else: - intf, diff = update_netbox_object(nb_intf, data, module.check_mode) - if intf is False: - module.fail_json( - msg="Request failed, couldn't update device: %s" % (data["name"]) - ) - if diff: - msg = "Interface %s updated" % (data["name"]) - changed = True - result["diff"] = diff - else: - msg = "Interface %s already exists" % (data["name"]) - changed = False - result.update({"interface": intf, "msg": msg, "changed": changed}) - return result - - -def ensure_interface_absent(nb, nb_endpoint, data): - """ - :returns dict(msg, changed, diff) - """ - nb_intf = nb_endpoint.get(name=data["name"], device_id=data["device"]) - result = dict() - if nb_intf: - dummy, diff = delete_netbox_object(nb_intf, module.check_mode) - changed = True - msg = "Interface %s deleted" % (data["name"]) - result["diff"] = diff - else: - msg = "Interface %s already absent" % (data["name"]) - changed = False - - result.update({"msg": msg, "changed": changed}) - return result - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/net_tools/netbox/netbox_ip_address.py b/lib/ansible/modules/net_tools/netbox/netbox_ip_address.py deleted file mode 100644 index 793b9e196da..00000000000 --- a/lib/ansible/modules/net_tools/netbox/netbox_ip_address.py +++ /dev/null @@ -1,517 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) -# 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: netbox_ip_address -short_description: Creates or removes IP addresses from Netbox -description: - - Creates or removes IP 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: - - Mikhail Yohman (@FragmentedPacket) - - Anthony Ruhier (@Anthony25) -requirements: - - pynetbox -version_added: '2.8' -options: - netbox_url: - description: - - URL of the Netbox instance resolvable by Ansible control host - required: true - netbox_token: - description: - - The token created within Netbox to authorize API access - required: true - data: - description: - - Defines the IP address configuration - suboptions: - family: - description: - - Specifies with address family the IP address belongs to - choices: - - 4 - - 6 - address: - description: - - Required if state is C(present) - prefix: - description: - - | - With state C(present), if an interface is given, it will ensure - that an IP inside this prefix (and vrf, if given) is attached - to this interface. Otherwise, it will get the next available IP - of this prefix and attach it. - With state C(new), it will force to get the next available IP in - this prefix. If an interface is given, it will also force to attach - it. - Required if state is C(present) or C(new) when no address is given. - Unused if an address is specified. - vrf: - description: - - VRF that IP address is associated with - tenant: - description: - - The tenant that the device will be assigned to - status: - description: - - The status of the IP address - choices: - - Active - - Reserved - - Deprecated - - DHCP - role: - description: - - The role of the IP address - choices: - - Loopback - - Secondary - - Anycast - - VIP - - VRRP - - HSRP - - GLBP - - CARP - interface: - description: - - | - The name and device of the interface that the IP address should be assigned to - Required if state is C(present) and a prefix specified. - description: - description: - - The description of the interface - nat_inside: - description: - - The inside IP address this IP is assigned to - tags: - description: - - Any tags that the IP address may need to be associated with - custom_fields: - description: - - must exist in Netbox - required: true - state: - description: - - | - Use C(present), C(new) or C(absent) for adding, force adding or removing. - C(present) will check if the IP is already created, and return it if - true. C(new) will force to create it anyway (useful for anycasts, for - example). - choices: [ absent, new, present ] - default: present - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. - default: 'yes' - type: bool -''' - -EXAMPLES = r''' -- name: "Test Netbox IP address module" - connection: local - hosts: localhost - gather_facts: False - - tasks: - - name: Create IP address within Netbox with only required information - netbox_ip_address: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - address: 192.168.1.10 - state: present - - name: Force to create (even if it already exists) the IP - netbox_ip_address: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - address: 192.168.1.10 - state: new - - name: Get a new available IP inside 192.168.1.0/24 - netbox_ip_address: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - prefix: 192.168.1.0/24 - state: new - - name: Delete IP address within netbox - netbox_ip_address: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - address: 192.168.1.10 - state: absent - - name: Create IP address with several specified options - netbox_ip_address: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - family: 4 - address: 192.168.1.20 - vrf: Test - tenant: Test Tenant - status: Reserved - role: Loopback - description: Test description - tags: - - Schnozzberry - state: present - - name: Create IP address and assign a nat_inside IP - netbox_ip_address: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - family: 4 - address: 192.168.1.30 - vrf: Test - nat_inside: - address: 192.168.1.20 - vrf: Test - interface: - name: GigabitEthernet1 - device: test100 - - name: Ensure that an IP inside 192.168.1.0/24 is attached to GigabitEthernet1 - netbox_ip_address: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - prefix: 192.168.1.0/24 - vrf: Test - interface: - name: GigabitEthernet1 - device: test100 - state: present - - name: Attach a new available IP of 192.168.1.0/24 to GigabitEthernet1 - netbox_ip_address: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - prefix: 192.168.1.0/24 - vrf: Test - interface: - name: GigabitEthernet1 - device: test100 - state: new -''' - -RETURN = r''' -ip_address: - 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 -''' - -import json -import traceback - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.net_tools.netbox.netbox_utils import ( - find_ids, - normalize_data, - create_netbox_object, - delete_netbox_object, - update_netbox_object, - IP_ADDRESS_ROLE, - IP_ADDRESS_STATUS -) -from ansible.module_utils.compat import ipaddress -from ansible.module_utils._text import to_text - - -PYNETBOX_IMP_ERR = None -try: - import pynetbox - HAS_PYNETBOX = True -except ImportError: - PYNETBOX_IMP_ERR = traceback.format_exc() - HAS_PYNETBOX = False - - -def main(): - ''' - Main entry point for module execution - ''' - argument_spec = dict( - netbox_url=dict(type="str", required=True), - netbox_token=dict(type="str", required=True, no_log=True), - data=dict(type="dict", required=True), - state=dict(required=False, default='present', choices=['present', 'absent', 'new']), - validate_certs=dict(type="bool", default=True) - ) - - global module - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - - # Fail module if pynetbox is not installed - if not HAS_PYNETBOX: - module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR) - - # Assign variables to be used with module - changed = False - app = 'ipam' - endpoint = 'ip_addresses' - url = module.params["netbox_url"] - token = module.params["netbox_token"] - data = module.params["data"] - state = module.params["state"] - validate_certs = module.params["validate_certs"] - - # Attempt to create Netbox API object - try: - nb = pynetbox.api(url, token=token, ssl_verify=validate_certs) - except Exception: - module.fail_json(msg="Failed to establish connection to Netbox API") - try: - nb_app = getattr(nb, app) - except AttributeError: - module.fail_json(msg="Incorrect application specified: %s" % (app)) - - nb_endpoint = getattr(nb_app, endpoint) - norm_data = normalize_data(data) - try: - norm_data = _check_and_adapt_data(nb, norm_data) - if state in ("new", "present"): - return _handle_state_new_present( - module, state, nb_app, nb_endpoint, norm_data - ) - elif state == "absent": - return module.exit_json( - **ensure_ip_address_absent(nb_endpoint, norm_data) - ) - else: - return module.fail_json(msg="Invalid state %s" % state) - except pynetbox.RequestError as e: - return module.fail_json(msg=json.loads(e.error)) - except ValueError as e: - return module.fail_json(msg=str(e)) - - -def _check_and_adapt_data(nb, data): - data = find_ids(nb, data) - - if data.get("vrf") and not isinstance(data["vrf"], int): - raise ValueError( - "%s does not exist - Please create VRF" % (data["vrf"]) - ) - if data.get("status"): - data["status"] = IP_ADDRESS_STATUS.get(data["status"].lower()) - if data.get("role"): - data["role"] = IP_ADDRESS_ROLE.get(data["role"].lower()) - - return data - - -def _handle_state_new_present(module, state, nb_app, nb_endpoint, data): - if data.get("address"): - if state == "present": - return module.exit_json( - **ensure_ip_address_present(nb_endpoint, data) - ) - elif state == "new": - return module.exit_json( - **create_ip_address(nb_endpoint, data) - ) - else: - if state == "present": - return module.exit_json( - **ensure_ip_in_prefix_present_on_netif( - nb_app, nb_endpoint, data - ) - ) - elif state == "new": - return module.exit_json( - **get_new_available_ip_address(nb_app, data) - ) - - -def ensure_ip_address_present(nb_endpoint, data): - """ - :returns dict(ip_address, msg, changed): dictionary resulting of the request, - where 'ip_address' is the serialized ip fetched or newly created in Netbox - """ - if not isinstance(data, dict): - changed = False - return {"msg": data, "changed": changed} - - try: - nb_addr = _search_ip(nb_endpoint, data) - except ValueError: - return _error_multiple_ip_results(data) - - result = {} - if not nb_addr: - return create_ip_address(nb_endpoint, data) - else: - ip_addr, diff = update_netbox_object(nb_addr, data, module.check_mode) - if ip_addr is False: - module.fail_json( - msg="Request failed, couldn't update IP: %s" % (data["address"]) - ) - if diff: - msg = "IP Address %s updated" % (data["address"]) - changed = True - result["diff"] = diff - else: - ip_addr = nb_addr.serialize() - changed = False - msg = "IP Address %s already exists" % (data["address"]) - - return {"ip_address": ip_addr, "msg": msg, "changed": changed} - - -def _search_ip(nb_endpoint, data): - get_query_params = {"address": data["address"]} - if data.get("vrf"): - get_query_params["vrf_id"] = data["vrf"] - - ip_addr = nb_endpoint.get(**get_query_params) - return ip_addr - - -def _error_multiple_ip_results(data): - changed = False - if "vrf" in data: - return {"msg": "Returned more than result", "changed": changed} - else: - return { - "msg": "Returned more than one result - Try specifying VRF.", - "changed": changed - } - - -def create_ip_address(nb_endpoint, data): - if not isinstance(data, dict): - changed = False - return {"msg": data, "changed": changed} - - ip_addr, diff = create_netbox_object(nb_endpoint, data, module.check_mode) - changed = True - msg = "IP Addresses %s created" % (data["address"]) - - return {"ip_address": ip_addr, "msg": msg, "changed": changed, "diff": diff} - - -def ensure_ip_in_prefix_present_on_netif(nb_app, nb_endpoint, data): - """ - :returns dict(ip_address, msg, changed): dictionary resulting of the request, - where 'ip_address' is the serialized ip fetched or newly created in Netbox - """ - if not isinstance(data, dict): - changed = False - return {"msg": data, "changed": changed} - - if not data.get("interface") or not data.get("prefix"): - raise ValueError("A prefix and interface are required") - - get_query_params = { - "interface_id": data["interface"], "parent": data["prefix"], - } - if data.get("vrf"): - get_query_params["vrf_id"] = data["vrf"] - - attached_ips = nb_endpoint.filter(**get_query_params) - if attached_ips: - ip_addr = attached_ips[-1].serialize() - changed = False - msg = "IP Address %s already attached" % (ip_addr["address"]) - - return {"ip_address": ip_addr, "msg": msg, "changed": changed} - else: - return get_new_available_ip_address(nb_app, data) - - -def get_new_available_ip_address(nb_app, data): - prefix_query = {"prefix": data["prefix"]} - if data.get("vrf"): - prefix_query["vrf_id"] = data["vrf"] - - result = {} - prefix = nb_app.prefixes.get(**prefix_query) - if not prefix: - changed = False - msg = "%s does not exist - please create first" % (data["prefix"]) - return {"msg": msg, "changed": changed} - elif prefix.available_ips.list(): - ip_addr, diff = create_netbox_object(prefix.available_ips, data, module.check_mode) - changed = True - msg = "IP Addresses %s created" % (ip_addr["address"]) - result["diff"] = diff - else: - changed = False - msg = "No available IPs available within %s" % (data['prefix']) - return {"msg": msg, "changed": changed} - - result.update({"ip_address": ip_addr, "msg": msg, "changed": changed}) - return result - - -def _get_prefix_id(nb_app, prefix, vrf_id=None): - ipaddr_prefix = ipaddress.ip_network(prefix) - network = to_text(ipaddr_prefix.network_address) - mask = ipaddr_prefix.prefixlen - - prefix_query_params = { - "prefix": network, - "mask_length": mask - } - if vrf_id: - prefix_query_params["vrf_id"] = vrf_id - - prefix_id = nb_app.prefixes.get(prefix_query_params) - if not prefix_id: - if vrf_id: - raise ValueError("Prefix %s does not exist in VRF %s - Please create it" % (prefix, vrf_id)) - else: - raise ValueError("Prefix %s does not exist - Please create it" % (prefix)) - - return prefix_id - - -def ensure_ip_address_absent(nb_endpoint, data): - """ - :returns dict(msg, changed) - """ - if not isinstance(data, dict): - changed = False - return {"msg": data, "changed": changed} - - try: - ip_addr = _search_ip(nb_endpoint, data) - except ValueError: - return _error_multiple_ip_results(data) - - result = {} - if ip_addr: - dummy, diff = delete_netbox_object(ip_addr, module.check_mode) - changed = True - msg = "IP Address %s deleted" % (data["address"]) - result["diff"] = diff - else: - changed = False - msg = "IP Address %s already absent" % (data["address"]) - - result.update({"msg": msg, "changed": changed}) - return result - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/net_tools/netbox/netbox_prefix.py b/lib/ansible/modules/net_tools/netbox/netbox_prefix.py deleted file mode 100644 index 58105d1e079..00000000000 --- a/lib/ansible/modules/net_tools/netbox/netbox_prefix.py +++ /dev/null @@ -1,461 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) -# 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 - -ANSIBLE_METADATA = {"metadata_version": "1.1", - "status": ["preview"], - "supported_by": "community"} - -DOCUMENTATION = r""" ---- -module: netbox_prefix -short_description: Creates or removes prefixes from Netbox -description: - - Creates or removes prefixes from Netbox -notes: - - Tags should be defined as a YAML list - - This should be ran with connection C(local) and hosts C(localhost) -author: - - Mikhail Yohman (@FragmentedPacket) - - Anthony Ruhier (@Anthony25) -requirements: - - pynetbox -version_added: '2.8' -options: - netbox_url: - description: - - URL of the Netbox instance resolvable by Ansible control host - required: true - type: str - netbox_token: - description: - - The token created within Netbox to authorize API access - required: true - type: str - data: - description: - - Defines the prefix configuration - suboptions: - family: - description: - - Specifies which address family the prefix prefix belongs to - choices: - - 4 - - 6 - type: int - prefix: - description: - - Required if state is C(present) and first_available is False. Will allocate or free this prefix. - type: str - parent: - description: - - Required if state is C(present) and first_available is C(yes). Will get a new available prefix in this parent prefix. - type: str - prefix_length: - description: - - | - Required ONLY if state is C(present) and first_available is C(yes). - Will get a new available prefix of the given prefix_length in this parent prefix. - type: str - site: - description: - - Site that prefix is associated with - type: str - vrf: - description: - - VRF that prefix is associated with - type: str - tenant: - description: - - The tenant that the prefix will be assigned to - type: str - vlan: - description: - - The VLAN the prefix will be assigned to - type: dict - status: - description: - - The status of the prefix - choices: - - Active - - Container - - Deprecated - - Reserved - type: str - role: - description: - - The role of the prefix - type: str - is_pool: - description: - - All IP Addresses within this prefix are considered usable - type: bool - description: - description: - - The description of the prefix - type: str - tags: - description: - - Any tags that the prefix may need to be associated with - type: list - custom_fields: - description: - - Must exist in Netbox and in key/value format - type: dict - required: true - state: - description: - - Use C(present) or C(absent) for adding or removing. - choices: [ absent, present ] - default: present - first_available: - description: - - If C(yes) and state C(present), if an parent is given, it will get the - first available prefix of the given prefix_length inside the given parent (and - vrf, if given). - Unused with state C(absent). - default: 'no' - type: bool - validate_certs: - description: - - If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. - default: "yes" - type: bool -""" - -EXAMPLES = r""" -- name: "Test Netbox prefix module" - connection: local - hosts: localhost - gather_facts: False - - tasks: - - name: Create prefix within Netbox with only required information - netbox_prefix: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - prefix: 10.156.0.0/19 - state: present - - - name: Delete prefix within netbox - netbox_prefix: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - prefix: 10.156.0.0/19 - state: absent - - - name: Create prefix with several specified options - netbox_prefix: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - family: 4 - prefix: 10.156.32.0/19 - site: Test Site - vrf: Test VRF - tenant: Test Tenant - vlan: - name: Test VLAN - site: Test Site - tenant: Test Tenant - vlan_group: Test Vlan Group - status: Reserved - role: Network of care - description: Test description - is_pool: true - tags: - - Schnozzberry - state: present - - - name: Get a new /24 inside 10.156.0.0/19 within Netbox - Parent doesn't exist - netbox_prefix: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - parent: 10.156.0.0/19 - prefix_length: 24 - state: present - first_available: yes - - - name: Create prefix within Netbox with only required information - netbox_prefix: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - prefix: 10.156.0.0/19 - state: present - - - name: Get a new /24 inside 10.156.0.0/19 within Netbox - netbox_prefix: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - parent: 10.156.0.0/19 - prefix_length: 24 - state: present - first_available: yes - - - name: Get a new /24 inside 10.157.0.0/19 within Netbox with additional values - netbox_prefix: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - parent: 10.157.0.0/19 - prefix_length: 24 - vrf: Test VRF - site: Test Site - state: present - first_available: yes -""" - -RETURN = r""" -prefix: - 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 -""" - -import json -import traceback -import re - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.net_tools.netbox.netbox_utils import ( - find_ids, - normalize_data, - create_netbox_object, - delete_netbox_object, - update_netbox_object, - PREFIX_STATUS, -) -from ansible.module_utils.compat import ipaddress -from ansible.module_utils._text import to_text - - -PYNETBOX_IMP_ERR = None -try: - import pynetbox - HAS_PYNETBOX = True -except ImportError: - PYNETBOX_IMP_ERR = traceback.format_exc() - HAS_PYNETBOX = False - - -def main(): - """ - Main entry point for module execution - """ - argument_spec = dict( - netbox_url=dict(type="str", required=True), - netbox_token=dict(type="str", required=True, no_log=True), - data=dict(type="dict", required=True), - state=dict(required=False, default="present", choices=["present", "absent"]), - first_available=dict(type="bool", required=False, default=False), - validate_certs=dict(type="bool", default=True), - ) - - global module - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - - # Fail module if pynetbox is not installed - if not HAS_PYNETBOX: - module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR) - - # Assign variables to be used with module - app = "ipam" - endpoint = "prefixes" - url = module.params["netbox_url"] - token = module.params["netbox_token"] - data = module.params["data"] - state = module.params["state"] - first_available = module.params["first_available"] - validate_certs = module.params["validate_certs"] - - # Attempt to create Netbox API object - try: - nb = pynetbox.api(url, token=token, ssl_verify=validate_certs) - except Exception: - module.fail_json(msg="Failed to establish connection to Netbox API") - try: - nb_app = getattr(nb, app) - except AttributeError: - module.fail_json(msg="Incorrect application specified: %s" % (app)) - nb_endpoint = getattr(nb_app, endpoint) - norm_data = normalize_data(data) - try: - norm_data = _check_and_adapt_data(nb, norm_data) - if "present" in state: - return module.exit_json(**ensure_prefix_present( - nb, nb_endpoint, norm_data, first_available - )) - else: - return module.exit_json( - **ensure_prefix_absent(nb, nb_endpoint, norm_data) - ) - except pynetbox.RequestError as e: - return module.fail_json(msg=json.loads(e.error)) - except ValueError as e: - return module.fail_json(msg=str(e)) - except AttributeError as e: - return module.fail_json(msg=str(e)) - - -def ensure_prefix_present(nb, nb_endpoint, data, first_available=False): - """ - :returns dict(prefix, msg, changed): dictionary resulting of the request, - where 'prefix' is the serialized device fetched or newly created in Netbox - """ - if not isinstance(data, dict): - changed = False - return {"msg": data, "changed": changed} - - if first_available: - for k in ("parent", "prefix_length"): - if k not in data: - raise ValueError("'%s' is required with first_available" % k) - - return get_new_available_prefix(nb_endpoint, data) - else: - if "prefix" not in data: - raise ValueError("'prefix' is required without first_available") - - return get_or_create_prefix(nb_endpoint, data) - - -def _check_and_adapt_data(nb, data): - data = find_ids(nb, data) - - if data.get("vrf") and not isinstance(data["vrf"], int): - raise ValueError( - "%s does not exist - Please create VRF" % (data["vrf"]) - ) - - if data.get("status"): - data["status"] = PREFIX_STATUS.get(data["status"].lower()) - - return data - - -def _search_prefix(nb_endpoint, data): - if data.get("prefix"): - prefix = ipaddress.ip_network(data["prefix"]) - elif data.get("parent"): - prefix = ipaddress.ip_network(data["parent"]) - - network = to_text(prefix.network_address) - mask = prefix.prefixlen - - if data.get("vrf"): - if not isinstance(data["vrf"], int): - raise ValueError("%s does not exist - Please create VRF" % (data["vrf"])) - else: - prefix = nb_endpoint.get(q=network, mask_length=mask, vrf_id=data["vrf"]) - else: - prefix = nb_endpoint.get(q=network, mask_length=mask, vrf="null") - - return prefix - - -def _error_multiple_prefix_results(data): - changed = False - - if data.get("vrf"): - return {"msg": "Returned more than one result", "changed": changed} - else: - return { - "msg": "Returned more than one result - Try specifying VRF.", - "changed": changed - } - - -def get_or_create_prefix(nb_endpoint, data): - try: - nb_prefix = _search_prefix(nb_endpoint, data) - except ValueError: - return _error_multiple_prefix_results(data) - - result = dict() - if not nb_prefix: - prefix, diff = create_netbox_object(nb_endpoint, data, module.check_mode) - changed = True - msg = "Prefix %s created" % (prefix["prefix"]) - result["diff"] = diff - else: - prefix, diff = update_netbox_object(nb_prefix, data, module.check_mode) - if prefix is False: - module.fail_json( - msg="Request failed, couldn't update prefix: %s" % (data["prefix"]) - ) - if diff: - msg = "Prefix %s updated" % (data["prefix"]) - changed = True - result["diff"] = diff - else: - msg = "Prefix %s already exists" % (data["prefix"]) - changed = False - - result.update({"prefix": prefix, "msg": msg, "changed": changed}) - return result - - -def get_new_available_prefix(nb_endpoint, data): - try: - parent_prefix = _search_prefix(nb_endpoint, data) - except ValueError: - return _error_multiple_prefix_results(data) - - result = dict() - if not parent_prefix: - changed = False - msg = "Parent prefix does not exist: %s" % (data["parent"]) - return {"msg": msg, "changed": changed} - elif parent_prefix.available_prefixes.list(): - prefix, diff = create_netbox_object(parent_prefix.available_prefixes, data, module.check_mode) - changed = True - msg = "Prefix %s created" % (prefix["prefix"]) - result["diff"] = diff - else: - changed = False - msg = "No available prefixes within %s" % (data["parent"]) - - result.update({"prefix": prefix, "msg": msg, "changed": changed}) - return result - - -def ensure_prefix_absent(nb, nb_endpoint, data): - """ - :returns dict(msg, changed) - """ - try: - nb_prefix = _search_prefix(nb_endpoint, data) - except ValueError: - return _error_multiple_prefix_results(data) - - result = dict() - if nb_prefix: - dummy, diff = delete_netbox_object(nb_prefix, module.check_mode) - changed = True - msg = "Prefix %s deleted" % (nb_prefix.prefix) - result["diff"] = diff - else: - msg = "Prefix %s already absent" % (data["prefix"]) - changed = False - - result.update({"msg": msg, "changed": changed}) - return result - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/net_tools/netbox/netbox_site.py b/lib/ansible/modules/net_tools/netbox/netbox_site.py deleted file mode 100644 index 7e6c8d8958c..00000000000 --- a/lib/ansible/modules/net_tools/netbox/netbox_site.py +++ /dev/null @@ -1,348 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2018, Mikhail Yohman (@FragmentedPacket) -# 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 - -ANSIBLE_METADATA = {"metadata_version": "1.1", - "status": ["preview"], - "supported_by": "community"} - -DOCUMENTATION = r""" ---- -module: netbox_site -short_description: Creates or removes sites from Netbox -description: - - Creates or removes sites from Netbox -notes: - - Tags should be defined as a YAML list - - This should be ran with connection C(local) and hosts C(localhost) -author: - - Mikhail Yohman (@FragmentedPacket) -requirements: - - pynetbox -version_added: "2.8" -options: - netbox_url: - description: - - URL of the Netbox instance resolvable by Ansible control host - required: true - type: str - netbox_token: - description: - - The token created within Netbox to authorize API access - required: true - type: str - data: - description: - - Defines the site configuration - suboptions: - name: - description: - - Name of the site to be created - required: true - type: str - status: - description: - - Status of the site - choices: - - Active - - Planned - - Retired - type: str - region: - description: - - The region that the site should be associated with - type: str - tenant: - description: - - The tenant the site will be assigned to - type: str - facility: - description: - - Data center provider or facility, ex. Equinix NY7 - type: str - asn: - description: - - The ASN associated with the site - type: int - time_zone: - description: - - Timezone associated with the site, ex. America/Denver - type: str - description: - description: - - The description of the prefix - type: str - physical_address: - description: - - Physical address of site - type: str - shipping_address: - description: - - Shipping address of site - type: str - latitude: - description: - - Latitude in decimal format - type: int - longitude: - description: - - Longitude in decimal format - type: int - contact_name: - description: - - Name of contact for site - type: str - contact_phone: - description: - - Contact phone number for site - type: str - contact_email: - description: - - Contact email for site - type: str - comments: - description: - - Comments for the site. This can be markdown syntax - type: str - tags: - description: - - Any tags that the prefix may need to be associated with - type: list - custom_fields: - description: - - must exist in Netbox - type: dict - required: true - state: - description: - - Use C(present) or C(absent) for adding or removing. - choices: [ absent, present ] - default: present - type: str - validate_certs: - description: - - | - If C(no), SSL certificates will not be validated. - This should only be used on personally controlled sites using self-signed certificates. - default: "yes" - type: bool -""" - -EXAMPLES = r""" -- name: "Test Netbox site module" - connection: local - hosts: localhost - gather_facts: False - tasks: - - name: Create site within Netbox with only required information - netbox_site: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - name: Test - Colorado - state: present - - - name: Delete site within netbox - netbox_site: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - name: Test - Colorado - state: absent - - - name: Create site with all parameters - netbox_site: - netbox_url: http://netbox.local - netbox_token: thisIsMyToken - data: - name: Test - California - status: Planned - region: Test Region - tenant: Test Tenant - facility: EquinoxCA7 - asn: 65001 - time_zone: America/Los Angeles - description: This is a test description - physical_address: Hollywood, CA, 90210 - shipping_address: Hollywood, CA, 90210 - latitude: 10.100000 - longitude: 12.200000 - contact_name: Jenny - contact_phone: 867-5309 - contact_email: jenny@changednumber.com - comments: ### Placeholder - state: present -""" - -RETURN = r""" -site: - 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 -""" - -import json -import traceback - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.net_tools.netbox.netbox_utils import ( - find_ids, - normalize_data, - create_netbox_object, - delete_netbox_object, - update_netbox_object, - SITE_STATUS, -) -from ansible.module_utils.compat import ipaddress -from ansible.module_utils._text import to_text - - -PYNETBOX_IMP_ERR = None -try: - import pynetbox - HAS_PYNETBOX = True -except ImportError: - PYNETBOX_IMP_ERR = traceback.format_exc() - HAS_PYNETBOX = False - - -def main(): - """ - Main entry point for module execution - """ - argument_spec = dict( - netbox_url=dict(type="str", required=True), - netbox_token=dict(type="str", required=True, no_log=True), - data=dict(type="dict", required=True), - state=dict(required=False, default="present", choices=["present", "absent"]), - validate_certs=dict(type="bool", default=True) - ) - - global module - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - - # Fail module if pynetbox is not installed - if not HAS_PYNETBOX: - module.fail_json(msg=missing_required_lib('pynetbox'), exception=PYNETBOX_IMP_ERR) - # Assign variables to be used with module - app = "dcim" - endpoint = "sites" - url = module.params["netbox_url"] - token = module.params["netbox_token"] - data = module.params["data"] - state = module.params["state"] - validate_certs = module.params["validate_certs"] - # Attempt to create Netbox API object - try: - nb = pynetbox.api(url, token=token, ssl_verify=validate_certs) - except Exception: - module.fail_json(msg="Failed to establish connection to Netbox API") - try: - nb_app = getattr(nb, app) - except AttributeError: - module.fail_json(msg="Incorrect application specified: %s" % (app)) - nb_endpoint = getattr(nb_app, endpoint) - norm_data = normalize_data(data) - try: - norm_data = _check_and_adapt_data(nb, norm_data) - - if "present" in state: - return module.exit_json( - **ensure_site_present(nb, nb_endpoint, norm_data) - ) - else: - return module.exit_json( - **ensure_site_absent(nb, nb_endpoint, norm_data) - ) - except pynetbox.RequestError as e: - return module.fail_json(msg=json.loads(e.error)) - except ValueError as e: - return module.fail_json(msg=str(e)) - except AttributeError as e: - return module.fail_json(msg=str(e)) - - -def _check_and_adapt_data(nb, data): - data = find_ids(nb, data) - - if data.get("status"): - data["status"] = SITE_STATUS.get(data["status"].lower()) - - if "-" in data["name"]: - site_slug = data["name"].replace(" ", "").lower() - elif " " in data["name"]: - site_slug = data["name"].replace(" ", "-").lower() - else: - site_slug = data["name"].lower() - - data["slug"] = site_slug - - return data - - -def ensure_site_present(nb, nb_endpoint, data): - """ - :returns dict(interface, msg, changed): dictionary resulting of the request, - where 'site' is the serialized interface fetched or newly created in Netbox - """ - - if not isinstance(data, dict): - changed = False - return {"msg": data, "changed": changed} - - nb_site = nb_endpoint.get(slug=data["slug"]) - result = dict() - if not nb_site: - site, diff = create_netbox_object(nb_endpoint, data, module.check_mode) - changed = True - msg = "Site %s created" % (data["name"]) - result["diff"] = diff - else: - site, diff = update_netbox_object(nb_site, data, module.check_mode) - if site is False: - module.fail_json( - msg="Request failed, couldn't update device: %s" % (data["name"]) - ) - if diff: - msg = "Site %s updated" % (data["name"]) - changed = True - result["diff"] = diff - else: - msg = "Site %s already exists" % (data["name"]) - changed = False - - result.update({"site": site, "msg": msg, "changed": changed}) - return result - - -def ensure_site_absent(nb, nb_endpoint, data): - """ - :returns dict(msg, changed) - """ - nb_site = nb_endpoint.get(slug=data["slug"]) - result = dict() - if nb_site: - dummy, diff = delete_netbox_object(nb_site, module.check_mode) - changed = True - msg = "Site %s deleted" % (data["name"]) - result["diff"] = diff - else: - msg = "Site %s already absent" % (data["name"]) - changed = False - - result.update({"msg": msg, "changed": changed}) - return result - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/plugins/inventory/netbox.py b/lib/ansible/plugins/inventory/netbox.py deleted file mode 100644 index a527e06e5a3..00000000000 --- a/lib/ansible/plugins/inventory/netbox.py +++ /dev/null @@ -1,499 +0,0 @@ -# Copyright (c) 2018 Remy Leone -# 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 = ''' - name: netbox - plugin_type: inventory - author: - - Remy Leone (@sieben) - - Anthony Ruhier (@Anthony25) - - Nikhil Singh Baliyan (@nikkytub) - - Sander Steffann (@steffann) - short_description: NetBox inventory source - description: - - Get inventory hosts from NetBox - extends_documentation_fragment: - - constructed - - inventory_cache - options: - plugin: - description: token that ensures this is a source file for the 'netbox' plugin. - required: True - choices: ['netbox'] - api_endpoint: - description: Endpoint of the NetBox API - required: True - env: - - name: NETBOX_API - validate_certs: - description: - - Allows connection when SSL certificates are not valid. Set to C(false) when certificates are not trusted. - default: True - type: boolean - config_context: - description: - - If True, it adds config-context in host vars. - - Config-context enables the association of arbitrary data to devices and virtual machines grouped by - region, site, role, platform, and/or tenant. Please check official netbox docs for more info. - default: False - type: boolean - token: - required: True - description: NetBox token. - env: - # in order of precedence - - name: NETBOX_TOKEN - - name: NETBOX_API_KEY - group_by: - description: Keys used to create groups. - type: list - choices: - - sites - - tenants - - racks - - tags - - device_roles - - device_types - - manufacturers - - platforms - default: [] - query_filters: - description: List of parameters passed to the query string (Multiple values may be separated by commas) - type: list - default: [] - timeout: - description: Timeout for Netbox requests in seconds - type: int - default: 60 - compose: - description: List of custom ansible host vars to create from the device object fetched from NetBox - default: {} - type: dict -''' - -EXAMPLES = ''' -# netbox_inventory.yml file in YAML format -# Example command line: ansible-inventory -v --list -i netbox_inventory.yml - -plugin: netbox -api_endpoint: http://localhost:8000 -validate_certs: True -config_context: False -group_by: - - device_roles -query_filters: - - role: network-edge-router - -# Query filters are passed directly as an argument to the fetching queries. -# You can repeat tags in the query string. - -query_filters: - - role: server - - tag: web - - tag: production - -# See the NetBox documentation at https://netbox.readthedocs.io/en/latest/api/overview/ -# the query_filters work as a logical **OR** -# -# Prefix any custom fields with cf_ and pass the field value with the regular NetBox query string - -query_filters: - - cf_foo: bar - -# NetBox inventory plugin also supports Constructable semantics -# You can fill your hosts vars using the compose option: - -plugin: netbox -compose: - foo: last_updated - bar: display_name - nested_variable: rack.display_name -''' - -import json -import uuid -from sys import version as python_version -from threading import Thread -from itertools import chain - -from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable -from ansible.module_utils.ansible_release import __version__ as ansible_version -from ansible.errors import AnsibleError -from ansible.module_utils._text import to_text -from ansible.module_utils.urls import open_url -from ansible.module_utils.six.moves.urllib.parse import urlencode -from ansible.module_utils.compat.ipaddress import ip_interface - -ALLOWED_DEVICE_QUERY_PARAMETERS = ( - "asset_tag", - "cluster_id", - "device_type_id", - "has_primary_ip", - "is_console_server", - "is_full_depth", - "is_network_device", - "is_pdu", - "mac_address", - "manufacturer", - "manufacturer_id", - "model", - "name", - "platform", - "platform_id", - "position", - "rack_group_id", - "rack_id", - "role", - "role_id", - "serial", - "site", - "site_id", - "status", - "tag", - "tenant", - "tenant_id", - "virtual_chassis_id", -) - - -class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): - NAME = 'netbox' - - def _fetch_information(self, url): - results = None - cache_key = self.get_cache_key(url) - - # get the user's cache option to see if we should save the cache if it is changing - user_cache_setting = self.get_option('cache') - - # read if the user has caching enabled and the cache isn't being refreshed - attempt_to_read_cache = user_cache_setting and self.use_cache - - # attempt to read the cache if inventory isn't being refreshed and the user has caching enabled - if attempt_to_read_cache: - try: - results = self._cache[cache_key] - need_to_fetch = False - except KeyError: - # occurs if the cache_key is not in the cache or if the cache_key expired - # we need to fetch the URL now - need_to_fetch = True - else: - # not reading from cache so do fetch - need_to_fetch = True - - if need_to_fetch: - self.display.v("Fetching: " + url) - response = open_url(url, headers=self.headers, timeout=self.timeout, validate_certs=self.validate_certs) - - try: - raw_data = to_text(response.read(), errors='surrogate_or_strict') - except UnicodeError: - raise AnsibleError("Incorrect encoding of fetched payload from NetBox API.") - - try: - results = json.loads(raw_data) - except ValueError: - raise AnsibleError("Incorrect JSON payload: %s" % raw_data) - - # put result in cache if enabled - if user_cache_setting: - self._cache[cache_key] = results - - return results - - def get_resource_list(self, api_url): - """Retrieves resource list from netbox API. - Returns: - A list of all resource from netbox API. - """ - if not api_url: - raise AnsibleError("Please check API URL in script configuration file.") - - hosts_list = [] - # Pagination. - while api_url: - self.display.v("Fetching: " + api_url) - # Get hosts list. - api_output = self._fetch_information(api_url) - hosts_list += api_output["results"] - api_url = api_output["next"] - - # Get hosts list. - return hosts_list - - @property - def group_extractors(self): - return { - "sites": self.extract_site, - "tenants": self.extract_tenant, - "racks": self.extract_rack, - "tags": self.extract_tags, - "disk": self.extract_disk, - "memory": self.extract_memory, - "vcpus": self.extract_vcpus, - "device_roles": self.extract_device_role, - "platforms": self.extract_platform, - "device_types": self.extract_device_type, - "config_context": self.extract_config_context, - "manufacturers": self.extract_manufacturer - } - - def extract_disk(self, host): - return host.get("disk") - - def extract_vcpus(self, host): - return host.get("vcpus") - - def extract_memory(self, host): - return host.get("memory") - - def extract_platform(self, host): - try: - return [self.platforms_lookup[host["platform"]["id"]]] - except Exception: - return - - def extract_device_type(self, host): - try: - return [self.device_types_lookup[host["device_type"]["id"]]] - except Exception: - return - - def extract_rack(self, host): - try: - return [self.racks_lookup[host["rack"]["id"]]] - except Exception: - return - - def extract_site(self, host): - try: - return [self.sites_lookup[host["site"]["id"]]] - except Exception: - return - - def extract_tenant(self, host): - try: - return [self.tenants_lookup[host["tenant"]["id"]]] - except Exception: - return - - def extract_device_role(self, host): - try: - if 'device_role' in host: - return [self.device_roles_lookup[host["device_role"]["id"]]] - elif 'role' in host: - return [self.device_roles_lookup[host["role"]["id"]]] - except Exception: - return - - def extract_config_context(self, host): - try: - return [host["config_context"]] - except Exception: - return - - def extract_manufacturer(self, host): - try: - return [self.manufacturers_lookup[host["device_type"]["manufacturer"]["id"]]] - except Exception: - return - - def extract_primary_ip(self, host): - try: - address = host["primary_ip"]["address"] - return str(ip_interface(address).ip) - except Exception: - return - - def extract_primary_ip4(self, host): - try: - address = host["primary_ip4"]["address"] - return str(ip_interface(address).ip) - except Exception: - return - - def extract_primary_ip6(self, host): - try: - address = host["primary_ip6"]["address"] - return str(ip_interface(address).ip) - except Exception: - return - - def extract_tags(self, host): - return host["tags"] - - def refresh_platforms_lookup(self): - url = self.api_endpoint + "/api/dcim/platforms/?limit=0" - platforms = self.get_resource_list(api_url=url) - self.platforms_lookup = dict((platform["id"], platform["name"]) for platform in platforms) - - def refresh_sites_lookup(self): - url = self.api_endpoint + "/api/dcim/sites/?limit=0" - sites = self.get_resource_list(api_url=url) - self.sites_lookup = dict((site["id"], site["name"]) for site in sites) - - def refresh_regions_lookup(self): - url = self.api_endpoint + "/api/dcim/regions/?limit=0" - regions = self.get_resource_list(api_url=url) - self.regions_lookup = dict((region["id"], region["name"]) for region in regions) - - def refresh_tenants_lookup(self): - url = self.api_endpoint + "/api/tenancy/tenants/?limit=0" - tenants = self.get_resource_list(api_url=url) - self.tenants_lookup = dict((tenant["id"], tenant["name"]) for tenant in tenants) - - def refresh_racks_lookup(self): - url = self.api_endpoint + "/api/dcim/racks/?limit=0" - racks = self.get_resource_list(api_url=url) - self.racks_lookup = dict((rack["id"], rack["name"]) for rack in racks) - - def refresh_device_roles_lookup(self): - url = self.api_endpoint + "/api/dcim/device-roles/?limit=0" - device_roles = self.get_resource_list(api_url=url) - self.device_roles_lookup = dict((device_role["id"], device_role["name"]) for device_role in device_roles) - - def refresh_device_types_lookup(self): - url = self.api_endpoint + "/api/dcim/device-types/?limit=0" - device_types = self.get_resource_list(api_url=url) - self.device_types_lookup = dict((device_type["id"], device_type["model"]) for device_type in device_types) - - def refresh_manufacturers_lookup(self): - url = self.api_endpoint + "/api/dcim/manufacturers/?limit=0" - manufacturers = self.get_resource_list(api_url=url) - self.manufacturers_lookup = dict((manufacturer["id"], manufacturer["name"]) for manufacturer in manufacturers) - - def refresh_lookups(self): - lookup_processes = ( - self.refresh_sites_lookup, - self.refresh_regions_lookup, - self.refresh_tenants_lookup, - self.refresh_racks_lookup, - self.refresh_device_roles_lookup, - self.refresh_platforms_lookup, - self.refresh_device_types_lookup, - self.refresh_manufacturers_lookup, - ) - - thread_list = [] - for p in lookup_processes: - t = Thread(target=p) - thread_list.append(t) - t.start() - - for thread in thread_list: - thread.join() - - def validate_query_parameters(self, x): - if not (isinstance(x, dict) and len(x) == 1): - self.display.warning("Warning query parameters %s not a dict with a single key." % x) - return - - k = tuple(x.keys())[0] - v = tuple(x.values())[0] - - if not (k in ALLOWED_DEVICE_QUERY_PARAMETERS or k.startswith("cf_")): - msg = "Warning: %s not in %s or starting with cf (Custom field)" % (k, ALLOWED_DEVICE_QUERY_PARAMETERS) - self.display.warning(msg=msg) - return - return k, v - - def refresh_url(self): - query_parameters = [("limit", 0)] - if self.query_filters: - query_parameters.extend(filter(lambda x: x, - map(self.validate_query_parameters, self.query_filters))) - if self.config_context: - self.device_url = self.api_endpoint + "/api/dcim/devices/?" + urlencode(query_parameters) - self.virtual_machines_url = self.api_endpoint + "/api/virtualization/virtual-machines/?" + urlencode(query_parameters) - else: - self.device_url = self.api_endpoint + "/api/dcim/devices/?" + urlencode(query_parameters) + "&exclude=config_context" - self.virtual_machines_url = self.api_endpoint + "/api/virtualization/virtual-machines/?" + urlencode(query_parameters) + "&exclude=config_context" - - def fetch_hosts(self): - return chain( - self.get_resource_list(self.device_url), - self.get_resource_list(self.virtual_machines_url), - ) - - def extract_name(self, host): - # An host in an Ansible inventory requires an hostname. - # name is an unique but not required attribute for a device in NetBox - # We default to an UUID for hostname in case the name is not set in NetBox - return host["name"] or str(uuid.uuid4()) - - def add_host_to_groups(self, host, hostname): - for group in self.group_by: - sub_groups = self.group_extractors[group](host) - - if not sub_groups: - continue - - for sub_group in sub_groups: - group_name = "_".join([group, sub_group]) - self.inventory.add_group(group=group_name) - self.inventory.add_host(group=group_name, host=hostname) - - def _fill_host_variables(self, host, hostname): - for attribute, extractor in self.group_extractors.items(): - if not extractor(host): - continue - self.inventory.set_variable(hostname, attribute, extractor(host)) - - if self.extract_primary_ip(host): - self.inventory.set_variable(hostname, "ansible_host", self.extract_primary_ip(host=host)) - - if self.extract_primary_ip4(host): - self.inventory.set_variable(hostname, "primary_ip4", self.extract_primary_ip4(host=host)) - - if self.extract_primary_ip6(host): - self.inventory.set_variable(hostname, "primary_ip6", self.extract_primary_ip6(host=host)) - - def main(self): - self.refresh_lookups() - self.refresh_url() - hosts_list = self.fetch_hosts() - - for host in hosts_list: - hostname = self.extract_name(host=host) - self.inventory.add_host(host=hostname) - self._fill_host_variables(host=host, hostname=hostname) - - strict = self.get_option("strict") - - # Composed variables - self._set_composite_vars(self.get_option('compose'), host, hostname, strict=strict) - - # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group - self._add_host_to_composed_groups(self.get_option('groups'), host, hostname, strict=strict) - - # Create groups based on variable values and add the corresponding hosts to it - self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host, hostname, strict=strict) - self.add_host_to_groups(host=host, hostname=hostname) - - def parse(self, inventory, loader, path, cache=True): - super(InventoryModule, self).parse(inventory, loader, path) - self._read_config_data(path=path) - self.use_cache = cache - - # Netbox access - token = self.get_option("token") - # Handle extra "/" from api_endpoint configuration and trim if necessary, see PR#49943 - self.api_endpoint = self.get_option("api_endpoint").strip('/') - self.timeout = self.get_option("timeout") - self.validate_certs = self.get_option("validate_certs") - self.config_context = self.get_option("config_context") - self.headers = { - 'Authorization': "Token %s" % token, - 'User-Agent': "ansible %s Python %s" % (ansible_version, python_version.split(' ')[0]), - 'Content-type': 'application/json' - } - - # Filter and group_by options - self.group_by = self.get_option("group_by") - self.query_filters = self.get_option("query_filters") - self.main() diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 2855db19440..cfc2d4bf924 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -86,7 +86,6 @@ lib/ansible/module_utils/gcp_utils.py future-import-boilerplate lib/ansible/module_utils/gcp_utils.py metaclass-boilerplate lib/ansible/module_utils/json_utils.py future-import-boilerplate lib/ansible/module_utils/json_utils.py metaclass-boilerplate -lib/ansible/module_utils/net_tools/netbox/netbox_utils.py future-import-boilerplate lib/ansible/module_utils/netapp.py future-import-boilerplate lib/ansible/module_utils/netapp.py metaclass-boilerplate lib/ansible/module_utils/netapp_elementsw_module.py future-import-boilerplate @@ -1509,14 +1508,6 @@ lib/ansible/modules/net_tools/basics/uri.py pylint:blacklisted-name lib/ansible/modules/net_tools/basics/uri.py validate-modules:doc-required-mismatch lib/ansible/modules/net_tools/basics/uri.py validate-modules:parameter-list-no-elements lib/ansible/modules/net_tools/basics/uri.py validate-modules:parameter-type-not-in-doc -lib/ansible/modules/net_tools/netbox/netbox_device.py validate-modules:doc-missing-type -lib/ansible/modules/net_tools/netbox/netbox_device.py validate-modules:parameter-type-not-in-doc -lib/ansible/modules/net_tools/netbox/netbox_interface.py validate-modules:parameter-type-not-in-doc -lib/ansible/modules/net_tools/netbox/netbox_ip_address.py validate-modules:doc-missing-type -lib/ansible/modules/net_tools/netbox/netbox_ip_address.py validate-modules:parameter-type-not-in-doc -lib/ansible/modules/net_tools/netbox/netbox_prefix.py validate-modules:doc-missing-type -lib/ansible/modules/net_tools/netbox/netbox_prefix.py validate-modules:parameter-type-not-in-doc -lib/ansible/modules/net_tools/netbox/netbox_site.py validate-modules:parameter-type-not-in-doc lib/ansible/modules/network/aci/aci_aaa_user.py validate-modules:doc-required-mismatch lib/ansible/modules/network/aci/aci_aaa_user_certificate.py validate-modules:doc-required-mismatch lib/ansible/modules/network/aci/aci_access_port_block_to_access_port.py validate-modules:doc-required-mismatch diff --git a/test/units/module_utils/net_tools/netbox/test_netbox_utils.py b/test/units/module_utils/net_tools/netbox/test_netbox_utils.py deleted file mode 100644 index 72584438051..00000000000 --- a/test/units/module_utils/net_tools/netbox/test_netbox_utils.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: (c) 2019, Bruno Inec (@sweenu) -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -import pytest - -from ansible.module_utils.net_tools.netbox.netbox_utils import ( - QUERY_TYPES, - _build_diff, - create_netbox_object, - delete_netbox_object, - update_netbox_object, - normalize_data, -) - - -def test_normalize_data(): - assert "name" not in QUERY_TYPES - assert QUERY_TYPES.get("rack") == "slug" - assert QUERY_TYPES.get("primary_ip") != "slug" - - raw_data = { - "name": "Some name", - "primary_ip": "10.3.72.74/31", - "rack": "Some rack", - } - normalized_data = raw_data.copy() - normalized_data["rack"] = "some-rack" - - assert normalize_data(raw_data) == normalized_data - - -def test_build_diff(): - before = "The state before" - after = {"A": "more", "complicated": "state"} - diff = _build_diff(before=before, after=after) - assert diff == {"before": before, "after": after} - - -@pytest.fixture -def nb_obj_mock(mocker): - serialized_object = {"The serialized": "object"} - nb_obj = mocker.Mock(name="nb_obj_mock") - nb_obj.delete.return_value = True - nb_obj.update.return_value = True - nb_obj.update.side_effect = serialized_object.update - nb_obj.serialize.return_value = serialized_object - - return nb_obj - - -@pytest.fixture -def endpoint_mock(mocker, nb_obj_mock): - endpoint = mocker.Mock(name="endpoint_mock") - endpoint.create.return_value = nb_obj_mock - - return endpoint - - -@pytest.fixture -def on_creation_diff(): - return _build_diff(before={"state": "absent"}, after={"state": "present"}) - - -@pytest.fixture -def on_deletion_diff(): - return _build_diff(before={"state": "present"}, after={"state": "absent"}) - - -@pytest.fixture -def data(): - return {"name": "Some Netbox object name"} - - -def test_create_netbox_object(endpoint_mock, data, on_creation_diff): - return_value = endpoint_mock.create().serialize() - - serialized_obj, diff = create_netbox_object( - endpoint_mock, data, check_mode=False - ) - assert endpoint_mock.create.called_once_with(data) - assert serialized_obj == return_value - assert diff == on_creation_diff - - -def test_create_netbox_object_in_check_mode(endpoint_mock, data, on_creation_diff): - serialized_obj, diff = create_netbox_object( - endpoint_mock, data, check_mode=True - ) - assert endpoint_mock.create.not_called() - assert serialized_obj == data - assert diff == on_creation_diff - - -def test_delete_netbox_object(nb_obj_mock, on_deletion_diff): - serialized_obj, diff = delete_netbox_object(nb_obj_mock, check_mode=False) - assert nb_obj_mock.delete.called_once() - assert serialized_obj == nb_obj_mock.serialize() - assert diff == on_deletion_diff - - -def test_delete_netbox_object_in_check_mode(nb_obj_mock, on_deletion_diff): - serialized_obj, diff = delete_netbox_object(nb_obj_mock, check_mode=True) - assert nb_obj_mock.delete.not_called() - assert serialized_obj == nb_obj_mock.serialize() - assert diff == on_deletion_diff - - -def test_update_netbox_object_no_changes(nb_obj_mock): - unchanged_data = nb_obj_mock.serialize() - serialized_obj, diff = update_netbox_object(nb_obj_mock, unchanged_data, check_mode=True) - assert nb_obj_mock.update.not_called() - assert serialized_obj == unchanged_data - assert diff is None - - -@pytest.fixture -def changed_serialized_obj(nb_obj_mock): - changed_serialized_obj = nb_obj_mock.serialize().copy() - changed_serialized_obj[list(changed_serialized_obj.keys())[0]] += " (modified)" - - return changed_serialized_obj - - -@pytest.fixture -def on_update_diff(nb_obj_mock, changed_serialized_obj): - return _build_diff(before=nb_obj_mock.serialize().copy(), after=changed_serialized_obj) - - -def test_update_netbox_object_with_changes( - nb_obj_mock, changed_serialized_obj, on_update_diff -): - serialized_obj, diff = update_netbox_object( - nb_obj_mock, changed_serialized_obj, check_mode=False - ) - assert nb_obj_mock.update.called_once_with(changed_serialized_obj) - assert serialized_obj == nb_obj_mock.serialize() - assert diff == on_update_diff - - -def test_update_netbox_object_with_changes_in_check_mode( - nb_obj_mock, changed_serialized_obj, on_update_diff -): - updated_serialized_obj = nb_obj_mock.serialize().copy() - updated_serialized_obj.update(changed_serialized_obj) - - serialized_obj, diff = update_netbox_object( - nb_obj_mock, changed_serialized_obj, check_mode=True - ) - assert nb_obj_mock.update.not_called() - - assert serialized_obj == updated_serialized_obj - assert diff == on_update_diff