Skip to content
Snippets Groups Projects
poller.py 7.14 KiB
import json
import logging
import re

from flask import Blueprint, Response, current_app
from inventory_provider import juniper
from inventory_provider.routes import common

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

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

    'definitions': {
        'service': {
            'type': 'object',
            'properties': {
                'id': {'type': 'integer'},
                'name': {'type': 'string'},
                'type': {'type': 'string'},
                'status': {'type': 'string'},
            },
            'required': ['id', 'name', 'type', 'status'],
            'additionalProperties': False
        },
        'interface': {
            'type': 'object',
            'properties': {
                'router': {'type': 'string'},
                'name': {'type': 'string'},
                'description': {'type': 'string'},
                'snmp-index': {
                    'type': 'integer',
                    'minimum': 1
                },
                'bundle': {
                    'type': 'array',
                    'items': {'type': 'string'}
                },
                'bundle-parents': {
                    'type': 'array',
                    'items': {'type': 'string'}
                },
                'circuits': {
                    'type': 'array',
                    'items': {'$ref': '#/definitions/service'}
                }
            },
            'required': [
                'router', 'name', 'description',
                'snmp-index', 'bundle', 'bundle-parents',
                'circuits'],
            'additionalProperties': False
        },
    },

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


@routes.after_request
def after_request(resp):
    return common.after_request(resp)


def _load_snmp_indexes(hostname=None):
    result = dict()
    key_pattern = f'snmp-interfaces:{hostname}*' \
        if hostname else 'snmp-interfaces:*'

    for doc in common.load_json_docs(
            config_params=current_app.config['INVENTORY_PROVIDER_CONFIG'],
            key_pattern=key_pattern):
        router = doc['key'][len('snmp-interfaces:'):]
        result[router] = {e['name']: e for e in doc['value']}

    return result


def _load_interface_bundles(hostname=None):
    result = dict()
    key_pattern = f'netconf-interface-bundles:{hostname}:*' \
        if hostname else 'netconf-interface-bundles:*'

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

        m = re.match(r'^netconf-interface-bundles:([^:]+):(.+)', doc['key'])
        assert m

        router = m.group(1)
        interface = m.group(2)
        result.setdefault(router, dict())
        result[router][interface] = doc['value']

    return result


def _load_services(hostname=None):
    result = dict()
    key_pattern = f'opsdb:interface_services:{hostname}:*' \
        if hostname else 'opsdb:interface_services:*'

    def _service_params(full_service_info):
        return {
            'id': full_service_info['id'],
            'name': full_service_info['name'],
            'type': full_service_info['service_type'],
            'status': full_service_info['status']
        }

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

        m = re.match(r'^opsdb:interface_services:([^:]+):(.+)', doc['key'])
        if not m:
            logger.warning(f'can\'t parse redis service key {doc["key"]}')
            # there are some weird records (dtn*, dp1*)
            continue

        router = m.group(1)
        interface = m.group(2)
        result.setdefault(router, dict())
        result[router][interface] = [_service_params(s) for s in doc['value']]

    return result


def _load_interfaces(hostname):
    key_pattern = f'netconf:{hostname}*' if hostname else 'netconf:*'
    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'][len('netconf:'):]

        for ifc in juniper.list_interfaces(doc['value']):
            if not ifc['description']:
                continue

            yield {
                'router': router,
                'name': ifc['name'],
                'bundle': ifc['bundle'],
                'bundle-parents': [],
                'snmp-index': -1,
                'description': ifc['description'],
                'circuits': []
            }


def _load_poller_interfaces(hostname=None):

    snmp_indexes = _load_snmp_indexes(hostname)
    bundles = _load_interface_bundles(hostname)
    services = _load_services(hostname)

    for ifc in _load_interfaces(hostname):

        router_snmp = snmp_indexes.get(ifc['router'], None)
        if not router_snmp or ifc['name'] not in router_snmp:
            # there's no way to poll this interface
            continue
        ifc['snmp-index'] = router_snmp[ifc['name']]['index']
        # TODO: uncomment this when it won't break poller-admin-service
        # not urgent ... it looks empirically like all logical-system
        #                interfaces are repeated for both communities
        # ifc['snmp-community'] = router_snmp[ifc['name']]['community']

        router_bundle = bundles.get(ifc['router'], None)
        if router_bundle:
            base_ifc = ifc['name'].split('.')[0]
            ifc['bundle-parents'] = router_bundle.get(base_ifc, [])

        router_services = services.get(ifc['router'], None)
        if router_services:
            ifc['circuits'] = router_services.get(ifc['name'], [])

        yield ifc


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

    The response is a list of information for all
    interfaces that should be polled, including service
    information and snmp information.

    .. asjson::
       inventory_provider.routes.poller.INTERFACE_LIST_SCHEMA

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

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

    r = common.get_current_redis()

    result = r.get(cache_key)
    if result:
        result = result.decode('utf-8')
    else:
        result = list(_load_poller_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")