saltstack/salt

View on GitHub
salt/modules/netbox.py

Summary

Maintainability
F
5 days
Test Coverage
# -*- coding: utf-8 -*-
'''
NetBox
======

Module to query NetBox

:codeauthor: Zach Moody <zmoody@do.co>
:maturity:   new
:depends:    pynetbox

The following config should be in the minion config file. In order to
work with ``secrets`` you should provide a token and path to your
private key file:

.. code-block:: yaml

  netbox:
    url: <NETBOX_URL>
    token: <NETBOX_USERNAME_API_TOKEN (OPTIONAL)>
    keyfile: </PATH/TO/NETBOX/KEY (OPTIONAL)>

.. versionadded:: 2018.3.0
'''
from __future__ import absolute_import, unicode_literals, print_function

import re
import logging

from salt.exceptions import CommandExecutionError

try:
    import pynetbox
    from pynetbox.lib import RequestError
    HAS_PYNETBOX = True
except ImportError:
    HAS_PYNETBOX = False

log = logging.getLogger(__name__)

AUTH_ENDPOINTS = (
    'secrets',
)

__func_alias__ = {
    'filter_': 'filter',
    'get_': 'get'
}


def __virtual__():
    '''
    pynetbox must be installed.
    '''
    if not HAS_PYNETBOX:
        return (
            False,
            'The netbox execution module cannot be loaded: '
            'pynetbox library is not installed.'
        )
    else:
        return True


def _config():
    config = __salt__['config.get']('netbox')
    if not config:
        raise CommandExecutionError(
            'NetBox execution module configuration could not be found'
        )
    return config


def _nb_obj(auth_required=False):
    pynb_kwargs = {}
    pynb_kwargs['token'] = _config().get('token')
    if auth_required:
        pynb_kwargs['private_key_file'] = _config().get('keyfile')
    return pynetbox.api(_config().get('url'), **pynb_kwargs)


def _strip_url_field(input_dict):
    if 'url' in input_dict.keys():
        del input_dict['url']
    for k, v in input_dict.items():
        if isinstance(v, dict):
            _strip_url_field(v)
    return input_dict


def _dict(iterable):
    if iterable:
        return dict(iterable)
    else:
        return {}


def _add(app, endpoint, payload):
    '''
    POST a payload
    '''
    nb = _nb_obj(auth_required=True)
    try:
        return getattr(getattr(nb, app), endpoint).create(**payload)
    except RequestError as e:
        log.error('%s, %s, %s', e.req.request.headers, e.request_body, e.error)
        return False


def slugify(value):
    ''''
    Slugify given value.
    Credit to Djangoproject https://docs.djangoproject.com/en/2.0/_modules/django/utils/text/#slugify
    '''
    value = re.sub(r'[^\w\s-]', '', value).strip().lower()
    return re.sub(r'[-\s]+', '-', value)


def _get(app, endpoint, id=None, auth_required=False, **kwargs):
    '''
    Helper function to do a GET request to Netbox.
    Returns the actual pynetbox object, which allows manipulation from other functions.
    '''
    nb = _nb_obj(auth_required=auth_required)
    if id:
        item = getattr(getattr(nb, app), endpoint).get(id)
    else:
        kwargs = __utils__['args.clean_kwargs'](**kwargs)
        item = getattr(getattr(nb, app), endpoint).get(**kwargs)
    return item


def _if_name_unit(if_name):
    if_name_split = if_name.split('.')
    if len(if_name_split) == 2:
        return if_name_split
    return if_name, '0'


def filter_(app, endpoint, **kwargs):
    '''
    Get a list of items from NetBox.

    app
        String of netbox app, e.g., ``dcim``, ``circuits``, ``ipam``
    endpoint
        String of app endpoint, e.g., ``sites``, ``regions``, ``devices``
    kwargs
        Optional arguments that can be used to filter.
        All filter keywords are available in Netbox,
        which can be found by surfing to the corresponding API endpoint,
        and clicking Filters. e.g., ``role=router``

    Returns a list of dictionaries

    .. code-block:: bash

        salt myminion netbox.filter dcim devices status=1 role=router
    '''
    ret = []
    nb = _nb_obj(auth_required=True if app in AUTH_ENDPOINTS else False)
    nb_query = getattr(getattr(nb, app), endpoint).filter(
        **__utils__['args.clean_kwargs'](**kwargs)
    )
    if nb_query:
        ret = [_strip_url_field(dict(i)) for i in nb_query]
    return sorted(ret)


def get_(app, endpoint, id=None, **kwargs):
    '''
    Get a single item from NetBox.

    app
        String of netbox app, e.g., ``dcim``, ``circuits``, ``ipam``
    endpoint
        String of app endpoint, e.g., ``sites``, ``regions``, ``devices``

    Returns a single dictionary

    To get an item based on ID.

    .. code-block:: bash

        salt myminion netbox.get dcim devices id=123

    Or using named arguments that correspond with accepted filters on
    the NetBox endpoint.

    .. code-block:: bash

        salt myminion netbox.get dcim devices name=my-router
    '''
    return _dict(_get(app, endpoint, id=id, auth_required=True if app in AUTH_ENDPOINTS else False, **kwargs))


def create_manufacturer(name):
    '''
    .. versionadded:: 2019.2.0

    Create a device manufacturer.

    name
        The name of the manufacturer, e.g., ``Juniper``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_manufacturer Juniper
    '''
    nb_man = get_('dcim', 'manufacturers', name=name)
    if nb_man:
        return False
    else:
        payload = {'name': name, 'slug': slugify(name)}
        man = _add('dcim', 'manufacturers', payload)
        if man:
            return {'dcim': {'manufacturers': payload}}
        else:
            return False


def create_device_type(model, manufacturer):
    '''
    .. versionadded:: 2019.2.0

    Create a device type. If the manufacturer doesn't exist, create a new manufacturer.

    model
        String of device model, e.g., ``MX480``
    manufacturer
        String of device manufacturer, e.g., ``Juniper``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_device_type MX480 Juniper
    '''
    nb_type = get_('dcim', 'device-types', model=model)
    if nb_type:
        return False
    nb_man = get_('dcim', 'manufacturers', name=manufacturer)
    new_man = None
    if not nb_man:
        new_man = create_manufacturer(manufacturer)
    payload = {'model': model, 'manufacturer': nb_man['id'], 'slug': slugify(model)}
    typ = _add('dcim', 'device-types', payload)
    ret_dict = {'dcim': {'device-types': payload}}
    if new_man:
        ret_dict['dcim'].update(new_man['dcim'])
    if typ:
        return ret_dict
    else:
        return False


def create_device_role(role, color):
    '''
    .. versionadded:: 2019.2.0

    Create a device role

    role
        String of device role, e.g., ``router``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_device_role router
    '''
    nb_role = get_('dcim', 'device-roles', name=role)
    if nb_role:
        return False
    else:
        payload = {'name': role, 'slug': slugify(role), 'color': color}
        role = _add('dcim', 'device-roles', payload)
        if role:
            return{'dcim': {'device-roles': payload}}
        else:
            return False


def create_platform(platform):
    '''
    .. versionadded:: 2019.2.0

    Create a new device platform

    platform
        String of device platform, e.g., ``junos``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_platform junos
    '''
    nb_platform = get_('dcim', 'platforms', slug=slugify(platform))
    if nb_platform:
        return False
    else:
        payload = {'name': platform, 'slug': slugify(platform)}
        plat = _add('dcim', 'platforms', payload)
        if plat:
            return {'dcim': {'platforms': payload}}
        else:
            return False


def create_site(site):
    '''
    .. versionadded:: 2019.2.0

    Create a new device site

    site
        String of device site, e.g., ``BRU``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_site BRU
    '''
    nb_site = get_('dcim', 'sites', name=site)
    if nb_site:
        return False
    else:
        payload = {'name': site, 'slug': slugify(site)}
        site = _add('dcim', 'sites', payload)
        if site:
            return {'dcim': {'sites': payload}}
        else:
            return False


def create_device(name,
                  role,
                  model,
                  manufacturer,
                  site):
    '''
    .. versionadded:: 2019.2.0

    Create a new device with a name, role, model, manufacturer and site.
    All these components need to be already in Netbox.

    name
        The name of the device, e.g., ``edge_router``
    role
        String of device role, e.g., ``router``
    model
        String of device model, e.g., ``MX480``
    manufacturer
        String of device manufacturer, e.g., ``Juniper``
    site
        String of device site, e.g., ``BRU``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_device edge_router router MX480 Juniper BRU
    '''
    try:
        nb_role = get_('dcim', 'device-roles', name=role)
        if not nb_role:
            return False

        nb_type = get_('dcim', 'device-types', model=model)
        if not nb_type:
            return False
        nb_site = get_('dcim', 'sites', name=site)
        if not nb_site:
            return False

        status = {'label': "Active", 'value': 1}
    except RequestError as e:
        log.error('%s, %s, %s', e.req.request.headers, e.request_body, e.error)
        return False

    payload = {'name': name, 'display_name': name, 'slug': slugify(name), 'device_type': nb_type['id'],
               'device_role': nb_role['id'], 'site': nb_site['id']}
    new_dev = _add('dcim', 'devices', payload)
    if new_dev:
        return {'dcim': {'devices': payload}}
    else:
        return False


def update_device(name, **kwargs):
    '''
    .. versionadded:: 2019.2.0

    Add attributes to an existing device, identified by name.

    name
        The name of the device, e.g., ``edge_router``
    kwargs
       Arguments to change in device, e.g., ``serial=JN2932930``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.update_device edge_router serial=JN2932920
    '''
    kwargs = __utils__['args.clean_kwargs'](**kwargs)
    nb_device = _get('dcim', 'devices', auth_required=True, name=name)
    for k, v in kwargs.items():
        setattr(nb_device, k, v)
    try:
        nb_device.save()
        return {'dcim': {'devices': kwargs}}
    except RequestError as e:
        log.error('%s, %s, %s', e.req.request.headers, e.request_body, e.error)
        return False


def create_inventory_item(device_name,
                          item_name,
                          manufacturer_name=None,
                          serial='',
                          part_id='',
                          description=''):
    '''
    .. versionadded:: 2019.2.0

    Add an inventory item to an existing device.

    device_name
        The name of the device, e.g., ``edge_router``.
    item_name
        String of inventory item name, e.g., ``Transceiver``.

    manufacturer_name
        String of inventory item manufacturer, e.g., ``Fiberstore``.

    serial
        String of inventory item serial, e.g., ``FS1238931``.

    part_id
        String of inventory item part id, e.g., ``740-01234``.

    description
        String of inventory item description, e.g., ``SFP+-10G-LR``.

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_inventory_item edge_router Transceiver part_id=740-01234
    '''
    nb_device = get_('dcim', 'devices', name=device_name)
    if not nb_device:
        return False
    if manufacturer_name:
        nb_man = get_('dcim', 'manufacturers', name=manufacturer_name)
        if not nb_man:
            create_manufacturer(manufacturer_name)
            nb_man = get_('dcim', 'manufacturers', name=manufacturer_name)
    payload = {'device': nb_device['id'], 'name': item_name, 'description': description, 'serial': serial,
               'part_id': part_id, 'parent': None}
    if manufacturer_name:
        payload['manufacturer'] = nb_man['id']
    done = _add('dcim', 'inventory-items', payload)
    if done:
        return {'dcim': {'inventory-items': payload}}
    else:
        return done


def delete_inventory_item(item_id):
    '''
    .. versionadded:: 2019.2.0

    Remove an item from a devices inventory. Identified by the netbox id

    item_id
        Integer of item to be deleted

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.delete_inventory_item 1354
    '''
    nb_inventory_item = _get('dcim', 'inventory-items', auth_required=True, id=item_id)
    nb_inventory_item.delete()
    return {'DELETE': {'dcim': {'inventory-items': item_id}}}


def create_interface_connection(interface_a, interface_b):
    '''
    .. versionadded:: 2019.2.0

    Create an interface connection between 2 interfaces

    interface_a
        Interface id for Side A
    interface_b
        Interface id for Side B

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_interface_connection 123 456
    '''
    payload = {'interface_a': interface_a,
               'interface_b': interface_b}
    ret = _add('dcim', 'interface-connections', payload)
    if ret:
        return {'dcim': {'interface-connections': {ret['id']: payload}}}
    else:
        return ret


def get_interfaces(device_name=None, **kwargs):
    '''
    .. versionadded:: 2019.2.0

    Returns interfaces for a specific device using arbitrary netbox filters

    device_name
        The name of the device, e.g., ``edge_router``
    kwargs
        Optional arguments to be used for filtering

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.get_interfaces edge_router name="et-0/0/5"

    '''
    if not device_name:
        device_name = __opts__['id']
    netbox_device = get_('dcim', 'devices', name=device_name)
    return filter_('dcim',
                   'interfaces',
                   device_id=netbox_device['id'],
                   **kwargs)


