diff --git a/salt/modules/netbox.py b/salt/modules/netbox.py index d71c153c0985..0c45cd230d24 100644 --- a/salt/modules/netbox.py +++ b/salt/modules/netbox.py @@ -22,25 +22,31 @@ .. versionadded:: 2018.3.0 ''' +from __future__ import absolute_import, unicode_literals, print_function -from __future__ import absolute_import, print_function, unicode_literals +import re import logging from salt.exceptions import CommandExecutionError -from salt.utils.args import clean_kwargs - -log = logging.getLogger(__name__) 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__(): ''' @@ -82,10 +88,71 @@ def _strip_url_field(input_dict): return input_dict -def filter(app, endpoint, **kwargs): +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("{}, {}, {}".format(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 @@ -93,17 +160,24 @@ def filter(app, endpoint, **kwargs): ret = [] nb = _nb_obj(auth_required=True if app in AUTH_ENDPOINTS else False) nb_query = getattr(getattr(nb, app), endpoint).filter( - **clean_kwargs(**kwargs) + **__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): +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 @@ -117,10 +191,974 @@ def get(app, endpoint, id=None, **kwargs): salt myminion netbox.get dcim devices name=my-router ''' - nb = _nb_obj(auth_required=True if app in AUTH_ENDPOINTS else False) - if id: - return dict(getattr(getattr(nb, app), endpoint).get(id)) + return _dict(_get(app, endpoint, id=id, auth_required=True if app in AUTH_ENDPOINTS else False, **kwargs)) + + +def create_manufacturer(name): + ''' + .. versionadded:: Fluorine + + 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: - return dict( - getattr(getattr(nb, app), endpoint).get(**clean_kwargs(**kwargs)) - ) + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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("{}, {}, {}".format(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:: Fluorine + + 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("{}, {}, {}".format(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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + Return a dictionary structured as standardised in the + `openconfig-interfaces `_ + 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:: Fluorine + + Return a dictionary structured as standardised in the + `openconfig-lacp `_ + 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 `_ + and `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:: Fluorine + + 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:: Fluorine + + 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("{}, {}, {}".format(e.req.request.headers, e.request_body, e.error)) + return False + + +def delete_interface(device_name, interface_name): + ''' + .. versionadded:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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:: Fluorine + + 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 diff --git a/tests/unit/modules/test_netbox.py b/tests/unit/modules/test_netbox.py index bd38f5c62666..d408b74e086b 100644 --- a/tests/unit/modules/test_netbox.py +++ b/tests/unit/modules/test_netbox.py @@ -34,6 +34,20 @@ } +def mocked_clean_kwargs_filter(**kwargs): + ''' + Mocked args.clean_kwargs for filter tests + ''' + return {'site': u'test'} + + +def mocked_clean_kwargs_get(**kwargs): + ''' + Mocked args.clean_kwargs for get tests + ''' + return {'name': u'test'} + + @skipIf(HAS_PYNETBOX is False, 'pynetbox lib not installed') @skipIf(NO_MOCK, NO_MOCK_REASON) @patch('salt.modules.netbox._config', MagicMock()) @@ -46,27 +60,30 @@ def setup_loader_modules(self): def test_get_by_id(self): with patch('pynetbox.api', MagicMock()) as mock: - netbox.get('dcim', 'devices', id=1) - self.assertEqual( - mock.mock_calls[1], - call().dcim.devices.get(1) - ) + with patch.dict(netbox.__utils__, {'args.clean_kwargs': mocked_clean_kwargs_get}): + netbox.get_('dcim', 'devices', id=1) + self.assertEqual( + mock.mock_calls[1], + call().dcim.devices.get(1) + ) def test_get_by_name(self): with patch('pynetbox.api', MagicMock()) as mock: - netbox.get('dcim', 'devices', name='test') - self.assertEqual( - mock.mock_calls[1], - call().dcim.devices.get(name='test') - ) + with patch.dict(netbox.__utils__, {'args.clean_kwargs': mocked_clean_kwargs_get}): + netbox.get_('dcim', 'devices', name='test') + self.assertEqual( + mock.mock_calls[1], + call().dcim.devices.get(name='test') + ) def test_filter_by_site(self): with patch('pynetbox.api', MagicMock()) as mock: - netbox.filter('dcim', 'devices', site='test') - self.assertEqual( - mock.mock_calls[1], - call().dcim.devices.filter(site='test') - ) + with patch.dict(netbox.__utils__, {'args.clean_kwargs': mocked_clean_kwargs_filter}): + netbox.filter_('dcim', 'devices', site='test') + self.assertEqual( + mock.mock_calls[1], + call().dcim.devices.filter(site='test') + ) def test_filter_url(self): strip_url = netbox._strip_url_field(NETBOX_RESPONSE_STUB) @@ -76,14 +93,16 @@ def test_filter_url(self): def test_get_secret(self): with patch('pynetbox.api', MagicMock()) as mock: - netbox.get('secrets', 'secrets', name='test') - self.assertTrue( - 'token' and 'private_key_file' in mock.call_args[1] - ) + with patch.dict(netbox.__utils__, {'args.clean_kwargs': mocked_clean_kwargs_get}): + netbox.get_('secrets', 'secrets', name='test') + self.assertTrue( + 'token' and 'private_key_file' in mock.call_args[1] + ) def test_token_present(self): with patch('pynetbox.api', MagicMock()) as mock: - netbox.get('dcim', 'devices', name='test') - self.assertTrue( - 'token' in mock.call_args[1] - ) + with patch.dict(netbox.__utils__, {'args.clean_kwargs': mocked_clean_kwargs_get}): + netbox.get_('dcim', 'devices', name='test') + self.assertTrue( + 'token' in mock.call_args[1] + )