-
Sam Roberts authoredSam Roberts authored
poller.py 45.40 KiB
"""
BRIAN support Endpoints
=========================
These endpoints are intended for use by BRIAN.
.. contents:: :local:
/poller/interfaces</hostname>
---------------------------------
.. autofunction:: inventory_provider.routes.poller.interfaces
/poller/speeds</hostname>
---------------------------------
.. autofunction:: inventory_provider.routes.poller.interface_speeds
/poller/eumetsat-multicast
---------------------------------
.. autofunction:: inventory_provider.routes.poller.eumetsat_multicast
/poller/gws/direct
---------------------------------
.. autofunction:: inventory_provider.routes.poller.gws_direct
/poller/gws/direct-config
---------------------------------
.. autofunction:: inventory_provider.routes.poller.gws_direct_config
/poller/gws/indirect
---------------------------------
.. autofunction:: inventory_provider.routes.poller.gws_indirect
/poller/services</service-type>
---------------------------------
.. autofunction:: inventory_provider.routes.poller.get_services
/poller/service-types
---------------------------------
.. autofunction:: inventory_provider.routes.poller.get_service_types
support method: _get_dashboards
---------------------------------
.. autofunction:: inventory_provider.routes.poller._get_dashboards
"""
from enum import Enum, auto
import itertools
import json
import logging
import re
from flask import Blueprint, Response, current_app, request, jsonify
from inventory_provider import juniper
from inventory_provider.routes import common
from inventory_provider.tasks.common import ims_sorted_service_type_key
from inventory_provider.routes.classifier import get_ims_equipment_name, \
get_ims_interface
from inventory_provider.routes.common import _ignore_cache_or_retrieve
logger = logging.getLogger(__name__)
routes = Blueprint('poller-support-routes', __name__)
Gb = 1 << 30
class INTERFACE_TYPES(Enum):
UNKNOWN = auto()
LOGICAL = auto()
PHYSICAL = auto()
AGGREGATE = auto()
class BRIAN_DASHBOARDS(Enum):
CLS = auto()
RE_PEER = auto()
RE_CUST = auto()
GEANTOPEN = auto()
GCS = auto()
L2_CIRCUIT = auto()
LHCONE_PEER = auto()
LHCONE_CUST = auto()
MDVPN_CUSTOMERS = auto()
INFRASTRUCTURE_BACKBONE = auto()
IAS_PRIVATE = auto()
IAS_PUBLIC = auto()
IAS_CUSTOMER = auto()
IAS_UPSTREAM = auto()
GWS_PHY_UPSTREAM = auto()
# aggregate dashboards
CLS_PEERS = auto()
IAS_PEERS = auto()
GWS_UPSTREAMS = auto()
LHCONE = auto()
CAE1 = auto()
# NREN customer
NREN = auto()
# COPERNICUS
COPERNICUS = auto()
# only used in INTERFACE_LIST_SCHEMA and sphinx docs
_DASHBOARD_IDS = [d.name for d in list(BRIAN_DASHBOARDS)]
_INTERFACE_TYPES = [i.name for i in list(INTERFACE_TYPES)]
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'}
},
'dashboards': {
'type': 'array',
'items': {'enum': _DASHBOARD_IDS}
},
'dashboard_info': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'interface_type': {'enum': _INTERFACE_TYPES}
},
'required': ['name', 'interface_type'],
'additionalProperties': False
}
},
'required': [
'router', 'name', 'description',
'snmp-index', 'bundle', 'bundle-parents',
'circuits', 'dashboards'],
'additionalProperties': False
},
},
'type': 'array',
'items': {'$ref': '#/definitions/interface'}
}
INTERFACE_SPEED_LIST_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'interface': {
'type': 'object',
'properties': {
'router': {'type': 'string'},
'name': {'type': 'string'},
'speed': {'type': 'integer'}
},
'required': ['router', 'name', 'speed'],
'additionalProperties': False
},
},
'type': 'array',
'items': {'$ref': '#/definitions/interface'}
}
MULTICAST_SUBSCRIPTION_LIST_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'ipv4-address': {
'type': 'string',
'pattern': r'^(\d+\.){3}\d+$'
},
'subscription': {
'type': 'object',
'properties': {
'router': {'type': 'string'},
'subscription': {'$ref': '#/definitions/ipv4-address'},
'endpoint': {'$ref': '#/definitions/ipv4-address'},
'oid': {
'type': 'string',
'pattern': r'^(\d+\.)*\d+$'
},
'community': {'type': 'string'}
},
'required': [
'router', 'subscription', 'endpoint', 'oid', 'community'],
'additionalProperties': False
},
},
'type': 'array',
'items': {'$ref': '#/definitions/subscription'}
}
GWS_DIRECT_DATA_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'oid': {
'type': 'string',
'pattern': r'^(\d+\.)*\d+$'
},
'snmp-v2': {
'type': 'object',
'properties': {
'community': {'type': 'string'}
},
'required': ['community'],
'additionalProperties': False
},
'snmp-v3-cred': {
'type': 'object',
'properties': {
'protocol': {'enum': ['MD5', 'DES']},
'password': {'type': 'string'}
},
'required': ['protocol', 'password'],
'additionalProperties': False
},
'snmp-v3': {
'type': 'object',
'properties': {
'sec-name': {'type': 'string'},
'auth': {'$ref': '#/definitions/snmp-v3-cred'},
'priv': {'$ref': '#/definitions/snmp-v3-cred'}
},
'required': ['sec-name'],
'additionalProperties': False
},
'counter': {
'type': 'object',
'properties': {
'field': {
'enum': [
'discards_in',
'discards_out',
'errors_in',
'errors_out',
'traffic_in',
'traffic_out'
]
},
'oid': {'$ref': '#/definitions/oid'},
'snmp': {
'oneOf': [
{'$ref': '#/definitions/snmp-v2'},
{'$ref': '#/definitions/snmp-v3'}
]
}
},
'required': ['field', 'oid', 'snmp'],
'additionalProperties': False
},
'interface-counters': {
'type': 'object',
'properties': {
'nren': {'type': 'string'},
'isp': {
'type': 'string',
'enum': ['Cogent', 'Telia', 'CenturyLink']
},
'hostname': {'type': 'string'},
'tag': {'type': 'string'},
'counters': {
'type': 'array',
'items': {'$ref': '#/definitions/counter'},
'minItems': 1
},
'info': {'type': 'string'}
},
'required': [
'nren', 'isp', 'hostname', 'tag', 'counters'],
'additionalProperties': False
}
},
'type': 'array',
'items': {'$ref': '#/definitions/interface-counters'}
}
SERVICES_LIST_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'oid': {
'type': 'string',
'pattern': r'^(\d+\.)*\d+$'
},
'counter-field': {
'type': 'object',
'properties': {
'field': {
'type': 'string',
'enum': ['egressOctets', 'ingressOctets']
},
'oid': {'$ref': '#/definitions/oid'}
},
'required': ['field', 'oid'],
'additionalProperties': False
},
'snmp-info': {
'type': 'object',
'properties': {
'ifIndex': {'type': 'integer'},
'community': {'type': 'string'},
'counters': {
'type': 'array',
'items': {'$ref': '#/definitions/counter-field'},
'minItems': 1
}
},
'required': ['ifIndex', 'community'],
'additionalProperties': False
},
'service': {
'type': 'object',
'properties': {
'id': {'type': 'integer'},
'name': {'type': 'string'},
'customer': {'type': 'string'},
'speed': {'type': 'integer'},
'pop': {'type': 'string'},
'hostname': {'type': 'string'},
'interface': {'type': 'string'},
'type': {'type': 'string'},
'status': {'type': 'string'},
'snmp': {'$ref': '#/definitions/snmp-info'}
},
'required': [
'id', 'name', 'customer',
'speed', 'pop', 'hostname',
'interface', 'type', 'status'],
'additionalProperties': False
}
},
'type': 'array',
'items': {'$ref': '#/definitions/service'}
}
STRING_LIST_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'type': 'array',
'items': {'type': 'string'}
}
@routes.after_request
def after_request(resp):
return common.after_request(resp)
def _get_dashboards(interface):
"""
Yield enums from BRIAN_DASHBOARDS to indicate which dashboards
this interface should be included in.
cf. POL1-482
Possible dashboard id's are:
.. asjson::
inventory_provider.routes.poller._DASHBOARD_IDS
:param interface: a dict with keys like router, name, description
:return: generator that yields enums from BRIAN_DASHBOARDS
"""
router = interface.get('router', '').lower()
ifc_name = interface.get('name', '')
description = interface.get('description', '').strip()
if 'SRV_L3VPN' in description and re.search(r'COPERNICUS.*?\|', description, flags=re.IGNORECASE):
yield BRIAN_DASHBOARDS.COPERNICUS
if 'SRV_CLS' in description:
yield BRIAN_DASHBOARDS.CLS
if 'SRV_CLS PRIVATE' in description:
yield BRIAN_DASHBOARDS.CLS_PEERS
if 'SRV_IAS PUBLIC' in description:
yield BRIAN_DASHBOARDS.IAS_PUBLIC
yield BRIAN_DASHBOARDS.IAS_PEERS
if 'SRV_IAS PRIVATE' in description:
yield BRIAN_DASHBOARDS.IAS_PRIVATE
yield BRIAN_DASHBOARDS.IAS_PEERS
if 'SRV_IAS CUSTOMER' in description:
yield BRIAN_DASHBOARDS.IAS_CUSTOMER
if 'SRV_IAS UPSTREAM' in description:
yield BRIAN_DASHBOARDS.IAS_UPSTREAM
if re.match('(SRV_GLOBAL|SRV_L3VPN|LAG) RE_INTERCONNECT', description):
yield BRIAN_DASHBOARDS.RE_PEER
if re.match(r'(PHY|LAG|SRV_GLOBAL) CUSTOMER', description):
yield BRIAN_DASHBOARDS.RE_CUST
if re.match('^SRV_GCS', description):
yield BRIAN_DASHBOARDS.GCS
if 'GEANTOPEN' in description:
yield BRIAN_DASHBOARDS.GEANTOPEN
if 'SRV_L2CIRCUIT' in description:
yield BRIAN_DASHBOARDS.L2_CIRCUIT
if 'LHCONE' in description:
if 'SRV_L3VPN RE' in description:
yield BRIAN_DASHBOARDS.LHCONE_PEER
if 'SRV_L3VPN CUSTOMER' in description:
yield BRIAN_DASHBOARDS.LHCONE_CUST
if re.match('SRV_L3VPN (CUSTOMER|RE_INTERCONNECT)', description):
yield BRIAN_DASHBOARDS.LHCONE
if re.match('^SRV_MDVPN CUSTOMER', description):
yield BRIAN_DASHBOARDS.MDVPN_CUSTOMERS
if re.match('(SRV_GLOBAL|LAG|PHY) INFRASTRUCTURE BACKBONE', description):
yield BRIAN_DASHBOARDS.INFRASTRUCTURE_BACKBONE
if router == 'mx1.lon.uk.geant.net' \
and re.match(r'^ae12(\.\d+|$)$', ifc_name):
yield BRIAN_DASHBOARDS.CAE1
if re.match('^PHY UPSTREAM', description):
yield BRIAN_DASHBOARDS.GWS_PHY_UPSTREAM
regex = r'(PHY|LAG|(SRV_(GLOBAL|LHCONE|MDVPN|IAS|CLS|L3VPN))) CUSTOMER'
if re.match(regex, description):
yield BRIAN_DASHBOARDS.NREN
def _get_dashboard_data(ifc):
def _get_interface_type(description):
if re.match(r'^PHY', description):
return INTERFACE_TYPES.PHYSICAL
if re.match(r'^SRV_', description):
return INTERFACE_TYPES.LOGICAL
if re.match(r'^LAG', description):
return INTERFACE_TYPES.AGGREGATE
return INTERFACE_TYPES.UNKNOWN
description = ifc.get('description', '').strip()
dashboards = ifc.get('dashboards', [])
interface_type = _get_interface_type(description)
if len(dashboards) == 0:
return ifc
def _get_customer_name(description):
name = description.split(' ')
if len(name) >= 3:
return name[2].strip().upper()
else:
# if the description isn't properly formatted
# use it as the name to make it obvious something is wrong
return description
def _get_backbone_name(description):
name = description.split('|')
if len(name) >= 2:
name = name[1].strip()
return name.replace('( ', '(')
else:
# if the description isn't properly formatted
# use it as the name to make it obvious something is wrong
return description
if BRIAN_DASHBOARDS.INFRASTRUCTURE_BACKBONE.name in dashboards:
name = _get_backbone_name(description)
elif BRIAN_DASHBOARDS.GWS_PHY_UPSTREAM.name in dashboards:
name = _get_customer_name(description)
host = ifc['router']
location = host.split('.')[1].upper()
name = f'{name} - {location}'
else:
name = _get_customer_name(description)
return {
**ifc,
'dashboard_info': {
'name': name,
'interface_type': interface_type.name
}
}
def _add_dashboards(interfaces):
"""
generator that dashboards to each interfaces.
:param interfaces: result of _load_interfaces
:return: generator with `dashboards` populated in each element
"""
for ifc in interfaces:
dashboards = _get_dashboards(ifc)
ifc['dashboards'] = sorted([d.name for d in dashboards])
yield _get_dashboard_data(ifc)
def _load_interface_bundles(config, hostname=None, use_next_redis=False):
result = dict()
def _load_docs(key_pattern):
for doc in common.load_json_docs(
config_params=config,
key_pattern=key_pattern,
num_threads=20,
use_next_redis=use_next_redis):
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']
base_key = 'netconf-interface-bundles'
base_key_pattern = f'{base_key}:{hostname}:*' \
if hostname else f'{base_key}:*'
_load_docs(base_key_pattern)
_load_docs(f'lab:{base_key_pattern}')
return result
def _load_services(config, hostname=None, use_next_redis=False):
# if hostname:
# hostname = get_ims_equipment_name(hostname)
result = dict()
key_pattern = f'ims:interface_services:{hostname}:*' \
if hostname else 'ims:interface_services:*'
def _filter_and_format_services(_services):
included_service_ids = set()
for s in _services:
if s['id'] in included_service_ids:
continue
if s['circuit_type'] == 'service':
included_service_ids.add(s['id'])
yield {
'id': s['id'],
'name': s['name'],
'type': s['service_type'],
'status': s['status']
}
for doc in common.load_json_docs(
config_params=config,
key_pattern=key_pattern,
num_threads=20,
use_next_redis=use_next_redis):
m = re.match(r'^ims: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] = \
list(_filter_and_format_services(doc['value']))
return result
def _load_netconf_docs(
config, filter_pattern, use_next_redis=False):
"""
yields dicts like:
{
'router': router hostname
'netconf': loaded netconf xml doc
}
:param config: app config
:param filter_pattern: search filter, including 'netconf:'
:param use_next_redis: use next instead of current redis, if true
:return: yields netconf docs, formatted as above
"""
m = re.match(r'^(.*netconf:).+', filter_pattern)
# TODO: probably better to not required netconf: to be passed in
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=config,
key_pattern=filter_pattern,
num_threads=10,
use_next_redis=use_next_redis):
yield {
'router': doc['key'][key_prefix_len:],
'netconf': doc['value']
}
def _load_interfaces(
config, hostname=None, no_lab=False, use_next_redis=False):
"""
loads basic interface data for production & lab routers
:param config:
:param hostname:
:param use_next_redis:
:return:
"""
def _load_docs(key_pattern):
for doc in _load_netconf_docs(config, key_pattern, use_next_redis):
for ifc in juniper.list_interfaces(doc['netconf']):
if not ifc['description']:
continue
yield {
'router': doc['router'],
'name': ifc['name'],
'bundle': ifc['bundle'],
'bundle-parents': [],
'description': ifc['description'],
'circuits': []
}
base_key_pattern = f'netconf:{hostname}*' if hostname else 'netconf:*'
yield from _load_docs(base_key_pattern)
if not no_lab:
yield from _load_docs(f'lab:{base_key_pattern}')
def _add_bundle_parents(interfaces, hostname=None):
"""
generator that adds bundle-parents info to each interface.
:param interfaces: result of _load_interfaces
:param hostname: hostname or None for all
:return: generator with bundle-parents populated in each element
"""
bundles = _load_interface_bundles(
current_app.config['INVENTORY_PROVIDER_CONFIG'], hostname)
for ifc in interfaces:
router_bundle = bundles.get(ifc['router'], None)
if router_bundle:
base_ifc = ifc['name'].split('.')[0]
ifc['bundle-parents'] = router_bundle.get(base_ifc, [])
yield ifc
def _add_circuits(interfaces, hostname=None):
"""
generator that adds service info to each interface.
:param interfaces: result of _load_interfaces
:param hostname: hostname or None for all
:return: generator with 'circuits' populated in each element, if present
"""
if hostname:
hostname = get_ims_equipment_name(hostname)
services = _load_services(
current_app.config['INVENTORY_PROVIDER_CONFIG'], hostname=hostname)
for ifc in interfaces:
router_services = services.get(
get_ims_equipment_name(ifc['router']), None)
if router_services:
ifc['circuits'] = router_services.get(
get_ims_interface(ifc['name']), []
)
yield ifc
def _add_snmp_indexes(interfaces, hostname=None):
"""
generator that adds snmp info to each interface, if available
:param interfaces: result of _load_interfaces
:param hostname: hostname or None for all
:return: generator with 'snmp-index' optionally added to each element
"""
snmp_indexes = common.load_snmp_indexes(
current_app.config['INVENTORY_PROVIDER_CONFIG'], hostname)
for ifc in interfaces:
router_snmp = snmp_indexes.get(ifc['router'], None)
if router_snmp and ifc['name'] in router_snmp:
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']
yield ifc
def _load_interfaces_to_poll(hostname=None):
"""
prepares the result of a call to /interfaces
:param hostname: hostname or None for all
:return: generator yielding interface elements
"""
no_lab = common.get_bool_request_arg('no-lab', False)
basic_interfaces = _load_interfaces(
current_app.config['INVENTORY_PROVIDER_CONFIG'],
hostname,
no_lab=no_lab)
# basic_interfaces = list(basic_interfaces)
with_bundles = _add_bundle_parents(basic_interfaces, hostname)
with_circuits = _add_circuits(with_bundles, hostname)
# with_circuits = list(with_circuits)
with_snmp = _add_snmp_indexes(with_circuits, hostname)
# with_snmp = list(with_snmp)
# only return interfaces that can be polled
def _has_snmp_index(ifc):
return 'snmp-index' in ifc
to_poll = filter(_has_snmp_index, with_snmp)
return _add_dashboards(to_poll)
@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 optional `no-lab` parameter omits lab routers
if it's truthiness evaluates to True.
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
:meth:`inventory_provider.routes.poller._get_services`
is where dashboard mappings is handled.
: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'
no_lab = common.get_bool_request_arg('no-lab', False)
if no_lab:
cache_key = f'{cache_key}:no_lab'
r = common.get_current_redis()
result = _ignore_cache_or_retrieve(request, cache_key, r)
if not result:
result = list(_load_interfaces_to_poll(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")
def interface_speed(ifc):
"""
Return the maximum bits per second expected for the given interface.
cf. https://www.juniper.net/documentation/us/en/software/
vmx/vmx-getting-started/topics/task/
vmx-chassis-interface-type-configuring.html
:param ifc:
:return: an integer bits per second
"""
def _name_to_speed(ifc_name):
if ifc_name.startswith('ge'):
return Gb
if ifc_name.startswith('xe'):
return 10 * Gb
if ifc_name.startswith('et'):
return 100 * Gb
logger.warning(f'unrecognized interface name: {ifc_name}')
return -1
if ifc['bundle-parents']:
if not ifc['name'].startswith('ae'):
logger.warning(
f'ifc has bundle-parents, but name is {ifc["name"]}')
return sum(_name_to_speed(name) for name in ifc['bundle-parents'])
return _name_to_speed(ifc['name'])
def _load_interfaces_and_speeds(hostname=None):
"""
prepares the result of a call to /speeds
:param hostname: hostname or None for all
:return: generator yielding interface elements
"""
no_lab = common.get_bool_request_arg('no-lab', False)
basic_interfaces = _load_interfaces(
current_app.config['INVENTORY_PROVIDER_CONFIG'],
hostname,
no_lab=no_lab)
with_bundles = _add_bundle_parents(basic_interfaces, hostname)
def _result_ifc(ifc):
return {
'router': ifc['router'],
'name': ifc['name'],
'speed': interface_speed(ifc)
}
return map(_result_ifc, with_bundles)
@routes.route("/speeds", methods=['GET', 'POST'])
@routes.route('/speeds/<hostname>', methods=['GET', 'POST'])
@common.require_accepts_json
def interface_speeds(hostname=None):
"""
Handler for `/poller/speeds` and
`/poller/speeds/<hostname>`
which returns information for either all interfaces
or those on the requested hostname.
The response is a list of maximum speed information (in bits
per second) for all known interfaces.
*speed <= 0 means the max interface speed can't be determined*
.. asjson::
inventory_provider.routes.poller.INTERFACE_SPEED_LIST_SCHEMA
:param hostname: optional, if present should be a router hostname
:return:
"""
cache_key = f'classifier-cache:poller-interface-speeds:{hostname}' \
if hostname else 'classifier-cache:poller-interface-speeds:all'
r = common.get_current_redis()
result = _ignore_cache_or_retrieve(request, cache_key, r)
if not result:
result = list(_load_interfaces_and_speeds(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")
def _load_community_strings(base_key_pattern):
for doc in _load_netconf_docs(
config=current_app.config['INVENTORY_PROVIDER_CONFIG'],
filter_pattern=base_key_pattern):
community = juniper.snmp_community_string(doc['netconf'])
if not community:
yield {
'router': doc['router'],
'error':
f'error extracting community string for {doc["router"]}'
}
else:
yield {
'router': doc['router'],
'community': community
}
@routes.route('/eumetsat-multicast', methods=['GET', 'POST'])
@routes.route('/eumetsat-multicast/<hostname>', methods=['GET', 'POST'])
@common.require_accepts_json
def eumetsat_multicast(hostname=None):
"""
Handler for `/poller/eumetsat-multicast</hostname>` which returns
information about multicast subscriptions.
The hostname parameter is optional. If it is present, only hostnames
matching `hostname*` are returned. If not present, data for all
`mx*` routers is returned.
The response is a list of oid/router/community structures that all
all subscription octet counters to be polled.
.. asjson::
inventory_provider.routes.poller.MULTICAST_SUBSCRIPTION_LIST_SCHEMA
This method returns essentially hard-coded data,
based on the information in POL1-395.
:return:
"""
SUBSCRIPTIONS = [{
'subscription': f'232.223.222.{idx}',
'endpoint': '193.17.9.3',
} for idx in range(1, 73)]
SUBSCRIPTIONS.append(
{'subscription': '232.223.223.1', 'endpoint': '193.17.9.7'})
SUBSCRIPTIONS.append(
{'subscription': '232.223.223.22', 'endpoint': '193.17.9.7'})
def _oid(sub):
return ('1.3.6.1.2.1.83.1.1.2.1.16'
f'.{sub["subscription"]}.{sub["endpoint"]}'
'.255.255.255.255')
r = common.get_current_redis()
cache_key = 'classifier-cache:poller-eumetsat-multicast'
if hostname:
cache_key = f'{cache_key}:{hostname}'
result = r.get(cache_key)
if result:
result = result.decode('utf-8')
else:
def _multicast_oids(router_info):
def _rsp_element(sub):
result = {
'router': router_info['router'],
'oid': _oid(sub),
'community': router_info['community']
}
result.update(sub)
return result
yield from map(_rsp_element, SUBSCRIPTIONS)
routers = list(_load_community_strings(
base_key_pattern=f'netconf:{hostname}*'
if hostname else 'netconf:mx*'))
errors = list(filter(lambda x: 'error' in x, routers))
if errors:
errors = [e['error'] for e in errors]
return Response(
response=', '.join(errors),
status=403, # forbidden
mimetype='text/html')
assert all('community' in r for r in routers) # sanity
result = list(map(_multicast_oids, routers))
result = itertools.chain(*result)
result = list(result)
if not result:
target = hostname or 'any routers!'
return Response(
response=f'no multicast config for {target}',
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")
@routes.route("/gws/direct", methods=['GET', 'POST'])
@common.require_accepts_json
def gws_direct():
"""
Handler for `/poller/gws/direct` which returns required for polling
customer equipment counters for ISP connetions.
The response is a list of nren/isp/counter structures that must be
polled.
.. asjson::
inventory_provider.routes.poller.GWS_DIRECT_DATA_SCHEMA
WARNING: interface tags in the `gws-direct` section of the config data
should be unique for each nren/isp combination. i.e. if there
are multiple community strings in use for a particular host, then please
keep the interface tags unique.
:return:
"""
cache_key = 'classifier-cache:gws-direct'
r = common.get_current_redis()
result = r.get(cache_key)
if result:
result = result.decode('utf-8')
else:
def _interfaces():
config_params = current_app.config['INVENTORY_PROVIDER_CONFIG']
for nren_isp in config_params['gws-direct']:
for host in nren_isp['hosts']:
snmp_params = {}
if 'community' in host:
# (snmp v2)
# sanity (already guaranteed by schema check)
assert 'sec-name' not in host
snmp_params['community'] = host['community']
else:
# (snmp v3)
# sanity (already guaranteed by schema check)
assert 'sec-name' in host
snmp_params['sec-name'] = host['sec-name']
if 'auth' in host:
snmp_params['auth'] = host['auth']
if 'priv' in host:
snmp_params['priv'] = host['priv']
for ifc in host['interfaces']:
ifc_data = {
'nren': nren_isp['nren'],
'isp': nren_isp['isp'],
'hostname': host['hostname'],
'tag': ifc['tag'],
'counters': [
{
'field': k,
'oid': v,
'snmp': snmp_params
}
for k, v in ifc['counters'].items()]
}
if 'info' in ifc:
ifc_data['info'] = ifc['info']
yield ifc_data
result = json.dumps(list(_interfaces()))
# cache this data for the next call
r.set(cache_key, result.encode('utf-8'))
return Response(result, mimetype="application/json")
# cf. https://gitlab.geant.net/puppet-apps/cacti/-/blob/production/files/scripts/juniper-firewall-dws.pl # noqa: E501
JNX_DCU_STATS_BYTES_OID = '1.3.6.1.4.1.2636.3.6.2.1.5'
JNX_FW_COUNTER_BYTES_OID = '1.3.6.1.4.1.2636.3.5.2.1.5'
JNX_ADDRESS_FAMILY = {
'ipv4': 1,
'ipv6': 2
}
JNX_FW_COUNTER_TYPE = {
'other': 1,
'counter': 2,
'policer': 3
}
def _str2oid(s):
chars = '.'.join(str(ord(c)) for c in s)
return f'{len(s)}.{chars}'
def _jnx_dcu_byte_count_oid(
ifIndex,
class_name='dws-in',
address_family=JNX_ADDRESS_FAMILY['ipv4']):
# sanity checks (in case of programming errors)
assert isinstance(ifIndex, int)
assert isinstance(class_name, str)
assert isinstance(address_family, int)
return '.'.join([
JNX_DCU_STATS_BYTES_OID,
str(ifIndex),
str(address_family),
_str2oid(class_name)
])
def _jnx_fw_counter_bytes_oid(
customer,
interface_name,
filter_name=None,
counter_name=None):
# sanity checks (in case of programming errors)
assert isinstance(customer, str)
assert isinstance(interface_name, str)
assert filter_name is None or isinstance(filter_name, str)
assert counter_name is None or isinstance(counter_name, str)
if filter_name is None:
filter_name = f'nren_IAS_{customer}_OUT-{interface_name}-o'
if counter_name is None:
counter_name = f'DWS-out-{interface_name}-o'
return '.'.join([
JNX_FW_COUNTER_BYTES_OID,
_str2oid(filter_name),
_str2oid(counter_name),
str(JNX_FW_COUNTER_TYPE['counter'])
])
def _get_services_internal(service_type=None):
"""
Performs the lookup and caching done for calls to
`/poller/services</service-type>`
This is a separate private utility so that it can be called by
:meth:`inventory_provider.routes.poller.get_services`
and :meth:`inventory_provider.routes.poller.get_service_types`
The response will be formatted according to the following schema:
.. asjson::
inventory_provider.routes.poller.SERVICES_LIST_SCHEMA
:param service_type: a service type, or None to return all
:return: service list, json-serialized to a string
"""
return_all = common.get_bool_request_arg('all', False)
include_snmp = common.get_bool_request_arg('snmp', False)
def _services():
key_pattern = f'ims:services:{service_type}:*' \
if service_type else 'ims:services:*'
for doc in common.load_json_docs(
config_params=current_app.config['INVENTORY_PROVIDER_CONFIG'],
key_pattern=key_pattern,
num_threads=20):
yield doc['value']
def _add_snmp(s):
all_snmp_info = common.load_snmp_indexes(
current_app.config['INVENTORY_PROVIDER_CONFIG'], )
snmp_interfaces = all_snmp_info.get(s['hostname'], {})
interface_info = snmp_interfaces.get(s['interface'], None)
if interface_info:
s['snmp'] = {
'ifIndex': interface_info['index'],
'community': interface_info['community'],
}
if s['type'] == 'GWS - INDIRECT':
s['snmp']['counters'] = [
{
'field': 'ingressOctets',
'oid': _jnx_dcu_byte_count_oid(
interface_info['index']),
},
{
'field': 'egressOctets',
'oid': _jnx_fw_counter_bytes_oid(
s['customer'], s['interface'])
}
]
return s
def _wanted_in_output(s):
return return_all or (s['status'].lower() == 'operational')
def _format_services(s):
return {
'id': s['id'],
'name': s['name'],
'customer': s['project'],
'speed': s['speed_value'],
'pop': s['here']['pop']['name'],
'hostname': common.ims_equipment_to_hostname(
s['here']['equipment']),
'interface': s['here']['port'].lower(),
'type': s['type'],
'status': s['status']
}
cache_key = f'classifier-cache:poller:services:{service_type}' \
if service_type else 'classifier-cache:poller:services:all-types'
if return_all:
cache_key = f'{cache_key}:all'
if include_snmp:
cache_key = f'{cache_key}:snmp'
redis = common.get_current_redis()
result = _ignore_cache_or_retrieve(request, cache_key, redis)
if not result:
result = _services()
result = filter(_wanted_in_output, result)
result = map(_format_services, result)
if include_snmp:
result = map(_add_snmp, result)
result = list(result)
if not result:
return None
# cache this data for the next call
result = json.dumps(result)
redis.set(cache_key, result)
return result
@routes.route('/services', methods=['GET', 'POST'])
@routes.route('/services/<service_type>', methods=['GET', 'POST'])
@common.require_accepts_json
def get_services(service_type=None):
"""
Handler for `/poller/services</service-type>`
Use `/poller/service-types` for possible values of `service-type`.
If the endpoint is called with no `service-type`,
all services are returned.
Supported url parameters:
:all: Default is False to return only operational services.
If present and evaluates to True, then return all services.
:snmp: If present and evalutes to True, add snmp interface polling
params to the response.
(sphinx bug: the param names aren't capitalized)
The response will be formatted according to the following schema:
.. asjson::
inventory_provider.routes.poller.SERVICES_LIST_SCHEMA
A few possible values for `service_type` are (currently): gws_internal,
geant_ip, geant_lambda, l3_vpn, md_vpn_proxy (there are more)
If the endpoint is called with no `service_type`,
all services are returned
:return:
"""
services_json_str = _get_services_internal(service_type)
if not services_json_str:
message = f'no {service_type} services found' \
if service_type else 'no services found'
return Response(
response=message,
status=404,
mimetype='text/html')
return Response(services_json_str, mimetype='application/json')
@routes.route("/gws/indirect", methods=['GET', 'POST'])
@common.require_accepts_json
def gws_indirect():
"""
Handler for `/poller/gws/indirect`
Same as `/poller/services/gws_indirect`
cf. :meth:`inventory_provider.routes.poller.get_services`
:return:
"""
return get_services(service_type='gws_indirect')
@routes.route('/service-types', methods=['GET', 'POST'])
@common.require_accepts_json
def get_service_types():
"""
Handler for `/poller/service-types`
This method returns a list of all values of `service_type` that
can be used with `/poller/services/service-type` to return
a non-empty list of services.
Adding a truthy `all` request parameter (e.g. `?all=1`) will
return also return valid service types for which none of the
defined services are operational.
The response will be formatted according to the following schema:
.. asjson::
inventory_provider.routes.poller.STRING_LIST_SCHEMA
"""
cache_key = 'classifier-cache:poller:service-types'
return_all = common.get_bool_request_arg('all', False)
if return_all:
cache_key = f'{cache_key}-all'
redis = common.get_current_redis()
service_types = _ignore_cache_or_retrieve(request, cache_key, redis)
if not service_types:
all_services = json.loads(_get_services_internal())
service_types = {
ims_sorted_service_type_key(s['type'])
for s in all_services
}
if not service_types:
return Response(
response='no service types found',
status=404,
mimetype='text/html')
# cache this data for the next call
service_types = sorted(list(service_types))
service_types = json.dumps(service_types)
redis.set(cache_key, service_types)
return Response(service_types, mimetype='application/json')
@routes.route('/gws/direct-config', methods=['GET', 'POST'])
def gws_direct_config():
"""
Handler for `/poller/gws/direct-config` which returns
the basic gws-direct config.
This api is only intended for config validation.
:return:
"""
format = request.args.get('format', default='json', type=str)
format = format.lower()
if format not in ('html', 'json'):
return Response(
response='format must be one of: html, json',
status=400,
mimetype="text/html")
def _counters():
config_params = current_app.config['INVENTORY_PROVIDER_CONFIG']
for nren_isp in config_params['gws-direct']:
for host in nren_isp['hosts']:
snmp_version = '2' if 'community' in host.keys() else '3'
for ifc in host['interfaces']:
for field, oid in ifc['counters'].items():
yield {
'nren': nren_isp['nren'],
'isp': nren_isp['isp'],
'hostname': host['hostname'],
'snmp': snmp_version,
'interface': ifc['tag'],
'field': field,
'oid': oid,
'info': ifc.get('info', '')
}
if format == 'json':
if not request.accept_mimetypes.accept_json:
return Response(
response="response will be json",
status=406,
mimetype="text/html")
else:
return jsonify(list(_counters()))
if not request.accept_mimetypes.accept_html:
return Response(
response="response will be html",
status=406,
mimetype="text/html")
page = '''<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>GWS Direct config</title>
</head>
<body>
<table class="table table-striped">
<thead>
{header_row}
</thead>
<tbody>
{data_rows}
</tbody>
</table>
</body>
</html>''' # noqa: E501
def _to_row(counter, header=False):
_columns = (
'nren', 'isp', 'hostname',
'snmp', 'interface', 'field', 'oid', 'info')
elems = ['<tr>']
for name in _columns:
if header:
elems.append(f'<th scope="col">{name}</th>')
else:
elems.append(f'<td scope="row">{counter[name]}</td>')
elems.append('</tr>')
return ''.join(elems)
header_row = _to_row(None, header=True)
data_rows = map(_to_row, _counters())
page = page.format(
header_row=header_row,
data_rows='\n'.join(data_rows))
return Response(
response=page,
status=200,
mimetype="text/html")