def openconfig_interfaces(device_name=None):
    '''
    .. versionadded:: 2019.2.0

    Return a dictionary structured as standardised in the
    `openconfig-interfaces <http://ops.openconfig.net/branches/master/openconfig-interfaces.html>`_
    YANG model, containing physical and configuration data available in Netbox,
    e.g., IP addresses, MTU, enabled / disabled, etc.

    device_name: ``None``
        The name of the device to query the interface data for. If not provided,
        will use the Minion ID.

    CLI Example:

    .. code-block:: bash

        salt '*' netbox.openconfig_interfaces
        salt '*' netbox.openconfig_interfaces device_name=cr1.thn.lon
    '''
    oc_if = {}
    interfaces = get_interfaces(device_name=device_name)
    ipaddresses = get_ipaddresses(device_name=device_name)
    for interface in interfaces:
        if_name, if_unit = _if_name_unit(interface['name'])
        if if_name not in oc_if:
            oc_if[if_name] = {
                'config': {
                    'name': if_name
                },
                'subinterfaces': {'subinterface': {}}
             }
        if if_unit == '0':
            oc_if[if_name]['config']['enabled'] = interface['enabled']
            if interface['description']:
                if if_name == interface['name']:
                    # When that's a real unit 0 interface
                    # Otherwise it will inherit the description from the subif
                    oc_if[if_name]['config']['description'] = str(interface['description'])
                else:
                    subif_descr = {
                        'subinterfaces': {
                            'subinterface': {
                                if_unit: {
                                    'config': {
                                        'description': str(interface['description'])
                                    }
                                }
                            }
                        }
                    }
                    oc_if[if_name] = __utils__['dictupdate.update'](oc_if[if_name], subif_descr)
            if interface['mtu']:
                oc_if[if_name]['config']['mtu'] = int(interface['mtu'])
        else:
            oc_if[if_name]['subinterfaces']['subinterface'][if_unit] = {
                'config': {
                    'index': int(if_unit),
                    'enabled': interface['enabled']
                }
            }
            if interface['description']:
                oc_if[if_name]['subinterfaces']['subinterface'][if_unit]['config']['description'] =\
                    str(interface['description'])
    for ipaddress in ipaddresses:
        ip, prefix_length = ipaddress['address'].split('/')
        if_name = ipaddress['interface']['name']
        if_name, if_unit = _if_name_unit(if_name)
        ipvkey = 'ipv{}'.format(ipaddress['family'])
        if if_unit not in oc_if[if_name]['subinterfaces']['subinterface']:
            oc_if[if_name]['subinterfaces']['subinterface'][if_unit] = {
                'config': {
                    'index': int(if_unit),
                    'enabled': True
                }
            }
        if ipvkey not in oc_if[if_name]['subinterfaces']['subinterface'][if_unit]:
            oc_if[if_name]['subinterfaces']['subinterface'][if_unit][ipvkey] = {
                'addresses': {
                    'address': {}
                }
            }
        oc_if[if_name]['subinterfaces']['subinterface'][if_unit][ipvkey]['addresses']['address'][ip] = {
            'config': {
                'ip': ip,
                'prefix_length': int(prefix_length)
            }
        }
    return {
        'interfaces': {
            'interface': oc_if
        }
    }


def openconfig_lacp(device_name=None):
    '''
    .. versionadded:: 2019.2.0

    Return a dictionary structured as standardised in the
    `openconfig-lacp <http://ops.openconfig.net/branches/master/openconfig-lacp.html>`_
    YANG model, with configuration data for Link Aggregation Control Protocol
    (LACP) for aggregate interfaces.

    .. note::
        The ``interval`` and ``lacp_mode`` keys have the values set as ``SLOW``
        and ``ACTIVE`` respectively, as this data is not currently available
        in Netbox, therefore defaulting to the values defined in the standard.
        See `interval <http://ops.openconfig.net/branches/master/docs/openconfig-lacp.html#lacp-interfaces-interface-config-interval>`_
        and `lacp-mode <http://ops.openconfig.net/branches/master/docs/openconfig-lacp.html#lacp-interfaces-interface-config-lacp-mode>`_
        for further details.

    device_name: ``None``
        The name of the device to query the LACP information for. If not provided,
        will use the Minion ID.

    CLI Example:

    .. code-block:: bash

        salt '*' netbox.openconfig_lacp
        salt '*' netbox.openconfig_lacp device_name=cr1.thn.lon
    '''
    oc_lacp = {}
    interfaces = get_interfaces(device_name=device_name)
    for interface in interfaces:
        if not interface['lag']:
            continue
        if_name, if_unit = _if_name_unit(interface['name'])
        parent_if = interface['lag']['name']
        if parent_if not in oc_lacp:
            oc_lacp[parent_if] = {
                'config': {
                    'name': parent_if,
                    'interval': 'SLOW',
                    'lacp_mode': 'ACTIVE'
                },
                'members': {
                    'member': {}
                }
            }
        oc_lacp[parent_if]['members']['member'][if_name] = {}
    return {
        'lacp': {
            'interfaces': {
                'interface': oc_lacp
            }
        }
    }


