Source code for _roster.netbox

# -*- coding: utf-8 -*-
"""
Load devices from `NetBox <https://github.com/digitalocean/netbox>`__, and make
them available for salt-ssh or salt-sproxy (or any other program that doesn't
require (Proxy) Minions running).

Make sure that the following options are configured on the Master:

.. code-block:: yaml

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

If you want to pre-filter the devices, so it won't try to pull the whole
database available in NetBox, you can configure another key, ``filters``, under
``netbox``, e.g.,

.. code-block:: yaml

    netbox:
      url: <NETBOX_URL>
      filters:
        site: <SITE>
        status: <STATUS>

.. hint::

    You can use any NetBox field as a filter.

.. important::

    In NetBox v2.6 the default view permissions changed, so ``salt-sproxy`` may
    not able to get the device list from NetBox by default.

    Add ``EXEMPT_VIEW_PERMISSIONS = ['*']`` to the ``configuration.py`` NetBox
    file to change this behavior.
    See https://github.com/netbox-community/netbox/releases/tag/v2.6.0 for more
    information
"""
# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals

import logging

try:
    import pynetbox  # pylint: disable=unused-import

    HAS_PYNETBOX = True
except ImportError:
    HAS_PYNETBOX = False

import salt.utils.args
from salt.exceptions import CommandExecutionError

import salt_sproxy._roster

__virtualname__ = "netbox"

log = logging.getLogger(__name__)

AUTH_ENDPOINTS = ("secrets",)


def __virtual__():
    if not HAS_PYNETBOX:
        return (False, "Please install pynetbox to be able to use the NetBox Roster")
    return __virtualname__


def _setval(key, val, dict_=None, delim=":"):
    """
    Set a value under the dictionary hierarchy identified
    under the key. The target 'foo:bar:baz' returns the
    dictionary hierarchy {'foo': {'bar': {'baz': {}}}}.
    """
    if not dict_:
        dict_ = {}
    prev_hier = dict_
    dict_hier = key.split(delim)
    for each in dict_hier[:-1]:
        if isinstance(each, str):
            if each not in prev_hier:
                prev_hier[each] = {}
            prev_hier = prev_hier[each]
        else:
            prev_hier[each] = [{}]
            prev_hier = prev_hier[each]
    prev_hier[dict_hier[-1]] = val
    return dict_


def _netbox_config():
    config = __opts__.get("netbox")
    if not config:
        raise CommandExecutionError(
            "NetBox configuration could not be found in the Master config"
        )
    return config


def _nb_obj(auth_required=False):
    pynb_kwargs = {}
    nb_config = _netbox_config()
    pynb_kwargs["token"] = nb_config.get("token")
    if auth_required:
        pynb_kwargs["private_key_file"] = nb_config.get("keyfile")
    return pynetbox.api(nb_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 _netbox_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.
    """
    ret = []
    nb = _nb_obj(auth_required=True if app in AUTH_ENDPOINTS else False)
    clean_kwargs = salt.utils.args.clean_kwargs(**kwargs)
    if not clean_kwargs:
        nb_query = getattr(getattr(nb, app), endpoint).all()
    else:
        nb_query = getattr(getattr(nb, app), endpoint).filter(**clean_kwargs)
    if nb_query:
        ret = [_strip_url_field(dict(i)) for i in nb_query]
    return ret


[docs]def targets(tgt, tgt_type="glob", **kwargs): """ Return the targets from NetBox. """ netbox_filters = __opts__.get("netbox", {}).get("filters", {}) netbox_filters.update(**kwargs) filtered = False if tgt_type == "list" or ( tgt_type == "glob" and not any([char in tgt for char in "*?[!"]) ): netbox_filters["name"] = tgt filtered = True elif tgt_type == "grain" and tgt.startswith("netbox:"): levels = tgt.split("netbox:")[1].split(":") if len(levels) > 2: netbox_filters[levels[0]] = _setval(":".join(levels[1:-1]), levels[-1]) filtered = True elif len(levels) == 2: netbox_filters[levels[0]] = levels[1] filtered = True log.debug("Querying NetBox with the following filters") log.debug(netbox_filters) netbox_devices = _netbox_filter("dcim", "devices", **netbox_filters) pool = { device["name"]: {"minion_opts": {"grains": {"netbox": device}}} for device in netbox_devices } if filtered: return pool pool = salt_sproxy._roster.load_cache( pool, __runner__, __opts__, tgt, tgt_type=tgt_type ) engine = salt_sproxy._roster.TGT_FUN[tgt_type] return engine(pool, tgt, opts=__opts__)