#!/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()