Skip to content
Snippets Groups Projects
msr.py 11.61 KiB
"""
MSR Support Endpoints
=========================

These endpoints are intended for use by MSR.

.. contents:: :local:

/msr/access-services
---------------------------------

.. autofunction:: inventory_provider.routes.msr.access_services


/msr/bgp/logical-systems
-------------------------------------

.. autofunction:: inventory_provider.routes.msr.get_logical_systems


/msr/bgp/logical-system-peerings</name>
------------------------------------------

.. autofunction:: inventory_provider.routes.msr.logical_system_peerings


/msr/bgp/groups
-------------------------------------

.. autofunction:: inventory_provider.routes.msr.get_peering_groups


/msr/bgp/group-peerings</name>
-------------------------------------

.. autofunction:: inventory_provider.routes.msr.bgp_group_peerings


/msr/bgp/routing-instances
-------------------------------------

.. autofunction:: inventory_provider.routes.msr.get_peering_routing_instances


/msr/bgp/routing-instance-peeringns</name>
--------------------------------------------

.. autofunction:: inventory_provider.routes.msr.bgp_routing_instance_peerings

helpers
-------------------------------------

.. autofunction:: inventory_provider.routes.msr._handle_peering_group_list_request

.. autofunction:: inventory_provider.routes.msr._handle_peering_group_request

"""
import itertools
import json

from flask import Blueprint, Response, request

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

routes = Blueprint("msr-query-routes", __name__)


ACCESS_SERVICES_LIST_SCHEMA = {
    "$schema": "http://json-schema.org/draft-07/schema#",

    "definitions": {
        "service": {
            "type": "object",
            "properties": {
                "id": {"type": "integer"},
                "name": {"type": "string"},
                "equipment": {"type": "string"},
                "pop_name": {"type": "string"},
                "other_end_equipment": {"type": "string"},
                "other_end_pop_name": {"type": "string"},
                "speed_value": {"type": "integer"},
                "speed_unit": {"type": "string"}
            },
            "required": [
                "id", "name",
                "pop_name", "equipment",
                "other_end_pop_name", "other_end_equipment",
                "speed_value", "speed_unit"
            ],
            "additionalProperties": False
        }
    },

    "type": "array",
    "items": {"$ref": "#/definitions/service"}
}

PEERING_GROUP_LIST_SCHEMA = {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "array",
    "items": {"type": "string"}

}

PEERING_LIST_SCHEMA = {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "definitions": {
        "peering-instance": {
            "type": "object",
            "properties": {
                "address": {"type": "string"},
                "description": {"type": "string"},
                "logical-system": {"type": "string"},
                "group": {"type": "string"},
                "hostname": {"type": "string"},
                "remote-asn": {"type": "integer"},
                "local-asn": {"type": "integer"},
                "instance": {"type": "string"}
            },
            # only vrr peerings have remote-asn
            # only group peerings have local-asn or instance
            # not all group peerings have 'description'
            # and only vrr or vpn-proxy peerings are within a logical system
            "required": [
                "address",
                "group",
                "hostname"],
            "additionalProperties": False
        }
    },
    "type": "array",
    "items": {"$ref": "#/definitions/peering-instance"}
}


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


@routes.route("/access-services", methods=['GET', 'POST'])
@common.require_accepts_json
def access_services():
    """
    Handler for `/msr/access-services`.

    This method is in development, not yet used.

    The response will be formatted according to the following schema:

    .. asjson::
       inventory_provider.routes.msr.ACCESS_SERVICES_LIST_SCHEMA

    :return:
    """
    redis = common.get_current_redis()

    def _services():
        for k in redis.scan_iter('ims:access_services:*'):
            service = redis.get(k.decode('utf-8')).decode('utf-8')
            yield json.loads(service)

    cache_key = 'classifier-cache:msr:access-services'

    result = _ignore_cache_or_retrieve(request, cache_key, redis)

    if not result:
        result = list(_services())

        if not result:
            return Response(
                response='no access services found',
                status=404,
                mimetype="text/html")

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

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


def _handle_peering_group_request(name, cache_key, group_key_base):
    """
    Common method for used by
    :meth:`inventory_provider.routes.msr.logical_system_peerings` and
    :meth:`inventory_provider.routes.msr.bgp_group_peerings`.

    This method will return a list of all peerings configured
    for the specified group `name on any router,
    or for all group names if `name` None.

    The response will be formatted according to the following schema:

    .. asjson::
       inventory_provider.routes.msr.PEERING_LIST_SCHEMA

    :param name: group/logical-system name, or None
    :param cache_key: base cache key for this type of request
    :param group_key_base: key above which the peerings are grouped
    :return: a json list, formatted as above
    """

    r = common.get_current_redis()

    def _get_all_subkeys():
        keys = []
        for k in r.scan_iter(f'{group_key_base}:*', count=1000):
            keys.append(k.decode('utf-8'))
        return keys

    def _load_list_items(key):
        value = r.get(key)
        if value:
            yield from json.loads(value.decode('utf-8'))

    if name:
        cache_key = f'{cache_key}:{name}'

    items = _ignore_cache_or_retrieve(request, cache_key, r)

    if not items:
        if name:
            items = list(_load_list_items(f'{group_key_base}:{name}'))
        else:
            gen_list = list(map(_load_list_items, _get_all_subkeys()))
            items = list(itertools.chain(*gen_list))

        if not items:
            return Response(
                response='no peerings found',
                status=404,
                mimetype="text/html")
        items = json.dumps(items)

        r.set(cache_key, items.encode('utf-8'))

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


