Skip to content
Snippets Groups Projects
Commit 3679b7ac authored by Release Webservice's avatar Release Webservice
Browse files

Finished release 0.61.

parents 085ae7c3 5742a0ad
No related branches found
No related tags found
No related merge requests found
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [0.61] - 2021-03-05
- POL1-380: added /poller/speeds route
## [0.60] - 2021-03-04 ## [0.60] - 2021-03-04
- DBOARD3-409: ignore inactive peerings and interfaces - DBOARD3-409: ignore inactive peerings and interfaces
- DBOARD3-410: handle overlapping/duplicate subnets - DBOARD3-410: handle overlapping/duplicate subnets
......
...@@ -6,7 +6,15 @@ BRIAN support Endpoints ...@@ -6,7 +6,15 @@ BRIAN support Endpoints
These endpoints are intended for use by BRIAN. These endpoints are intended for use by BRIAN.
/poller/interfaces .. contents:: :local:
/poller/interfaces</hostname>
--------------------------------- ---------------------------------
.. autofunction:: inventory_provider.routes.poller.interfaces .. autofunction:: inventory_provider.routes.poller.interfaces
/poller/speeds</hostname>
---------------------------------
.. autofunction:: inventory_provider.routes.poller.interface_speeds
...@@ -9,6 +9,8 @@ from inventory_provider.routes import common ...@@ -9,6 +9,8 @@ from inventory_provider.routes import common
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
routes = Blueprint('poller-support-routes', __name__) routes = Blueprint('poller-support-routes', __name__)
Gb = 1 << 30
INTERFACE_LIST_SCHEMA = { INTERFACE_LIST_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#', '$schema': 'http://json-schema.org/draft-07/schema#',
...@@ -59,6 +61,26 @@ INTERFACE_LIST_SCHEMA = { ...@@ -59,6 +61,26 @@ INTERFACE_LIST_SCHEMA = {
'items': {'$ref': '#/definitions/interface'} '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'}
}
@routes.after_request @routes.after_request
def after_request(resp): def after_request(resp):
...@@ -150,42 +172,83 @@ def _load_interfaces(hostname): ...@@ -150,42 +172,83 @@ def _load_interfaces(hostname):
'name': ifc['name'], 'name': ifc['name'],
'bundle': ifc['bundle'], 'bundle': ifc['bundle'],
'bundle-parents': [], 'bundle-parents': [],
'snmp-index': -1,
'description': ifc['description'], 'description': ifc['description'],
'circuits': [] 'circuits': []
} }
def _load_poller_interfaces(hostname=None): def _add_bundle_parents(interfaces, hostname=None):
"""
generator that adds bundle-parents info to each interface.
snmp_indexes = _load_snmp_indexes(hostname) :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(hostname) bundles = _load_interface_bundles(hostname)
services = _load_services(hostname) for ifc in interfaces:
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) router_bundle = bundles.get(ifc['router'], None)
if router_bundle: if router_bundle:
base_ifc = ifc['name'].split('.')[0] base_ifc = ifc['name'].split('.')[0]
ifc['bundle-parents'] = router_bundle.get(base_ifc, []) 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
"""
services = _load_services(hostname)
for ifc in interfaces:
router_services = services.get(ifc['router'], None) router_services = services.get(ifc['router'], None)
if router_services: if router_services:
ifc['circuits'] = router_services.get(ifc['name'], []) ifc['circuits'] = router_services.get(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 = _load_snmp_indexes(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 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
"""
basic_interfaces = _load_interfaces(hostname)
with_bundles = _add_bundle_parents(basic_interfaces, hostname)
with_circuits = _add_circuits(with_bundles, hostname)
with_snmp = _add_snmp_indexes(with_circuits, hostname)
def _has_snmp_index(ifc):
return 'snmp-index' in ifc
# only return interfaces that can be polled
return filter(_has_snmp_index, with_snmp)
@routes.route("/interfaces", methods=['GET', 'POST']) @routes.route("/interfaces", methods=['GET', 'POST'])
@routes.route('/interfaces/<hostname>', methods=['GET', 'POST']) @routes.route('/interfaces/<hostname>', methods=['GET', 'POST'])
@common.require_accepts_json @common.require_accepts_json
...@@ -216,7 +279,103 @@ def interfaces(hostname=None): ...@@ -216,7 +279,103 @@ def interfaces(hostname=None):
if result: if result:
result = result.decode('utf-8') result = result.decode('utf-8')
else: else:
result = list(_load_poller_interfaces(hostname)) 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
"""
basic_interfaces = _load_interfaces(hostname)
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 = r.get(cache_key)
if result:
result = result.decode('utf-8')
else:
result = list(_load_interfaces_and_speeds(hostname))
if not result: if not result:
return Response( return Response(
response='no interfaces found', response='no interfaces found',
......
...@@ -2,7 +2,7 @@ from setuptools import setup, find_packages ...@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name='inventory-provider', name='inventory-provider',
version="0.60", version="0.61",
author='GEANT', author='GEANT',
author_email='swd@geant.org', author_email='swd@geant.org',
description='Dashboard inventory provider', description='Dashboard inventory provider',
......
import json import json
import jsonschema import jsonschema
from inventory_provider.routes.poller import INTERFACE_LIST_SCHEMA from inventory_provider.routes.poller \
import INTERFACE_LIST_SCHEMA, INTERFACE_SPEED_LIST_SCHEMA
DEFAULT_REQUEST_HEADERS = { DEFAULT_REQUEST_HEADERS = {
"Content-type": "application/json", "Content-type": "application/json",
...@@ -19,3 +20,16 @@ def test_router_interfaces(router, client): ...@@ -19,3 +20,16 @@ def test_router_interfaces(router, client):
assert response # at least shouldn't be empty assert response # at least shouldn't be empty
response_routers = {ifc['router'] for ifc in response} response_routers = {ifc['router'] for ifc in response}
assert response_routers == {router} assert response_routers == {router}
def test_router_interface_speeds(router, client):
rv = client.post(
f'/poller/speeds/{router}',
headers=DEFAULT_REQUEST_HEADERS)
assert rv.status_code == 200
response = json.loads(rv.data.decode("utf-8"))
jsonschema.validate(response, INTERFACE_SPEED_LIST_SCHEMA)
assert response # at least shouldn't be empty
response_routers = {ifc['router'] for ifc in response}
assert response_routers == {router}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment