diff --git a/lib/ansible/module_utils/net_tools/netbox/netbox_utils.py b/lib/ansible/module_utils/net_tools/netbox/netbox_utils.py index e9efceb8028..56fe9b572f2 100644 --- a/lib/ansible/module_utils/net_tools/netbox/netbox_utils.py +++ b/lib/ansible/module_utils/net_tools/netbox/netbox_utils.py @@ -197,7 +197,10 @@ def create_netbox_object(nb_endpoint, data, check_mode): if check_mode: serialized_nb_obj = data else: - serialized_nb_obj = nb_endpoint.create(data).serialize() + try: + serialized_nb_obj = nb_endpoint.create(data).serialize() + except AttributeError: + serialized_nb_obj = nb_endpoint.create(data) diff = _build_diff(before={"state": "absent"}, after={"state": "present"}) return serialized_nb_obj, diff diff --git a/lib/ansible/modules/net_tools/netbox/netbox_prefix.py b/lib/ansible/modules/net_tools/netbox/netbox_prefix.py new file mode 100644 index 00000000000..e114283e0a6 --- /dev/null +++ b/lib/ansible/modules/net_tools/netbox/netbox_prefix.py @@ -0,0 +1,461 @@ +#!/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()