def create_interface(device_name,
                     interface_name,
                     mac_address=None,
                     description=None,
                     enabled=None,
                     lag=None,
                     lag_parent=None,
                     form_factor=None):
    '''
    .. versionadded:: 2019.2.0

    Attach an interface to a device. If not all arguments are provided,
    they will default to Netbox defaults.

    device_name
        The name of the device, e.g., ``edge_router``
    interface_name
        The name of the interface, e.g., ``TenGigE0/0/0/0``
    mac_address
        String of mac address, e.g., ``50:87:89:73:92:C8``
    description
        String of interface description, e.g., ``NTT``
    enabled
        String of boolean interface status, e.g., ``True``
    lag:
        Boolean of interface lag status, e.g., ``True``
    lag_parent
        String of interface lag parent name, e.g., ``ae13``
    form_factor
        Integer of form factor id, obtained through _choices API endpoint, e.g., ``200``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_interface edge_router ae13 description="Core uplink"
    '''
    nb_device = get_('dcim', 'devices', name=device_name)
    if not nb_device:
        return False
    if lag_parent:
        lag_interface = get_('dcim', 'interfaces', device_id=nb_device['id'], name=lag_parent)
        if not lag_interface:
            return False
    if not description:
        description = ''
    if not enabled:
        enabled = 'false'
    # Set default form factor to 1200. This maps to SFP+ (10GE). This should be addressed by
    # the _choices endpoint.
    payload = {'device': nb_device['id'], 'name': interface_name,
               'description': description, 'enabled': enabled, 'form_factor': 1200}
    if form_factor is not None:
        payload['form_factor'] = form_factor
    if lag:
        payload['form_factor'] = 200
    if lag_parent:
        payload['lag'] = lag_interface['id']
    if mac_address:
        payload['mac_address'] = mac_address
    nb_interface = get_('dcim', 'interfaces', device_id=nb_device['id'], name=interface_name)
    if not nb_interface:
        nb_interface = _add('dcim', 'interfaces', payload)
    if nb_interface:
        return {'dcim': {'interfaces': {nb_interface['id']: payload}}}
    else:
        return nb_interface


def update_interface(device_name, interface_name, **kwargs):
    '''
    .. versionadded:: 2019.2.0

    Update an existing interface with new attributes.

    device_name
        The name of the device, e.g., ``edge_router``
    interface_name
        The name of the interface, e.g., ``ae13``
    kwargs
        Arguments to change in interface, e.g., ``mac_address=50:87:69:53:32:D0``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.update_interface edge_router ae13 mac_address=50:87:69:53:32:D0
    '''
    nb_device = get_('dcim', 'devices', name=device_name)
    nb_interface = _get('dcim', 'interfaces', auth_required=True, device_id=nb_device['id'], name=interface_name)
    if not nb_device:
        return False
    if not nb_interface:
        return False
    else:
        for k, v in __utils__['args.clean_kwargs'](**kwargs).items():
            setattr(nb_interface, k, v)
        try:
            nb_interface.save()
            return {'dcim': {'interfaces': {nb_interface.id: dict(nb_interface)}}}
        except RequestError as e:
            log.error('%s, %s, %s', e.req.request.headers, e.request_body, e.error)
            return False


