Skip to content
Snippets Groups Projects
lnetd.py 5.13 KiB
"""

LnetD support
=========================

This endpoint is intended for use with LnetD

.. contents:: :local:

/LnetD/interfaces</hostname>
---------------------------------

.. autofunction:: inventory_provider.routes.lnetd.interfaces

"""
import json
import logging
import re

from flask import Blueprint, Response, current_app, request

from inventory_provider import juniper
from inventory_provider.routes import common
from inventory_provider.routes.common import _ignore_cache_or_retrieve

logger = logging.getLogger(__name__)
routes = Blueprint('lnetd-support-routes', __name__)

INTERFACE_LIST_SCHEMA = {
    '$schema': 'http://json-schema.org/draft-07/schema#',

    'definitions': {

        'ipv4-interface-address': {
            'type': 'string',
            'pattern': r'^(\d+\.){3}\d+/\d+$'
        },
        'ipv6-interface-address': {
            'type': 'string',
            'pattern': r'^[a-f\d:]+/\d+$'
        },

        'interface': {
            'type': 'object',
            'properties': {
                'hostname': {'type': 'string'},
                'interface': {'type': 'string'},
                'ifIndex': {'type': 'integer', 'minimum': 1},
                'ipv4': {
                    'type': 'array',
                    'items': {'$ref': '#/definitions/ipv4-interface-address'}
                },
                'ipv6': {
                    'type': 'array',
                    'items': {'$ref': '#/definitions/ipv6-interface-address'}
                },
            },
            'required': ['hostname', 'interface', 'ifIndex', 'ipv4', 'ipv6'],
            'additionalProperties': False
        },
    },

    'type': 'array',
    'items': {'$ref': '#/definitions/interface'}
}


def _add_snmp_indexes(interfaces, hostname=None):
    """
    generator that adds snmp ifIndex to each interface, if available
    (only interfaces with an snmp index are yielded)

    :param interfaces: result of _load_interfaces
    :param hostname: hostname or None for all
    :return: generator that yields interfaces with 'ifIndex' added
    """
    snmp_indexes = common.load_snmp_indexes(hostname)
    for ifc in interfaces:

        hostname = ifc['hostname']
        if hostname not in snmp_indexes:
            continue

        interface = ifc['interface']
        if interface not in snmp_indexes[hostname]:
            continue

        ifc['ifIndex'] = snmp_indexes[hostname][interface]['index']
        yield ifc


def _load_router_interfaces(hostname):
    """
    loads basic interface data for production & lab routers

    :param hostname:
    :return:
    """
    def _load_docs(key_pattern):

        m = re.match(r'^(.*netconf:).+', key_pattern)
        assert m  # sanity
        key_prefix_len = len(m.group(1))
        assert key_prefix_len >= len('netconf:')  # sanity

        for doc in common.load_xml_docs(
                config_params=current_app.config['INVENTORY_PROVIDER_CONFIG'],
                key_pattern=key_pattern,
                num_threads=10):

            router = doc['key'][key_prefix_len:]

            for ifc in juniper.list_interfaces(doc['value']):
                if not ifc['description']:
                    continue
                # seems we're only interested in interfaces with addresses?
                if not ifc['ipv4'] and not ifc['ipv6']:
                    continue

                yield {
                    'hostname': router,
                    'interface': ifc['name'],
                    'ipv4': ifc['ipv4'],
                    'ipv6': ifc['ipv6']
                }

    base_key_pattern = f'netconf:{hostname}*' if hostname else 'netconf:*'
    yield from _load_docs(base_key_pattern)
    yield from _load_docs(f'lab:{base_key_pattern}')


def _load_interfaces(hostname=None):
    """
    prepares the result of a call to /interfaces

    :param hostname: hostname or None for all
    :return: generator yielding interface elements
    """
    basic_interfaces = _load_router_interfaces(hostname)
    return _add_snmp_indexes(basic_interfaces, hostname)


@routes.route("/interfaces", methods=['GET', 'POST'])
@routes.route('/interfaces/<hostname>', methods=['GET', 'POST'])
@common.require_accepts_json
def interfaces(hostname=None):
    """
    Handler for `/LnetD/interfaces` and
    `/LnetD/interfaces/<hostname>`
    which returns information for either all interfaces
    or those on the requested hostname.

    .. asjson::
       inventory_provider.routes.lnetd.INTERFACE_LIST_SCHEMA

    :param hostname: optional, if present should be a router hostname
    :return:
    """

    cache_key = f'classifier-cache:lnetd-interfaces:{hostname}' \
        if hostname else 'classifier-cache:lnetd-interfaces:all'

    r = common.get_current_redis()

    result = _ignore_cache_or_retrieve(request, cache_key, r)

    if not result:
        result = list(_load_interfaces(hostname))
        if not result:
            return Response(
                response='no interfaces found',
                status=404,
                mimetype='text/html')

        result = json.dumps(result)
        # cache this data for the next call
        r.set(cache_key, result.encode('utf-8'))

    return Response(result, mimetype="application/json")