@routes.route("/bgp/logical-system-peerings", methods=['GET', 'POST'])
@routes.route("/bgp/logical-system-peerings/<name>", methods=['GET', 'POST'])
@common.require_accepts_json
def logical_system_peerings(name=None):
    """
    Handler for `/msr/bgp/logical-system-peerings`

    This method will return a list of all peerings configured
    for the requested logical-system name on any router, or for any
    logical system if no parameter is given.

    :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_request`
    """  # noqa: E501
    return _handle_peering_group_request(
        name=name,
        cache_key='classifier-cache:msr:logical-system-peerings',
        group_key_base='juniper-peerings:logical-system')


@routes.route("/bgp/group-peerings", methods=['GET', 'POST'])
@routes.route("/bgp/group-peerings/<name>", methods=['GET', 'POST'])
@common.require_accepts_json
def bgp_group_peerings(name=None):
    """
    Handler for `/msr/bgp/group-peerings`

    This method will return a list of all peerings configured
    for the requested group name on any router, or for any
    group system if no parameter is given.

    :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_request`
    """  # noqa: E501
    return _handle_peering_group_request(
        name=name,
        cache_key='classifier-cache:msr:group-peerings',
        group_key_base='juniper-peerings:group')


@routes.route("/bgp/routing-instance-peerings", methods=['GET', 'POST'])
@routes.route("/bgp/routing-instance-peerings/<name>", methods=['GET', 'POST'])
@common.require_accepts_json
def bgp_routing_instance_peerings(name=None):
    """
    Handler for `/msr/bgp/routing-instance-peerings`

    This method will return a list of all peerings configured
    for the requested routing-instance name on any router, or for any
    routing instance if no parameter is given.

    :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_request`
    """  # noqa: E501
    return _handle_peering_group_request(
        name=name,
        cache_key='classifier-cache:msr:routing-instance-peerings',
        group_key_base='juniper-peerings:routing-instance')


def _handle_peering_group_list_request(cache_key, group_key_base):
    """
    Common method for used by
    :meth:`inventory_provider.routes.msr.get_logical_systems` and
    :meth:`inventory_provider.routes.msr.get_peering_groups`.

    This method will return a list of all immediate subkeys of
     `group_key_base`.

    The response will be formatted according to the following schema:

    .. asjson::
       inventory_provider.routes.msr.PEERING_GROUP_LIST_SCHEMA

    :param cache_key: base cache key for this type of request
    :param group_key_base: key above which the peerings are grouped
    :return: a json list, formatted as above
    """

    r = common.get_current_redis()

    def _get_all_subkeys():
        for k in r.scan_iter(f'{group_key_base}:*', count=1000):
            k = k.decode('utf-8')
            yield k[len(group_key_base) + 1:]

    names = _ignore_cache_or_retrieve(request, cache_key, r)

    if not names:
        names = list(_get_all_subkeys())
        if not names:
            return Response(
                response='no groups found',
                status=404,
                mimetype="text/html")
        names = json.dumps(sorted(names))

        r.set(cache_key, names.encode('utf-8'))

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


@routes.route("/bgp/logical-systems", methods=['GET', 'POST'])
@common.require_accepts_json
def get_logical_systems():
    """
    Handler for `/msr/bgp/logical-systems`

    Returns a list of logical system names for which peering
    information is available.

    :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_list_request`
    """  # noqa: E501
    return _handle_peering_group_list_request(
        cache_key='classifier-cache:msr:logical-systems',
        group_key_base='juniper-peerings:logical-system')


@routes.route("/bgp/groups", methods=['GET', 'POST'])
@common.require_accepts_json
def get_peering_groups():
    """
    Handler for `/msr/bgp/groups`

    Returns a list of group names for which peering
    information is available.

    :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_list_request`
    """  # noqa: E501
    return _handle_peering_group_list_request(
        cache_key='classifier-cache:msr:peering-groups',
        group_key_base='juniper-peerings:group')


@routes.route("/bgp/routing-instances", methods=['GET', 'POST'])
@common.require_accepts_json
def get_peering_routing_instances():
    """
    Handler for `/msr/bgp/routing-instances`

    Returns a list of routing-instance names for which peering
    information is available.

    :return: see :meth:`inventory_provider.routes.msr._handle_peering_group_list_request`
    """  # noqa: E501
    return _handle_peering_group_list_request(
        cache_key='classifier-cache:msr:routing-instances',
        group_key_base='juniper-peerings:routing-instance')