def delete_interface(device_name, interface_name):
    '''
    .. versionadded:: 2019.2.0

    Delete an interface from a device.

    device_name
        The name of the device, e.g., ``edge_router``.

    interface_name
        The name of the interface, e.g., ``ae13``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.delete_interface edge_router ae13
    '''
    nb_device = get_('dcim', 'devices', name=device_name)
    nb_interface = _get('dcim', 'interfaces', auth_required=True, device_id=nb_device['id'], name=interface_name)
    if nb_interface:
        nb_interface.delete()
        return {'DELETE': {'dcim': {'interfaces': {nb_interface.id: nb_interface.name}}}}
    return False


def make_interface_lag(device_name, interface_name):
    '''
    .. versionadded:: 2019.2.0

    Update an interface to be a LAG.

    device_name
        The name of the device, e.g., ``edge_router``.

    interface_name
        The name of the interface, e.g., ``ae13``.

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.make_interface_lag edge_router ae13
    '''
    return update_interface(device_name, interface_name, form_factor=200)


def make_interface_child(device_name, interface_name, parent_name):
    '''
    .. versionadded:: 2019.2.0

    Set an interface as part of a LAG.

    device_name
        The name of the device, e.g., ``edge_router``.

    interface_name
        The name of the interface to be attached to LAG, e.g., ``xe-1/0/2``.

    parent_name
        The name of the LAG interface, e.g., ``ae13``.

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.make_interface_child xe-1/0/2 ae13
    '''
    nb_device = get_('dcim', 'devices', name=device_name)
    nb_parent = get_('dcim', 'interfaces', device_id=nb_device['id'], name=parent_name)
    if nb_device and nb_parent:
        return update_interface(device_name, interface_name, lag=nb_parent['id'])
    else:
        return False


def get_ipaddresses(device_name=None, **kwargs):
    '''
    .. versionadded:: 2019.2.0

    Filters for an IP address using specified filters

    device_name
        The name of the device to check for the IP address
    kwargs
        Optional arguments that can be used to filter, e.g., ``family=4``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.get_ipaddresses device_name family=4
    '''
    if not device_name:
        device_name = __opts__['id']
    netbox_device = get_('dcim', 'devices', name=device_name)
    return filter_('ipam',
                   'ip-addresses',
                   device_id=netbox_device['id'],
                   **kwargs)


def create_ipaddress(ip_address, family, device=None, interface=None):
    '''
    .. versionadded:: 2019.2.0

    Add an IP address, and optionally attach it to an interface.

    ip_address
        The IP address and CIDR, e.g., ``192.168.1.1/24``
    family
        Integer of IP family, e.g., ``4``
    device
        The name of the device to attach IP to, e.g., ``edge_router``
    interface
        The name of the interface to attach IP to, e.g., ``ae13``

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_ipaddress 192.168.1.1/24 4 device=edge_router interface=ae13
    '''
    nb_addr = None
    payload = {'family': family, 'address': ip_address}
    if interface and device:
        nb_device = get_('dcim', 'devices', name=device)
        if not nb_device:
            return False
        nb_interface = get_('dcim', 'interfaces', device_id=nb_device['id'], name=interface)
        if not nb_interface:
            return False
        nb_addr = get_('ipam', 'ip-addresses', q=ip_address, interface_id=nb_interface['id'], family=family)
        if nb_addr:
            log.error(nb_addr)
            return False
        else:
            payload['interface'] = nb_interface['id']
    ipaddr = _add('ipam', 'ip-addresses', payload)
    if ipaddr:
        return {'ipam': {'ip-addresses': payload}}
    else:
        return ipaddr


def delete_ipaddress(ipaddr_id):
    '''
    .. versionadded:: 2019.2.0

    Delete an IP address. IP addresses in Netbox are a combination of address
    and the interface it is assigned to.

    id
        The Netbox id for the IP address.

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.delete_ipaddress 9002
    '''

    nb_ipaddr = _get('ipam', 'ip-addresses', auth_required=True, id=ipaddr_id)
    if nb_ipaddr:
        nb_ipaddr.delete()
        return {'DELETE': {'ipam': {'ip-address': ipaddr_id}}}
    return False


def create_circuit_provider(name, asn=None):
    '''
    .. versionadded:: 2019.2.0

    Create a new Netbox circuit provider

    name
        The name of the circuit provider
    asn
        The ASN of the circuit provider

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_circuit_provider Telia 1299
    '''

    nb_circuit_provider = get_('circuits', 'providers', name=name)
    payload = {}
    if nb_circuit_provider:
        if nb_circuit_provider['asn'] == asn:
            return False
        else:
            log.error('Duplicate provider with different ASN: %s: %s', name, asn)
            raise CommandExecutionError(
                'Duplicate provider with different ASN: {}: {}'.format(name, asn)
            )
    else:
        payload = {
            'name': name,
            'slug': slugify(name)
        }
        if asn:
            payload['asn'] = asn
        circuit_provider = _add('circuits', 'providers', payload)
        if circuit_provider:
            return {'circuits': {'providers': {circuit_provider['id']: payload}}}
        else:
            return circuit_provider


def get_circuit_provider(name, asn=None):
    '''
    .. versionadded:: 2019.2.0

    Get a circuit provider with a given name and optional ASN.

    name
        The name of the circuit provider
    asn
        The ASN of the circuit provider

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.get_circuit_provider Telia 1299
    '''
    if asn:
        nb_circuit_provider = get_('circuits', 'providers', asn=asn)
    else:
        nb_circuit_provider = get_('circuits', 'providers', name=name)
    return nb_circuit_provider


def create_circuit_type(name):
    '''
    .. versionadded:: 2019.2.0

    Create a new Netbox circuit type.

    name
        The name of the circuit type

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_circuit_type Transit
    '''
    nb_circuit_type = get_('circuits', 'circuit-types', slug=slugify(name))
    if nb_circuit_type:
        return False
    else:
        payload = {
            'name': name,
            'slug': slugify(name)
        }
        circuit_type = _add('circuits', 'circuit-types', payload)
        if circuit_type:
            return {'circuits': {'circuit-types': {circuit_type['id']: payload}}}
        else:
            return circuit_type


def create_circuit(name, provider_id, circuit_type, description=None):
    '''
    .. versionadded:: 2019.2.0

    Create a new Netbox circuit

    name
        Name of the circuit
    provider_id
        The netbox id of the circuit provider
    circuit_type
        The name of the circuit type
    asn
        The ASN of the circuit provider
    description
        The description of the circuit

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_circuit NEW_CIRCUIT_01 Telia Transit 1299 "New Telia circuit"
    '''

    nb_circuit_provider = get_('circuits', 'providers', provider_id)
    nb_circuit_type = get_('circuits', 'circuit-types', slug=slugify(circuit_type))
    if nb_circuit_provider and nb_circuit_type:
        payload = {
            'cid': name,
            'provider': nb_circuit_provider['id'],
            'type': nb_circuit_type['id']
        }
        if description:
            payload['description'] = description
        nb_circuit = get_('circuits', 'circuits', cid=name)
        if nb_circuit:
            return False
        circuit = _add('circuits', 'circuits', payload)
        if circuit:
            return {'circuits': {'circuits': {circuit['id']: payload}}}
        else:
            return circuit
    else:
        return False


def create_circuit_termination(circuit, interface, device, speed, xconnect_id=None, term_side='A'):
    '''
    .. versionadded:: 2019.2.0

    Terminate a circuit on an interface

    circuit
        The name of the circuit
    interface
        The name of the interface to terminate on
    device
        The name of the device the interface belongs to
    speed
        The speed of the circuit, in Kbps
    xconnect_id
        The cross-connect identifier
    term_side
        The side of the circuit termination

    CLI Example:

    .. code-block:: bash

        salt myminion netbox.create_circuit_termination NEW_CIRCUIT_01 xe-0/0/1 myminion 10000 xconnect_id=XCON01
    '''

    nb_device = get_('dcim', 'devices', name=device)
    nb_interface = get_('dcim', 'interfaces', device_id=nb_device['id'], name=interface)
    nb_circuit = get_('circuits', 'circuits', cid=circuit)
    if nb_circuit and nb_device:
        nb_termination = get_('circuits',
                             'circuit-terminations',
                             q=nb_circuit['cid'])
        if nb_termination:
            return False
        payload = {
            'circuit': nb_circuit['id'],
            'interface': nb_interface['id'],
            'site': nb_device['site']['id'],
            'port_speed': speed,
            'term_side': term_side
        }
        if xconnect_id:
            payload['xconnect_id'] = xconnect_id
        circuit_termination = _add('circuits', 'circuit-terminations', payload)
        if circuit_termination:
            return {'circuits': {'circuit-terminations': {circuit_termination['id']: payload}}}
        else:
            return circuit_termination