diff --git a/README.md b/README.md index b993b2c7d2b04303fde0b357b63686f8868001c7..7067dcb4a7686030b9ed926ed06df5f1c21ee0a6 100644 --- a/README.md +++ b/README.md @@ -1,1688 +1,12 @@ - -* [Inventory Provider](#inventory-provider) - * [Overview](#overview) - * [Configuration](#configuration) - * [Running this module](#running-this-module) - * [Protocol Specification](#protocol-specification) - * [/data/version](#dataversion) - * [/data/routers](#datarouters) - * [/data/interfaces](#datainterfaces) - * [/data/pop](#datapop) - * [/jobs/update](#jobsupdate) - * [/jobs/log](#jobslog) - * [/jobs/reload-router-config](#jobsreload-router-config) - * [/jobs/check-task-status](#jobscheck-task-status) - * [/classifier/juniper-link-info](#classifierjuniper-link-info) - * [/classifier/peer-info](#classifierpeer-info) - * [/classifier/infinera-lambda-info](#classifierinfinera-lambda-info) - * [/classifier/coriant-info](#classifiercoriant-info) - * [/poller/interfaces](#pollerinterfaces) - * [/lg/routers](#lgrouters) - * [/msr/access-services](#msraccess-services) - * [/testing/flushdb](#testingflushdb) - * [/testing/infinera-dna-addresses](#testinginfinera-dna-addresses) - * [/testing/coriant-tnms-addresses](#testingcoriant-tnms-addresses) - * [/testing/juniper-server-addresses](#testingjuniper-server-addresses) - * [/data/snmp](#datasnmp) - * [/data/bgp](#databgp) - * [Backend (Redis) Storage Schema](#backend-redis-storage-schema) - - - # Inventory Provider +The Inventory Provider +acts as a single point of truth for +information about the GÉANT network, exposed by +by an HTTP API. -## Overview - -This module implements a Flask-based webservice which -provides some access to GÉANT network router inventory -information. - -The webservice is communicates with clients over HTTP. -Responses to valid requests are returned as JSON messages. -The server will therefore return an error unless -`application/json` is in the `Accept` request header field. - -HTTP communication and JSON grammar details are -beyond the scope of this document. -Please refer to [RFC 2616](https://tools.ietf.org/html/rfc2616) -and www.json.org for more details. - - -## Configuration - -Several configuration files must be provided when launching -the web service. - -The runtime-accessible filename of the first configuration file -must be stored in the environment variable -`SETTINGS_FILENAME`. - -The configuration file is python and defines a single variable. -The following is an example: - -```python -INVENTORY_PROVIDER_CONFIG_FILENAME = "/somepath/config.json" -ENABLE_TESTING_ROUTES = True -``` - -- `INVENTORY_PROVIDER_CONFIG_FILENAME`: [REQUIRED] Run-time accessible filename -of a json file containing the server configuration parameters. This file -must be formatted according to the following json schema: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "timeout": { - "type": "number", - "maximum": 10, # sanity - "exclusiveMinimum": 0 - }, - "database-credentials": { - "type": "object", - "properties": { - "hostname": {"type": "string"}, - "dbname": {"type": "string"}, - "username": {"type": "string"}, - "password": {"type": "string"} - }, - "required": ["hostname", "dbname", "username", "password"], - "additionalProperties": False - }, - "ssh-credentials": { - "type": "object", - "properties": { - "username": {"type": "string"}, - "private-key": {"type": "string"}, - "known-hosts": {"type": "string"} - }, - "required": ["private-key", "known-hosts"], - "additionalProperties": False - }, - "redis-credentials": { - "type": "object", - "properties": { - "hostname": {"type": "string"}, - "port": {"type": "integer"}, - "socket_timeout": {"$ref": "#/definitions/timeout"} - }, - "required": ["hostname", "port"], - "additionalProperties": False - }, - "redis-sentinel-config": { - "type": "object", - "properties": { - "hostname": {"type": "string"}, - "port": {"type": "integer"}, - "name": {"type": "string"}, - "redis_socket_timeout": {"$ref": "#/definitions/timeout"}, - "sentinel_socket_timeout": {"$ref": "#/definitions/timeout"} - }, - "required": ["hostname", "port", "name"], - "additionalProperties": False - }, - "interface-address": { - "type": "object", - "properties": { - "address": {"type": "string"}, - "network": {"type": "string"}, - "interface": {"type": "string"}, - "router": {"type": "string"} - }, - "required": ["address", "network", "interface", "router"], - "additionalProperties": False - } - }, - - "type": "object", - "properties": { - "ops-db": {"$ref": "#/definitions/database-credentials"}, - "ssh": {"$ref": "#/definitions/ssh-credentials"}, - "redis": {"$ref": "#/definitions/redis-credentials"}, - "sentinel": {"$ref": "#/definitions/redis-sentinel-config"}, - "redis-databases": { - "type": "array", - "minItems": 1, - "items": {"type": "integer"} - }, - "managed-routers": {"type": "string"}, - "unmanaged-interfaces": { - "type": "array", - "items": {"$ref": "#/definitions/interface-address"} - } - }, - "oneOf": [ - { - "required": [ - "ops-db", - "ssh", - "redis", - "redis-databases", - "managed-routers"] - }, - { - "required": [ - "ops-db", - "ssh", - "sentinel", - "redis-databases", - "managed-routers"] - } - ], - "additionalProperties": False - } - ``` - - -- `ENABLE_TESTING_ROUTES`: [OPTIONAL (default value: False)] -Flat (can be any value that evaluates to True) to enable -routes to special utilities used for testing. -*This must never be enabled in a production environment.* - - -## Running this module - -This module has been tested in the following execution environments: - -- As an embedded Flask application. -For example, the application could be launched as follows: +Documentation can be generated by running sphinx: ```bash -$ export FLASK_APP=app.py -$ export SETTINGS_FILENAME=settings.cfg -$ flask run -``` - -- As an Apache/`mod_wsgi` service. - - Details of Apache and `mod_wsgi` - configuration are beyond the scope of this document. - -- As a `gunicorn` wsgi service. - - Details of `gunicorn` configuration are - beyond the scope of this document. - - -## Protocol Specification - -The following resources can be requested from the webservice. - -### resources - -Any non-empty responses are JSON formatted messages. - -#### /data/version - - * /data/version - - The response will be an object - containing the module and protocol versions of the - running server and will be formatted as follows: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "api": { - "type": "string", - "pattern": r'\d+\.\d+' - }, - "module": { - "type": "string", - "pattern": r'\d+\.\d+' - } - }, - "required": ["api", "module"], - "additionalProperties": False - } - ``` - -#### /data/routers - - * /data/routers - - The response will be a list of router hostnames - for which information is available and will be - formatted as follows: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": {"type": "string"} - } - ``` - -#### /data/interfaces - - * /data/interfaces</*`hostname`*> - - The response will be a list of information about - the interfaces present on the requested host - and will be formatted as follows: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "router": {"type": "string"}, - "description": {"type": "string"}, - "ipv4": { - "type": "array", - "items": {"type": "string"} - }, - "ipv6": { - "type": "array", - "items": {"type": "string"} - } - }, - "required": ["name", "description", "router", "ipv4", "ipv6"], - "additionalProperties": False - } - } - ``` - -#### /data/pop - - * /data/pop/*`name`* - - Returns location information for the equipment identified - by `name`. - - The response will be formatted as follows: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - - "definitions": { - "pop-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "abbreviation": {"type": "string"}, - "country": {"type": "string"}, - "city": {"type": "string"}, - "longitude": {"type": "number"}, - "latitude": {"type": "number"} - }, - "required": [ - "name", - "abbreviation", - "country", - "city", - "longitude", - "latitude" - ], - "additionalProperties": False - }, - "equipment-info": { - "type": "object", - "properties": { - 'equipment-name': {"type": "string"}, - 'status': {"type": "string"}, - 'pop': {"$ref": "#/definitions/pop-info"} - }, - "required": [ - "equipment-name", - "status", - "pop" - ], - "additionalProperties": False - - } - }, - - "type": "array", - "items": {"$ref": "#/definitions/equipment-info"} - } - ``` - -#### /jobs/update - - * /jobs/update - - This resource updates the inventory network data for juniper devices. - The function completes asynchronously and a list of outstanding - task id's is returned so the caller can - use `/jobs/check-task-status` to determine when all jobs - are finished. The response will be formatted as follows: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": {"type": "string"} - } - ``` - -#### /jobs/log - - * /jobs/log - - This resource returns the state of the previous (or current) - tasks associated with a call to `/jobs/update`. The response - contains error or warning messages, if any were generated. - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "pending": {"type": "array", "items": {"type": "string"}}, - "errors": {"type": "array", "items": {"type": "string"}}, - "failed": {"type": "array", "items": {"type": "string"}}, - "warnings": {"type": "array", "items": {"type": "string"}}, - }, - "required": ["pending", "errors", "failed", "warnings"], - "additionalProperties": False - } - ``` - -#### /jobs/reload-router-config - - * /jobs/reload-router-config/*`equipment-name`* - - This resource updates the inventory network data for - the identified juniper device. This function completes - asynchronously and returns the same value as - `/jobs/update`, except the return contains exactly - one task id. - -#### /jobs/check-task-status - - * /jobs/check-task-status/*`task-id`* - - This resource returns the current status of - an asynchronous task started by `/jobs/update` - or `jobs/reload-router-config`. The return value - will be formatted as follows: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "id": {"type": "string"}, - "status": {"type": "string"}, - "exception": {"type": "boolean"}, - "ready": {"type": "boolean"}, - "success": {"type": "boolean"}, - "result": {"type": "object"} - }, - "required": ["id", "status", "exception", "ready", "success"], - "additionalProperties": False - } - ``` - -#### /classifier/juniper-link-info - - * /classifier/juniper-link-info/*`source-equipment`*/*`source-interface`* - - The source-equipment is the equipment that causes the trap, not the NMS that - sends it. - - The response will be an object containing the - metadata, formatted according to the following schema: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - - "definitions": { - "location-endpoint": { - "type": "object", - "properties": { - "equipment": {"type": "string"}, - "name": {"type": "string"}, - "abbreviation": {"type": "string"} - }, - "required": ["equipment", "name", "abbreviation"], - "additionalProperties": False - }, - "location": { - "type": "object", - "properties": { - "a": {"$ref": "#/definitions/location-endpoint"}, - "b": {"$ref": "#/definitions/location-endpoint"} - }, - "required": ["a"], - "additionalProperties": False - }, - "locations-list": { - "type": "array", - "items": {"$ref": "#/definitions/location"} - }, - "ip-address": { - "type": "string", - "oneOf": [ - {"pattern": r'^(\d+\.){3}\d+$'}, - {"pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$'} - ] - }, - "ipv4-interface-address": { - "type": "string", - "pattern": r'^(\d+\.){3}\d+/\d+$' - }, - "ipv6-interface-address": { - "type": "string", - "pattern": r'^[a-f\d:]+/\d+$' - }, - "snmp-info": { - "type": "object", - "properties": { - "community": {"type": "string"}, - "index": {"type": "integer"} - }, - "required": ["community", "index"], - "additionalProperties": False - }, - "interface-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "description": {"type": "string"}, - "ipv4": { - "type": "array", - "items": {"$ref": "#/definitions/ipv4-interface-address"} - }, - "ipv6": { - "type": "array", - "items": {"$ref": "#/definitions/ipv6-interface-address"} - }, - "bundle": {"type": "array"}, - "bundle_members": {"type": "array"}, - "snmp": {"$ref": "#/definitions/snmp-info"} - }, - "required": ["name", "description", "ipv4", "ipv6"], - "additionalProperties": False - }, - "service-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "project": {"type": "string"} - "equipment": {"type": "string"}, - "pop": {"type": "string"}, - "pop_abbreviation": {"type": "string"}, - - "other_end_pop": {"type": "string"}, - "other_end_pop_abbreviation": {"type": "string"}, - "other_end_equipment": {"type": "string"}, - "port": {"type": "string"}, - "other_end_port": {"type": "string"}, - "logical_unit": { - "oneOf": [ - {"type": "integer"}, - {"type": "string", "maxLength": 0} - ] - }, - "other_end_logical_unit": { - "oneOf": [ - {"type": "integer"}, - {"type": "string", "maxLength": 0} - ] - }, - "manufacturer": { - "type": "string", - "enum": ["juniper", "coriant", "infinera", - "cisco", "hewlett packard", - "corsa", "graham smith uk ltd", - "unknown", ""] - }, - "card_id": {"type": "string"}, - "other_end_card_id": {"type": "string"}, - "interface_name": {"type": "string"}, - "other_end_interface_name": {"type": "string"}, - - 'other_end_pop_name': {"type": "string"}, - 'pop_name': {"type": "string"} - }, - "additionalProperties": False - }, - "related-service-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "project": {"type": "string"} - }, - "required": ["name", "status", "circuit_type", "project"], - "additionalProperties": False - } - }, - - "type": "object", - "properties": { - "services": { - "type": "array", - "items": {"$ref": "#/definitions/service-info"} - }, - "interface": {"$ref": "#/definitions/interface-info"}, - "related-services": { - "type": "array", - "items": {"$ref": "#/definitions/related-service-info"} - }, - "locations": {"$ref": "#/definitions/locations-list"} - }, - # "required": ["interface"], - "additionalProperties": False - } - ``` - - - `locations`: info about the `a` (and `b`, if relevant) - ends of the circuit - - `services`: info about the service served by this interface - - `interface`: relevant interface parameters - - `related-services`: all top-level services depending on this interface - - -#### /classifier/peer-info - - * /classifier/peer-info/*`address`* - - The `address` parameter should be the ip address of - a remote peer. If this address is found in the system - then information about the interface is returned, otherwise - 404 is returned (or 422 if the address can't be parsed) - - The response will be formatted according to the following syntax: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - - "definitions": { - "location-endpoint": { - "type": "object", - "properties": { - "equipment": {"type": "string"}, - "name": {"type": "string"}, - "abbreviation": {"type": "string"} - }, - "required": ["equipment", "name", "abbreviation"], - "additionalProperties": False - }, - "location": { - "type": "object", - "properties": { - "a": {"$ref": "#/definitions/location-endpoint"}, - "b": {"$ref": "#/definitions/location-endpoint"} - }, - "required": ["a"], - "additionalProperties": False - }, - "locations-list": { - "type": "array", - "items": {"$ref": "#/definitions/location"} - }, - "ip-address": { - "type": "string", - "oneOf": [ - {"pattern": r'^(\d+\.){3}\d+$'}, - {"pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$'} - ] - }, - "interface-address": { - "type": "string", - "oneOf": [ - {"pattern": r'^(\d+\.){3}\d+/\d+$'}, - {"pattern": r'^[a-f\d:]+/\d+$'} - ] - }, - "vpn-rr-peer": { - "type": "object", - "properties": { - "name": {"$ref": "#/definitions/ip-address"}, - "description": {"type": "string"}, - "peer-as": {"type": "integer"}, - "router": {"type": "string"} - }, - "required": ["name", "description"], - "additionalProperties": False - }, - "ix-public-peer": { - "type": "object", - "properties": { - "name": {"$ref": "#/definitions/ip-address"}, - "description": {"type": "string"}, - "router": {"type": "string"}, - "as": { - "type": "object", - "properties": { - "local": {"type": "integer"}, - "peer": {"type": "integer"}, - }, - "required": ["local", "peer"], - "additionalProperties": False - } - }, - "required": ["name", "description", "as"], - "additionalProperties": False - }, - "ix-public-peer-list": { - "type": "array", - "items": {"$ref": "#/definitions/ip-address"} - }, - "ix-public-peer-info": { - "type": "object", - "properties": { - "peer": {"$ref": "#/definitions/ix-public-peer"}, - "group": {"$ref": "#/definitions/ix-public-peer-list"}, - "router": {"$ref": "#/definitions/ix-public-peer-list"} - }, - "required": ["peer", "group", "router"], - "additionalProperties": False - }, - "interface-info": { - "type": "object", - "properties": { - "name": {"$ref": "#/definitions/ip-address"}, - "interface address": { - "$ref": "#/definitions/interface-address"}, - "interface name": {"type": "string"}, - "router": {"type": "string"} - }, - "required": [ - "name", "interface address", "interface name", "router"], - "additionalProperties": False - }, - "service-info": { - "type": "object" - }, - "interface-lookup-info": { - "type": "object", - "properties": { - "interface": {"$ref": "#/definitions/interface-info"}, - "services": { - "type": "array", - "items": {"$ref": "#/definitions/service-info"} - } - } - }, - "snmp-info": { - "type": "object", - "properties": { - "hostname": {"type": "string"}, - "oid": {"type": "string"}, - "community": {"type": "string"} - }, - "required": ["oid", "community", "hostname"], - "additionalProperties": False - }, - "asn-group-member": { - "type": "object", - "properties": { - "router": {"type": "string"}, - "address": {"type": "string"}, - "group": {"type": "string"} - }, - "required": ["router", "address", "group"], - "additionalProperties": False - }, - "asn-group": { - "type": "object", - "properties": { - "asn": {"type": "integer"}, - "peers": { - "type": "array", - "items": {"$ref": "#/definitions/asn-group-member"} - } - }, - "required": ["asn", "peers"], - "additionalProperties": False - } - - }, - - "type": "object", - "properties": { - "ix-public-peer-info": { - "$ref": "#/definitions/ix-public-peer-info"}, - "vpn-rr-peer-info": {"$ref": "#/definitions/vpn-rr-peer"}, - "interfaces": { - "type": "array", - "items": {"$ref": "#/definitions/interface-lookup-info"} - }, - "locations": {"$ref": "#/definitions/locations-list"}, - "snmp": { - "type": "array", - "items": {"$ref": "#/definitions/snmp-info"} - }, - "asn": {"$ref": "#/definitions/asn-group"} - }, - "additionalProperties": False - } - ``` - - - `locations`: info about the `a` (and `b`, if relevant) - ends of the circuit - - `interfaces`: interfaces involved on either end and service info - - `vpn-rr-peer-info`: peering info - - `ix-public-peer-info`: peering info - - -#### /classifier/infinera-lambda-info - - * /classifier/infinera-lambda-info/*`source-equipment`*/*`source-interface`* - - The source-equipment is the equipment that causes the trap, not the NMS that - sends it. - - The response will be an object containing the - metadata, formatted according to the following schema: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - - "definitions": { - "location-endpoint": { - "type": "object", - "properties": { - "equipment": {"type": "string"}, - "name": {"type": "string"}, - "abbreviation": {"type": "string"} - }, - "required": ["equipment", "name", "abbreviation"], - "additionalProperties": False - }, - "location": { - "type": "object", - "properties": { - "a": {"$ref": "#/definitions/location-endpoint"}, - "b": {"$ref": "#/definitions/location-endpoint"} - }, - "required": ["a"], - "additionalProperties": False - }, - "locations-list": { - "type": "array", - "items": {"$ref": "#/definitions/location"} - }, - "service-info": { - "type": "object", - "properties": { - "id": {"type": "integer"}, - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "service_type": {"type": "string"}, - "project": {"type": "string"}, - "pop_name": {"type": "string"}, - "pop_abbreviation": {"type": "string"}, - "other_end_pop_name": {"type": "string"}, - "other_end_pop_abbreviation": {"type": "string"}, - "equipment": {"type": "string"}, - "other_end_equipment": {"type": "string"}, - "port": {"type": "string"}, - "other_end_port": {"type": "string"}, - "logical_unit": { - "oneOf": [ - {"type": "integer"}, - {"type": "string", "maxLength": 0} - ] - }, - "other_end_logical_unit": { - "oneOf": [ - {"type": "integer"}, - {"type": "string", "maxLength": 0} - ] - }, - "manufacturer": { - "type": "string", - "enum": ["juniper", "coriant", "infinera", - "cisco", "hewlett packard", - "corsa", "graham smith uk ltd", - "unknown", ""] - }, - "card_id": {"type": "string"}, - "other_end_card_id": {"type": "string"}, - "interface_name": {"type": "string"}, - "other_end_interface_name": {"type": "string"} - }, - "additionalProperties": False - }, - "related-service-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "project": {"type": "string"} - }, - "required": ["name", "status", "circuit_type", "project"], - "additionalProperties": False - }, - "geant-lambda": { - "type": "object", - "properties": { - "id": {"type": "integer"}, - "name": {"type": "string"}, - "project": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - }, - "additionalProperties": False - } - }, - - "type": "object", - "properties": { - "services": { - "type": "array", - "items": {"$ref": "#/definitions/service-info"} - }, - "related-services": { - "type": "array", - "items": {"$ref": "#/definitions/related-service-info"} - }, - "geant-lambda": { - "$ref": "#/definitions/geant-lambda" - }, - "locations": {"$ref": "#/definitions/locations-list"} - }, - "additionalProperties": False - } - ``` - - - `locations`: info about the `a` (and `b`, if relevant) - ends of the circuit - - `services`: info about any services that use this port - - `related-services`: any top-level services depending on this circuit - - `geant-lambda`: info about the lambda service using this port - - -#### /classifier/coriant-info - - * /classifier/coriant-info/*`equipment name`*/*`entity name`* - - Returns information about the effective path - of a coriant card/port combination. - - The response will be formatted according to the following schema: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - - "definitions": { - "location-endpoint": { - "type": "object", - "properties": { - "equipment": {"type": "string"}, - "name": {"type": "string"}, - "abbreviation": {"type": "string"} - }, - "required": ["equipment", "name", "abbreviation"], - "additionalProperties": False - }, - "location": { - "type": "object", - "properties": { - "a": {"$ref": "#/definitions/location-endpoint"}, - "b": {"$ref": "#/definitions/location-endpoint"} - }, - "required": ["a"], - "additionalProperties": False - }, - "locations-list": { - "type": "array", - "items": {"$ref": "#/definitions/location"} - }, - "pop-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "abbreviation": {"type": "string"}, - "country": {"type": "string"}, - "city": {"type": "string"}, - "longitude": {"type": "number"}, - "latitude": {"type": "number"} - }, - "required": [ - "name", - "abbreviation", - "country", - "city", - "longitude", - "latitude" - ], - "additionalProperties": False - }, - "endpoint": { - "type": "object", - "properties": { - "equipment name": {"type": "string"}, - "card id": {"type": "string"}, - "port number": {"type": "string"}, - "pop": {"$ref": "#/definitions/pop-info"} - }, - "required": ["equipment name", "port number", "pop"], - "additionalProperties": False - }, - "path": { - "type": "object", - "properties": { - 'category': {"type": "string"}, - 'circuit_type': {"type": "string"}, - 'service_type': {"type": "string"}, - 'peering_type': {"type": "string"}, - 'status': {"type": "string"}, - 'name': {"type": "string"}, - 'a': {"$ref": "#/definitions/endpoint"}, - 'b': {"$ref": "#/definitions/endpoint"} - }, - "required": [ - "category", - "circuit_type", - "service_type", - "peering_type", - "status", - "a", - "b"], - "additionalProperties": False - }, - "related-service-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "project": {"type": "string"} - }, - "additionalProperties": False - } - }, - - "type": "object", - "properties": { - "equipment name": {"type": "string"}, - "card id": {"type": "string"}, - "port number": {"type": "string"}, - "path": {"$ref": "#/definitions/path"}, - "related-services": { - "type": "array", - "items": {"$ref": "#/definitions/related-service-info"} - }, - }, - "required": ["equipment name", "card id", "port number"], - "additionalProperties": False - } - ``` - - - `locations`: info about the `a` and `b` ends of the circuit - - `path`: detailed path info (included `a` and `b` ends) - - `related-services`: any top-level services depending on this circuit - - -#### /poller/interfaces - - * /poller/interfaces</*`hostname`*> - - The response will be the list of active interfaces on the - router `hostname`. - Each element of the returned list contains information necessary - for setting up snmp throughput counter polling. - - The response will be formatted according to the following schema: - - ```json - { - "$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"} - } - ``` - -#### /lg/routers - - * /lg/routers/*`access`* - - The response will be the list of routers with pop location information. - The list will be filtered according to the value of the `access` parameter. - - There are 2 valid values for `access`, these are: `public`, `all` - - The response will be formatted according to the following schema: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "pop-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "abbreviation": {"type": "string"}, - "country": {"type": "string"}, - "country code": {"type": "string"}, - "city": {"type": "string"}, - "longitude": {"type": "number"}, - "latitude": {"type": "number"} - }, - "required": [ - "name", - "abbreviation", - "country", - "country code", - "city", - "longitude", - "latitude" - ], - "additionalProperties": False - }, - "router": { - "type": "object", - "properties": { - "equipment name": {"type": "string"}, - "type": { - "type": "string", - "enum": ["INTERNAL", "CORE"] - }, - "pop": {"$ref": "#/definitions/pop-info"} - }, - "required": ["equipment name", "type", "pop"], - "additionalProperties": False - } - }, - - "type": "array", - "items": {"$ref": "#/definitions/router"} - } - ``` - - -#### /msr/access-services - - * /msr/access-services - - The response will be the list of services that - are of type 'IP Acccess', and will formatted - according to the following schema: - - ```json - { - "$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"} - } - ``` - -### Testing utilities - -The following routes are only available if the server -was started with the `ENABLE_TESTING_ROUTES` flag. These routes -are present only for testing, or for deprectated routes that -are intended to be deleted. - - -#### /testing/flushdb - - * /testing/flushdb - - This method erases all data in the backend redis - database. - -#### /testing/infinera-dna-addresses -#### /testing/coriant-tnms-addresses -#### /testing/juniper-server-addresses - - * /testing/infinera-dna-addresses - * /testing/coriant-tnms-addresses - * /testing/juniper-server-addresses - - All of these resources return lists of source addresses - of known senders of snmp traps. Responses will be - formatted as follows: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": {"type": "string"} - } - ``` - - -#### /data/snmp - - * /data/snmp/*`hostname`* - - The response will be a list of information about - the interfaces discovered through snmp - queries on the requested host - and will be formatted as follows: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": { - "type": "object", - "properties": { - "index": {"type": "string"}, - "name": {"type": "string"} - }, - "required": ["index", "name"], - "additionalProperties": False - } - } - ``` - -#### /data/bgp - - * /data/bgp/*`hostname`* - - The response will be a list of information about - the bgp peerings configured for the requested host - and will be formatted as follows: - - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": { - "type": "object", - "properties": { - "description": {"type": "string"}, - "as": { - "type": "object", - "properties": { - "peer": { - "type": "string", - "pattern": r'^\d+$' - }, - "local": { - "type": "string", - "pattern": r'^\d+$' - }, - }, - "required": ["peer", "local"], - "additionalProperties": False - }, - }, - "required": ["description", "as"], - "additionalProperties": False - } - } - ``` - -## Backend (Redis) Storage Schema - -* `netconf:<hostname>` - - * key example - * `netconf:mx1.ams.nl.geant.net` - * value format - * cf. validation in `inventory_provider.juniper.load_config` - -* `snmp-interfaces:<hostname>` - - * key example: - * `snmp-interfaces:mx1.lon2.uk.geant.net` - * value schema - TODO: this needs to be checked ... - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "v4ifc": { - "type": "object", - "properties": { - "v4Address": { - "type": "string", - "pattern": r'^(\d+\.){3}\d+$' - }, - "v4Mask": { - "type": "string", - "pattern": r'^(\d+\.){3}\d+$' - }, - "v4InterfaceName": {"type": "string"}, - "index": { - "type": "string", - "pattern": r'^\d+$' - } - }, - "required": [ - "v4Address", "v4Mask", "v4InterfaceName", "index"], - "additionalProperties": False - }, - "v6ifc": { - "type": "object", - "properties": { - "v6Address": { - "type": "string", - "pattern": r'^[a-f\d:]+$' - }, - "v6Mask": { - "type": "string", - "pattern": r'^\d+$' - }, - "v6InterfaceName": {"type": "string"}, - "index": { - "type": "string", - "pattern": r'^\d+$' - } - }, - "required": [ - "v6Address", "v6Mask", "v6InterfaceName", "index"], - "additionalProperties": False - } - }, - - "type": "array", - "items": { - "anyOf": [ - {"$ref": "#/definitions/v4ifc"}, - {"$ref": "#/definitions/v6ifc"} - ] - } - } - ``` - - -* `opsdb:interface_services:<equipment name>:<interface name>` - - * key examples - * `opsdb:interface_services:mx1.ams.nl.geant.net:ae15.1103` - * `opsdb:interface_services:Lab node 2:1-1/5` - * value schema - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "interface": { - "type": "object", - "properties": { - "id": {"type": "integer"}, - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "service_type": {"type": "string"}, - "project": {"type": "string"}, - "equipment": {"type": "string"}, - "port": { - "anyOf": [ - { - "type": "string", - "pattern": r'^[\d:]+$' - }, - { - "type": "string", - "enum": ['console'] - } - ] - }, - "logical_unit": { - "anyOf": [ - {"type": "integer"}, - {"type": "string", "maxLength": 0} - ] - }, - "manufacturer": { - "type": "string", - "enum": ["juniper", "coriant", "infinera", - "cisco", "hewlett packard", - "corsa", "graham smith uk ltd", - "unknown", ""] - }, - "card_id": {"type": "string"}, - "interface_name": {"type": "string"} - }, - "required": ["id", "name", "status", - "circuit_type", "service_type", - "project", "equipment", "port", - "logical_unit", "manufacturer", - "card_id", "interface_name"], - "additionalProperties": False - } - }, - - "type": "object", - "patternProperties": { - "^.+$": { - "type": "array", - "items": {"$ref": "#/definitions/interface"} - } - } - } - ``` - - -* `opsdb:location:<equipment name>` - - * key examples - * `opsdb:location:mx1.ams.nl.geant.net` - * `opsdb:location:taas.srv1.par.fr.geant.net` - * `opsdb:location:A test EX 3-Oct` - * value schema - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": { - "type": "object", - "properties": { - "absid": {"type": "integer"}, - "equipment_name": {"type": "string"}, - "pop_name": {"type": "string"}, - "pop_abbreviation": {"type": "string"}, - "pop_site_id": {"type": "string"}, - "country": {"type": "string"}, - "longitude": {"type": "number"}, - "latitude": {"type": "number"}, - }, - "required": ["absid", "equipment_name", - "pop_name", "pop_abbreviation", "pop_site_id", - "country", "longitude", "latitude"], - "additionalProperties": False - } - } - ``` - -* `opsdb:services:children:<circuit db id>` -* `opsdb:services:parents:<circuit db id>` - - * key examples - * `opsdb:services:children:12363` - * `opsdb:services:parents:14407` - * value schema - TODO: this needs to be checked ... - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "circuit_relationship": { - "type": "object", - "properties": { - "parent_circuit": {"type": "string"}, - "parent_circuit_id": {"type": "integer"}, - "parent_circuit_status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "child_circuit": {"type": "string"}, - "child_circuit_id": {"type": "integer"}, - "child_circuit_status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - } - "circuit_group": {"type": "integer"}, - }, - "required": [ - "parent_circuit", - "parent_circuit_id", - "parent_circuit_status", - "child_circuit", - "child_circuit_id", - "child_circuit_status", - "circuit_group" - ], - "additionalProperties": False - } - }, - - "type": "object", - "patternProperties": { - "^\d+$": { - "type": "array", - "items": {"$ref": "#/definitions/circuit_relationship"} - } - } - } - ``` - -* `alarmsdb:interface_status:<equipment name>:<interface name>` - * key examples - * `alarmsdb:interface_status:Lab node 1:1-1/3` - * `alarmsdb:interface_status:mx1.ams.nl.geant.net:ae15.1500` - * valid values: - TODO: verify these values - * `unknown` - * `up` - * `down` - - -* `ix_public_peer:<address>` - * key examples - * `ix_public_peer:193.203.0.203` - * `ix_public_peer:2001:07f8:00a0:0000:0000:5926:0000:0002` - * valid values: - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "ip-address": { - "type": "string", - "oneOf": [ - {"pattern": r'^(\d+\.){3}\d+$'}, - {"pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$'} - ] - } - }, - - "type": "object", - "properties": { - "name": {"$ref": "#/definitions/ip-address"}, - "description": {"type": "string"}, - "as": { - "type": "object", - "properties": { - "local": {"type": "integer"}, - "peer": {"type": "integer"}, - }, - "required": ["local", "peer"], - "additionalProperties": False - } - }, - "required": ["name", "description", "as"], - "additionalProperties": False - } - ``` - -* `vpn_rr_peers/<address>` - * key examples - * `vpn_rr_peers:193.203.0.203` - * valid values: - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "ip-address": { - "type": "string", - "oneOf": [ - {"pattern": r'^(\d+\.){3}\d+$'}, - {"pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$'} - ] - } - }, - - "type": "object", - "properties": { - "name": {"$ref": "#/definitions/ip-address"}, - "description": {"type": "string"}, - "peer-as": {"type": "integer"} - }, - "required": ["name", "description"], - "additionalProperties": False - } - ``` - -* `reverse_interface_addresses/<address>` - * key examples - * `reverse_interface_addresses:193.203.0.203` - * `reverse_interface_addresses:2001:07f8:00a0:0000:0000:5926:0000:0002` - * valid values: - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "v4a": { - "type": "string", - "pattern": r'^(\d+\.){3}\d+$' - }, - "v6a": { - "type": "string", - "pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$' - }, - "v4i": { - "type": "string", - "pattern": r'^(\d+\.){3}\d+/\d+$' - }, - "v6i": { - "type": "string", - "pattern": r'^[a-f\d:]+/\d+$' - } - }, - - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "oneOf": [ - {"$ref": "#/definitions/v4a"}, - {"$ref": "#/definitions/v6a"} - ] - }, - "interface address": { - "oneOf": [ - {"$ref": "#/definitions/v4i"}, - {"$ref": "#/definitions/v6i"} - ] - }, - "interface name": {"type": "string"}, - }, - "required": ["name", "interface address", "interface name"], - "additionalProperties": False - } - } - ``` +sphinx-build -M html docs/source docs/build +``` \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..d0c3cbf1020d5c292abdedf27627c6abe25e2293 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000000000000000000000000000000000000..886c27c24903cbd2482cb12c35adcd47038c40eb --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,101 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +from importlib import import_module +from docutils.parsers.rst import Directive +from docutils import nodes +from sphinx import addnodes +import json +import os +import sys + +sys.path.insert(0, os.path.abspath( + os.path.join( + os.path.dirname(__file__), + '..', '..', 'inventory_provider'))) + + +class RenderAsJSON(Directive): + # cf. https://stackoverflow.com/a/59883833 + + required_arguments = 1 + + def run(self): + module_path, member_name = self.arguments[0].rsplit('.', 1) + + member_data = getattr(import_module(module_path), member_name) + code = json.dumps(member_data, indent=2) + + literal = nodes.literal_block(code, code) + literal['language'] = 'json' + + return [ + addnodes.desc_name(text=member_name), + addnodes.desc_content('', literal) + ] + + +def setup(app): + app.add_directive('asjson', RenderAsJSON) + + +# -- Project information ----------------------------------------------------- + +project = 'Inventory Provider' +copyright = '2021, swd@geant.org' +author = 'swd@geant.org' + +# The full version, including alpha/beta/rc tags +release = '0.57' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx_rtd_theme', + 'sphinx.ext.autodoc', + 'sphinx.ext.coverage' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' +# html_theme = 'alabaster' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Both the class’ and the __init__ method’s docstring +# are concatenated and inserted. +autoclass_content = "both" +autodoc_typehints = "none" diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst new file mode 100644 index 0000000000000000000000000000000000000000..3502d074966387065a92af957caaae1a22c2aacd --- /dev/null +++ b/docs/source/configuration.rst @@ -0,0 +1,69 @@ +.. notes and hints about confiuguration and running + +Configuration and Running +========================= + +Configuration +------------- + +The module requires two configuration files to be provided +when launching the web service. + +The first configuration file +must be stored in the environment variable +`SETTINGS_FILENAME` and is parsed as a Flask configuration +file. The file is python and there are two +custom variables, and adding other Flask-specific +options is not supported and has not been tested. + +* `INVENTORY_PROVIDER_CONFIG_FILENAME` + + * [REQUIRED] Run-time accessible filename + of a json file containing the server configuration parameters. This file + must be formatted according to the following json schema + +* ENABLE_TESTING_ROUTES + + * [OPTIONAL, default value: False] + Boolean flag (can be any value that evaluates to True) + to enable routes to special utilities used for testing. + *This must never be enabled in a production environment.* + +The following is an example of a configuration file. + +.. code-block:: python + + INVENTORY_PROVIDER_CONFIG_FILENAME = "/somepath/config.json" + ENABLE_TESTING_ROUTES = True + + +The second required file contains Inventory Provider application-level +configuration parameters. + +.. autofunction:: inventory_provider.config.load + + +Running +-------- + +This module has been tested in the following execution environments: + +* As an embedded Flask application. + For example, the application could be launched as follows: + + .. code-block:: bash + + $ export FLASK_APP=app.py + $ export SETTINGS_FILENAME=settings.cfg + $ flask run + +* As an Apache/`mod_wsgi` service. + + * Details of Apache and `mod_wsgi` + configuration are beyond the scope of this document. + +* As a `gunicorn` wsgi service. + + * Details of `gunicorn` configuration are + beyond the scope of this document. + diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..0e8a203bbf90d00f7d8c9ffbdf2f6063cd892448 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +.. Inventory Provider documentation master file, created by + sphinx-quickstart on Sun Jan 24 11:28:52 2021. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Inventory Provider +================== + +Documentation for Inventory Provider. +This service acts as a single point of truth for +information about the GÉANT network, exposed by +by an HTTP API. + + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + + configuration + protocol/index \ No newline at end of file diff --git a/docs/source/protocol/classifier.rst b/docs/source/protocol/classifier.rst new file mode 100644 index 0000000000000000000000000000000000000000..df7036adbabff8c35b79ac5166333c5ddd26d10d --- /dev/null +++ b/docs/source/protocol/classifier.rst @@ -0,0 +1,35 @@ +.. classifier endpoint docs + + +Classifier Endpoints +========================= + +These endpoints are intended for use by Dashboard V3. + +/classifier/peer-info +--------------------- + +.. autofunction:: inventory_provider.routes.classifier.get_bgp_peer_info + +/classifier/juniper-link-info +----------------------------- + +.. autofunction:: inventory_provider.routes.classifier.get_juniper_link_info + + +/classifier/infinera-lambda-info +-------------------------------- + +.. autofunction:: inventory_provider.routes.classifier.get_infinera_lambda_info + + +/classifier/infinera-fiberlink-info +------------------------------------ + +.. autofunction:: + inventory_provider.routes.classifier.get_fiberlink_trap_metadata + +/classifier/coriant-info +------------------------ + +.. autofunction:: inventory_provider.routes.classifier.get_coriant_info \ No newline at end of file diff --git a/docs/source/protocol/data.rst b/docs/source/protocol/data.rst new file mode 100644 index 0000000000000000000000000000000000000000..72531fd8f43f4c748df910f204b6342c7a0d0d7b --- /dev/null +++ b/docs/source/protocol/data.rst @@ -0,0 +1,22 @@ +.. poller endpoint docs + + +Data Endpoints +========================= + +These endpoints are temporary and will be removed. + +/data/routers +--------------------------------- + +.. autofunction:: inventory_provider.routes.data.routers + +/data/interfaces +--------------------------------- + +.. autofunction:: inventory_provider.routes.data.router_interfaces + +/data/pop +--------------------------------- + +.. autofunction:: inventory_provider.routes.data.equipment_location diff --git a/docs/source/protocol/index.rst b/docs/source/protocol/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..e0ef1976524f092d255755ee35ba77f4ca5edbaf --- /dev/null +++ b/docs/source/protocol/index.rst @@ -0,0 +1,36 @@ +.. protocol intro + +Protocol +========================= + +This module implements a Flask-based webservice which +communicates with clients over HTTP. +Responses to valid requests are returned as JSON messages. +The server will therefore return an error unless +`application/json` is in the `Accept` request header field. + +HTTP communication and JSON grammar details are +beyond the scope of this document. +Please refer to [RFC 2616](https://tools.ietf.org/html/rfc2616) +and www.json.org for more details. + + +/version +------------- + +.. autofunction:: inventory_provider.routes.default.version + + +API modules +-------------- + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + classifier + poller + lg + data + jobs + msr \ No newline at end of file diff --git a/docs/source/protocol/jobs.rst b/docs/source/protocol/jobs.rst new file mode 100644 index 0000000000000000000000000000000000000000..866f98e135cf58dcee02accb6071cd09e7b31e35 --- /dev/null +++ b/docs/source/protocol/jobs.rst @@ -0,0 +1,24 @@ +.. poller endpoint docs + + +Jobs Endpoints +========================= + +These endpoints are used for monitoring running jobs. + +/jobs/update +--------------------------------- + +.. autofunction:: inventory_provider.routes.jobs.update + + +/jobs/check-task-status +--------------------------------- + +.. autofunction:: inventory_provider.routes.jobs.check_task_status + + +/jobs/log +--------------------------------- + +.. autofunction:: inventory_provider.routes.jobs.load_task_log diff --git a/docs/source/protocol/lg.rst b/docs/source/protocol/lg.rst new file mode 100644 index 0000000000000000000000000000000000000000..363555fed626cc07904df9e158db1fa78d7b0d30 --- /dev/null +++ b/docs/source/protocol/lg.rst @@ -0,0 +1,12 @@ +.. LG endpoint docs + + +LG Support Endpoints +========================= + +These endpoints are intended for use by LG. + +/lg/interfaces +--------------------------------- + +.. autofunction:: inventory_provider.routes.lg.routers diff --git a/docs/source/protocol/msr.rst b/docs/source/protocol/msr.rst new file mode 100644 index 0000000000000000000000000000000000000000..c73074bb6b5b6bb8ca59e05ff298e4ab5ae01a12 --- /dev/null +++ b/docs/source/protocol/msr.rst @@ -0,0 +1,12 @@ +.. poller endpoint docs + + +MSR Support Endpoints +========================= + +These endpoints are intended for use by MSR. + +/msr/access-services +--------------------------------- + +.. autofunction:: inventory_provider.routes.msr.access_services diff --git a/docs/source/protocol/poller.rst b/docs/source/protocol/poller.rst new file mode 100644 index 0000000000000000000000000000000000000000..8bfa6ddc4bb39ece105e3ba0adf93072a22e8355 --- /dev/null +++ b/docs/source/protocol/poller.rst @@ -0,0 +1,12 @@ +.. poller endpoint docs + + +BRIAN support Endpoints +========================= + +These endpoints are intended for use by BRIAN. + +/poller/interfaces +--------------------------------- + +.. autofunction:: inventory_provider.routes.poller.interfaces diff --git a/inventory_provider/config.py b/inventory_provider/config.py index aa6ef5ffe623a9037c39fef4bdba1156e22b5ba1..d8ce6e9f5ba1200789cb74ad565c947632f2d6a9 100644 --- a/inventory_provider/config.py +++ b/inventory_provider/config.py @@ -1,5 +1,4 @@ import json - import jsonschema CONFIG_SCHEMA = { @@ -142,10 +141,14 @@ CONFIG_SCHEMA = { def load(f): """ - loads, validates and returns configuration parameters + Loads, validates and returns configuration parameters. + + Input is validated against this jsonschema: + + .. asjson:: inventory_provider.config.CONFIG_SCHEMA :param f: file-like object that produces the config file - :return: + :return: a dict containing the parsed configuration parameters """ config = json.loads(f.read()) jsonschema.validate(config, CONFIG_SCHEMA) diff --git a/inventory_provider/routes/classifier.py b/inventory_provider/routes/classifier.py index ba79aa3f56bd0dee8b3baf1659710b30fb8e034b..f1bb72de10ed59d583d59be31d8ff2268b3ad1c7 100644 --- a/inventory_provider/routes/classifier.py +++ b/inventory_provider/routes/classifier.py @@ -183,6 +183,20 @@ def _link_interface_info(r, hostname, interface): methods=['GET', 'POST']) @common.require_accepts_json def get_juniper_link_info(source_equipment, interface): + """ + Handler for /classifier/juniper-link-info that + returns metadata about an IP interface. + + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.classifier_schema.JUNIPER_LINK_RESPONSE_SCHEMA + + :param source_equipment: router hostname + :param interface: link interface name + :return: + """ + r = common.get_current_redis() cache_key = 'classifier-cache:juniper:%s:%s' % ( @@ -472,8 +486,18 @@ def find_interfaces_and_services(address_str): @routes.route("/peer-info/<address>", methods=['GET', 'POST']) @common.require_accepts_json -def peer_info(address): +def get_bgp_peer_info(address): + """ + Handler for /classifier/peer-info that returns bgp peering metadata. + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.classifier_schema.PEER_INFO_RESPONSE_SCHEMA + + :param address: string representation of a bgp peer address + :return: + """ # canonicalize the input address first ... try: obj = ipaddress.ip_address(address) @@ -541,7 +565,21 @@ def peer_info(address): "<source_equipment>/<interface>/<circuit_id>", methods=['GET', 'POST']) @common.require_accepts_json -def get_trap_metadata(source_equipment, interface, circuit_id): +def get_infinera_lambda_info(source_equipment, interface, circuit_id): + """ + Handler for /classifier/infinera-lambda-info that + returns metadata for as DTNX port. + + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.classifier_schema.INFINERA_LAMBDA_INFO_RESPONSE_SCHEMA + + :param source_equipment: DTNX name + :param address: interface/port name + :param circuit_id: infinera circuit id + :return: + """ interface = interface.replace('-T', '-') cache_key = 'classifier-cache:infinera:%s:%s' % ( @@ -595,6 +633,17 @@ def get_trap_metadata(source_equipment, interface, circuit_id): methods=['GET', 'POST']) @common.require_accepts_json def get_fiberlink_trap_metadata(ne_name_str, object_name_str): + """ + Handler for /classifier/infinera-fiberlink-info that + returns metadata for a particular opitical path segment. + + TODO: no schema is declared, and there are no validation tests + + :param ne_name_str: OLA or DTNX equipment name + :param object_name_str: path name + :return: + """ + objects = object_name_str.split('_') shelves = [x.split('-')[0] for x in objects] p = r'([a-zA-Z\d]+?-(OLA|DTNX)\d+(-\d)?)' @@ -665,6 +714,20 @@ def get_fiberlink_trap_metadata(ne_name_str, object_name_str): methods=['GET', 'POST']) @common.require_accepts_json def get_coriant_info(equipment_name, entity_string): + """ + Handler for /classifier/coriant-info that + returns metadata for a coriant path. + + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.classifier_schema.CORIANT_INFO_RESPONSE_SCHEMA + + :param source_equipment: grv hostname + :param entity_string: path name + :return: + """ + r = common.get_current_redis() cache_key = 'classifier-cache:coriant:%s:%s' % ( diff --git a/inventory_provider/routes/classifier_schema.py b/inventory_provider/routes/classifier_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..3d2ca9e1fd986318e751fc95bbe69629f7700059 --- /dev/null +++ b/inventory_provider/routes/classifier_schema.py @@ -0,0 +1,554 @@ + + +_common_locations_schema_definitions = { + "location-endpoint": { + "type": "object", + "properties": { + "equipment": {"type": "string"}, + "name": {"type": "string"}, + "abbreviation": {"type": "string"} + }, + "required": ["equipment", "name", "abbreviation"], + "additionalProperties": False + }, + "location": { + "type": "object", + "properties": { + "a": {"$ref": "#/definitions/location-endpoint"}, + "b": {"$ref": "#/definitions/location-endpoint"} + }, + "required": ["a"], + "additionalProperties": False + }, + "locations-list": { + "type": "array", + "items": {"$ref": "#/definitions/location"} + } +} + +_juniper_link_response_schema_definitions = { + "ip-address": { + "type": "string", + "oneOf": [ + {"pattern": r'^(\d+\.){3}\d+$'}, + {"pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$'} + ] + }, + "ipv4-interface-address": { + "type": "string", + "pattern": r'^(\d+\.){3}\d+/\d+$' + }, + "ipv6-interface-address": { + "type": "string", + "pattern": r'^[a-f\d:]+/\d+$' + }, + "snmp-info": { + "type": "object", + "properties": { + "community": {"type": "string"}, + "index": {"type": "integer"} + }, + "required": ["community", "index"], + "additionalProperties": False + }, + "interface-info": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "description": {"type": "string"}, + "ipv4": { + "type": "array", + "items": {"$ref": "#/definitions/ipv4-interface-address"} + }, + "ipv6": { + "type": "array", + "items": {"$ref": "#/definitions/ipv6-interface-address"} + }, + + # TODO: check what's changed: added to make tests pass + 'bundle': {"type": "array"}, + 'bundle_members': {"type": "array"}, + 'snmp': {"$ref": "#/definitions/snmp-info"} + }, + "required": ["name", "description", "ipv4", "ipv6"], + "additionalProperties": False + }, + "service-info": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"}, + "status": { + "type": "string", + "enum": ["operational", "installed", "planned", "ordered"] + }, + "circuit_type": { + "type": "string", + "enum": ["path", "service", "l2circuit"] + }, + "service_type": {"type": "string"}, + "project": {"type": "string"}, + "equipment": {"type": "string"}, + "pop": {"type": "string"}, + "pop_abbreviation": {"type": "string"}, + + "other_end_pop": {"type": "string"}, + "other_end_pop_abbreviation": {"type": "string"}, + "other_end_equipment": {"type": "string"}, + "port": {"type": "string"}, + "other_end_port": {"type": "string"}, + "logical_unit": { + "oneOf": [ + {"type": "integer"}, + {"type": "string", "maxLength": 0} + ] + }, + "other_end_logical_unit": { + "oneOf": [ + {"type": "integer"}, + {"type": "string", "maxLength": 0} + ] + }, + "manufacturer": { + "type": "string", + "enum": ["juniper", "coriant", "infinera", + "cisco", "hewlett packard", + "corsa", "graham smith uk ltd", + "unknown", ""] + }, + "card_id": {"type": "string"}, + "other_end_card_id": {"type": "string"}, + "interface_name": {"type": "string"}, + "other_end_interface_name": {"type": "string"}, + + # TODO: check what's changed: added to make tests pass + 'other_end_pop_name': {"type": "string"}, + 'pop_name': {"type": "string"} + }, + # TODO: modify service-info so that "" entries are just omitted + # (... rather than requiring 'oneOf') + # TODO: put 'other_end_*' params in a sub dictionary + # "required": [ + # "id", "name", "status", + # "circuit_type", "service_type", + # "project", "port", "manufacturer", + # "equipment", "logical_unit", "card_id", "interface_name" + # ], + "additionalProperties": False + }, + "related-service-info": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "status": { + "type": "string", + "enum": ["operational", "installed", "planned", "ordered"] + }, + "circuit_type": { + "type": "string", + "enum": ["path", "service", "l2circuit"] + }, + "project": {"type": "string"} + }, + "required": ["name", "status", "circuit_type", "project"], + "additionalProperties": False + } +} + +JUNIPER_LINK_RESPONSE_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + + "definitions": { + **_juniper_link_response_schema_definitions, + **_common_locations_schema_definitions + }, + + "type": "object", + "properties": { + "services": { + "type": "array", + "items": {"$ref": "#/definitions/service-info"} + }, + "interface": {"$ref": "#/definitions/interface-info"}, + "related-services": { + "type": "array", + "items": {"$ref": "#/definitions/related-service-info"} + }, + "locations": {"$ref": "#/definitions/locations-list"} + }, + "required": ["interface", "locations"], + "additionalProperties": False +} + + +_peer_info_response_schema_definitions = { + "ip-address": { + "type": "string", + "oneOf": [ + {"pattern": r'^(\d+\.){3}\d+$'}, + {"pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$'} + ] + }, + "interface-address": { + "type": "string", + "oneOf": [ + {"pattern": r'^(\d+\.){3}\d+/\d+$'}, + {"pattern": r'^[a-f\d:]+/\d+$'} + ] + }, + "vpn-rr-peer": { + "type": "object", + "properties": { + "name": {"$ref": "#/definitions/ip-address"}, + "description": {"type": "string"}, + "peer-as": {"type": "integer"}, + "router": {"type": "string"} + }, + "required": ["name", "description"], + "additionalProperties": False + }, + "ix-public-peer": { + "type": "object", + "properties": { + "name": {"$ref": "#/definitions/ip-address"}, + "description": {"type": "string"}, + "router": {"type": "string"}, + "as": { + "type": "object", + "properties": { + "local": {"type": "integer"}, + "peer": {"type": "integer"}, + }, + "required": ["local", "peer"], + "additionalProperties": False + } + }, + "required": ["name", "description", "as"], + "additionalProperties": False + }, + "ix-public-peer-list": { + "type": "array", + "items": {"$ref": "#/definitions/ip-address"} + }, + "ix-public-peer-info": { + "type": "object", + "properties": { + "peer": {"$ref": "#/definitions/ix-public-peer"}, + "group": {"$ref": "#/definitions/ix-public-peer-list"}, + "router": {"$ref": "#/definitions/ix-public-peer-list"} + }, + "required": ["peer", "group", "router"], + "additionalProperties": False + }, + "interface-info": { + "type": "object", + "properties": { + "name": {"$ref": "#/definitions/ip-address"}, + "interface address": { + "$ref": "#/definitions/interface-address"}, + "interface name": {"type": "string"}, + "router": {"type": "string"} + }, + "required": [ + "name", "interface address", "interface name", "router"], + "additionalProperties": False + }, + "service-info": { + "type": "object" + }, + "interface-lookup-info": { + "type": "object", + "properties": { + "interface": {"$ref": "#/definitions/interface-info"}, + "services": { + "type": "array", + "items": {"$ref": "#/definitions/service-info"} + } + } + }, + "snmp-info": { + "type": "object", + "properties": { + "hostname": {"type": "string"}, + "oid": {"type": "string"}, + "community": {"type": "string"} + }, + "required": ["oid", "community", "hostname"], + "additionalProperties": False + }, + "asn-group-member": { + "type": "object", + "properties": { + "router": {"type": "string"}, + "address": {"type": "string"}, + "group": {"type": "string"} + }, + "required": ["router", "address", "group"], + "additionalProperties": False + }, + "asn-group": { + "type": "object", + "properties": { + "asn": {"type": "integer"}, + "peers": { + "type": "array", + "items": {"$ref": "#/definitions/asn-group-member"} + } + }, + "required": ["asn", "peers"], + "additionalProperties": False + } +} + + +PEER_INFO_RESPONSE_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + + "definitions": { + **_peer_info_response_schema_definitions, + **_common_locations_schema_definitions + }, + + "type": "object", + "properties": { + "ix-public-peer-info": { + "$ref": "#/definitions/ix-public-peer-info"}, + "vpn-rr-peer-info": {"$ref": "#/definitions/vpn-rr-peer"}, + "interfaces": { + "type": "array", + "items": {"$ref": "#/definitions/interface-lookup-info"} + }, + "locations": {"$ref": "#/definitions/locations-list"}, + "snmp": { + "type": "array", + "items": {"$ref": "#/definitions/snmp-info"} + }, + "asn": {"$ref": "#/definitions/asn-group"} + }, + "additionalProperties": False +} + +_infinera_lambda_response_schema_definitions = { + "service-info": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"}, + "status": { + "type": "string", + "enum": ["operational", "installed", "planned", "ordered"] + }, + "circuit_type": { + "type": "string", + "enum": ["path", "service", "l2circuit"] + }, + "service_type": {"type": "string"}, + "project": {"type": "string"}, + "pop_name": {"type": "string"}, + "pop_abbreviation": {"type": "string"}, + "other_end_pop_name": {"type": "string"}, + "other_end_pop_abbreviation": {"type": "string"}, + "equipment": {"type": "string"}, + "other_end_equipment": {"type": "string"}, + "port": {"type": "string"}, + "other_end_port": {"type": "string"}, + "logical_unit": { + "oneOf": [ + {"type": "integer"}, + {"type": "string", "maxLength": 0} + ] + }, + "other_end_logical_unit": { + "oneOf": [ + {"type": "integer"}, + {"type": "string", "maxLength": 0} + ] + }, + "manufacturer": { + "type": "string", + "enum": ["juniper", "coriant", "infinera", + "cisco", "hewlett packard", + "corsa", "graham smith uk ltd", + "unknown", ""] + }, + "card_id": {"type": "string"}, + "other_end_card_id": {"type": "string"}, + "interface_name": {"type": "string"}, + "other_end_interface_name": {"type": "string"} + }, + "additionalProperties": False + }, + "related-service-info": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "status": { + "type": "string", + "enum": ["operational", "installed", "planned", "ordered"] + }, + "circuit_type": { + "type": "string", + "enum": ["path", "service", "l2circuit"] + }, + "project": {"type": "string"} + }, + "required": ["name", "status", "circuit_type", "project"], + "additionalProperties": False + }, + "geant-lambda": { + "type": "object", + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"}, + "project": {"type": "string"}, + "status": { + "type": "string", + "enum": ["operational", "installed", "planned", "ordered"] + }, + }, + "additionalProperties": False + } +} + +INFINERA_LAMBDA_INFO_RESPONSE_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + + "definitions": { + **_infinera_lambda_response_schema_definitions, + **_common_locations_schema_definitions + }, + + "type": "object", + "properties": { + "services": { + "type": "array", + "items": {"$ref": "#/definitions/service-info"} + }, + "related-services": { + "type": "array", + "items": {"$ref": "#/definitions/related-service-info"} + }, + "geant-lambda": { + "$ref": "#/definitions/geant-lambda" + }, + "locations": {"$ref": "#/definitions/locations-list"} + }, + "additionalProperties": False +} + +CORIANT_INFO_RESPONSE_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + + "definitions": { + "location-endpoint": { + "type": "object", + "properties": { + "equipment": {"type": "string"}, + "name": {"type": "string"}, + "abbreviation": {"type": "string"} + }, + "required": ["equipment", "name", "abbreviation"], + "additionalProperties": False + }, + "location": { + "type": "object", + "properties": { + "a": {"$ref": "#/definitions/location-endpoint"}, + "b": {"$ref": "#/definitions/location-endpoint"} + }, + "required": ["a"], + "additionalProperties": False + }, + "locations-list": { + "type": "array", + "items": {"$ref": "#/definitions/location"} + }, + "pop-info": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "abbreviation": {"type": "string"}, + "country": {"type": "string"}, + "city": {"type": "string"}, + "longitude": {"type": "number"}, + "latitude": {"type": "number"} + }, + "required": [ + "name", + "abbreviation", + "country", + "city", + "longitude", + "latitude" + ], + "additionalProperties": False + }, + "endpoint": { + "type": "object", + "properties": { + "equipment name": {"type": "string"}, + "card id": {"type": "string"}, + "port number": {"type": "string"}, + "pop": {"$ref": "#/definitions/pop-info"} + }, + "required": ["equipment name", "port number", "pop"], + "additionalProperties": False + }, + "path": { + "type": "object", + "properties": { + 'category': {"type": "string"}, + 'circuit_type': {"type": "string"}, + 'service_type': {"type": "string"}, + 'peering_type': {"type": "string"}, + 'status': {"type": "string"}, + 'name': {"type": "string"}, + 'a': {"$ref": "#/definitions/endpoint"}, + 'b': {"$ref": "#/definitions/endpoint"} + }, + "required": [ + "category", + "circuit_type", + "service_type", + "peering_type", + "status", + "a", + "b"], + "additionalProperties": False + }, + "related-service-info": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "status": { + "type": "string", + "enum": ["operational", "installed", "planned", "ordered"] + }, + "circuit_type": { + "type": "string", + "enum": ["path", "service", "l2circuit"] + }, + "project": {"type": "string"} + }, + "additionalProperties": False + } + }, + + "type": "object", + "properties": { + "equipment name": {"type": "string"}, + "card id": {"type": "string"}, + "port number": {"type": "string"}, + "path": {"$ref": "#/definitions/path"}, + "related-services": { + "type": "array", + "items": {"$ref": "#/definitions/related-service-info"} + }, + }, + "required": ["equipment name", "card id", "port number"], + "additionalProperties": False +} diff --git a/inventory_provider/routes/data.py b/inventory_provider/routes/data.py index f25bd80dae5374c1b43b3802e697a07327b6c137..9c9842d9ec2e32b921b585f8d5a66d7032934552 100644 --- a/inventory_provider/routes/data.py +++ b/inventory_provider/routes/data.py @@ -9,6 +9,80 @@ from inventory_provider.db import opsdb routes = Blueprint("inventory-data-query-routes", __name__) +ROUTERS_RESPONSE_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": {"type": "string"} +} + +ROUTER_INTERFACES_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "router": {"type": "string"}, + "description": {"type": "string"}, + "ipv4": { + "type": "array", + "items": {"type": "string"} + }, + "ipv6": { + "type": "array", + "items": {"type": "string"} + } + }, + "required": ["name", "description", "router", "ipv4", "ipv6"], + "additionalProperties": False + } +} + +POP_RESPONSE_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + + "definitions": { + "pop-info": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "abbreviation": {"type": "string"}, + "country": {"type": "string"}, + "city": {"type": "string"}, + "longitude": {"type": "number"}, + "latitude": {"type": "number"} + }, + "required": [ + "name", + "abbreviation", + "country", + "city", + "longitude", + "latitude" + ], + "additionalProperties": False + }, + "equipment-info": { + "type": "object", + "properties": { + 'equipment-name': {"type": "string"}, + 'status': {"type": "string"}, + 'pop': {"$ref": "#/definitions/pop-info"} + }, + "required": [ + "equipment-name", + "status", + "pop" + ], + "additionalProperties": False + + } + }, + + "type": "array", + "items": {"$ref": "#/definitions/equipment-info"} +} + @routes.after_request def after_request(resp): @@ -18,6 +92,17 @@ def after_request(resp): @routes.route("/routers", methods=['GET', 'POST']) @common.require_accepts_json def routers(): + """ + Handler for `/data/routers`. + + The response will be a list of router hostnames + for which information is available and will formatted + according to the following schema: + + .. asjson:: inventory_provider.routes.data.ROUTERS_RESPONSE_SCHEMA + + :return: + """ r = common.get_current_redis() result = [] for k in r.keys('netconf:*'): @@ -31,6 +116,18 @@ def routers(): @routes.route("/interfaces/<hostname>", methods=['GET', 'POST']) @common.require_accepts_json def router_interfaces(hostname=None): + """ + Handler for `/data/interfaces</hostname>`. + + The response will be a list of information about + the interfaces present on the requested host + and will be formatted as follows: + + .. asjson:: inventory_provider.routes.data.ROUTER_INTERFACES_SCHEMA + + :param hostname: optional hostname + :return: + """ cache_key = f'classifier-cache:netconf-interfaces:{hostname}' \ if hostname else 'classifier-cache:netconf-interfaces:all' @@ -67,6 +164,18 @@ def router_interfaces(hostname=None): @routes.route("/pop/<equipment_name>", methods=['GET', 'POST']) @common.require_accepts_json def equipment_location(equipment_name): + """ + Handler for `/data/pop/[equipment name]`. + + Returns location information for the equipment identified + by `equipment name` and will be formatted as follows: + + .. asjson:: inventory_provider.routes.data.POP_RESPONSE_SCHEMA + + :param equipment_name: equipment name + :return: + """ + config = current_app.config['INVENTORY_PROVIDER_CONFIG'] with db.connection(config['ops-db']) as cx: diff --git a/inventory_provider/routes/default.py b/inventory_provider/routes/default.py index d7f1e897ed91f982a69b6a1d56f29f5fac471cad..f65d6124835c09f70459f9d2defe01a36e23dbd6 100644 --- a/inventory_provider/routes/default.py +++ b/inventory_provider/routes/default.py @@ -8,6 +8,40 @@ routes = Blueprint("inventory-data-default-routes", __name__) API_VERSION = '0.1' +VERSION_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + + "definitions": { + "latch": { + "type": "object", + "properties": { + "current": {"type": "integer"}, + "next": {"type": "integer"}, + "this": {"type": "integer"}, + "failure": {"type": "boolean"}, + "pending": {"type": "boolean"}, + }, + "required": ["current", "next", "this", "pending", "failure"], + "additionalProperties": False + } + }, + + "type": "object", + "properties": { + "api": { + "type": "string", + "pattern": r'\d+\.\d+' + }, + "module": { + "type": "string", + "pattern": r'\d+\.\d+' + }, + "latch": {"$ref": "#/definitions/latch"} + }, + "required": ["api", "module"], + "additionalProperties": False +} + @routes.after_request def after_request(resp): @@ -17,6 +51,15 @@ def after_request(resp): @routes.route("/version", methods=['GET', 'POST']) @common.require_accepts_json def version(): + """ + Returns a json object with information about the module version. + + The response will be formatted according to the following schema: + + .. asjson:: inventory_provider.routes.default.VERSION_SCHEMA + + :return: + """ config = current_app.config["INVENTORY_PROVIDER_CONFIG"] version_params = { 'api': API_VERSION, diff --git a/inventory_provider/routes/jobs.py b/inventory_provider/routes/jobs.py index af879679526263d59d14093f99f149bb80a47169..00f6a6a2ec746fc4d40cd249b93c570eccdc63f7 100644 --- a/inventory_provider/routes/jobs.py +++ b/inventory_provider/routes/jobs.py @@ -13,6 +13,70 @@ routes = Blueprint("inventory-data-job-routes", __name__) logger = logging.getLogger(__name__) +TASK_ID_RESPONSE_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "task id": {"type": "string"} + }, + "required": ["task id"], + "additionalProperties": False +} + +TASK_LOG_RESPONSE_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "pending": {"type": "array", "items": {"type": "string"}}, + "errors": {"type": "array", "items": {"type": "string"}}, + "failed": {"type": "array", "items": {"type": "string"}}, + "warnings": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["pending", "errors", "failed", "warnings"], + "additionalProperties": False +} + +INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "task": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "status": {"type": "string"}, + "exception": {"type": "boolean"}, + "ready": {"type": "boolean"}, + "success": {"type": "boolean"}, + "result": {"type": ["object", "null"]}, + "parent": {"type": ["string", "null"]} + }, + "required": [ + "id", "status", "exception", "ready", "success", "parent"], + "additionalProperties": False + } + }, + + "type": "array", + "items": {"$ref": "#/definitions/task"} +} + + +# INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA = { +# "$schema": "http://json-schema.org/draft-07/schema#", +# "type": "object", +# "properties": { +# "id": {"type": "string"}, +# "status": {"type": "string"}, +# "exception": {"type": "boolean"}, +# "ready": {"type": "boolean"}, +# "success": {"type": "boolean"}, +# "result": {"type": "object"} +# }, +# "required": ["id", "status", "exception", "ready", "success"], +# "additionalProperties": False +# } + + @routes.after_request def after_request(resp): return common.after_request(resp) @@ -21,7 +85,19 @@ def after_request(resp): @routes.route("/update", methods=['GET', 'POST']) @common.require_accepts_json def update(): + """ + Handler for `/jobs/update`. + + This resource updates the inventory network data for juniper devices. + The function completes asynchronously and a list of outstanding + task id's is returned so the caller can + use `/jobs/check-task-status` to determine when all jobs + are finished. The response will be formatted as follows: + .. asjson:: inventory_provider.routes.jobs.TASK_ID_RESPONSE_SCHEMA + + :return: + """ force = request.args.get('force', default='false', type=str) try: force = strtobool(force) @@ -55,6 +131,20 @@ def reload_router_config(equipment_name): @routes.route("check-task-status/<task_id>", methods=['GET', 'POST']) @common.require_accepts_json def check_task_status(task_id): + """ + Handler for /jobs/check-task-status/*`task-id`* + + This resource returns the current status of + an asynchronous task started by `/jobs/update` + or `jobs/reload-router-config`. The return value + will be formatted as follows: + + .. asjson:: + inventory_provider.routes.jobs.INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA + + :param task_id: + :return: + """ return jsonify(list(worker.check_task_status(task_id))) @@ -76,7 +166,18 @@ def check_update_status(): @routes.route("log", methods=['GET', 'POST']) @common.require_accepts_json def load_task_log(): + """ + Handler for `/jobs/log`. + + This resource returns the state of the previous (or current) + tasks associated with a call to `/jobs/update`. The response + contains error or warning messages, if any were generated, and + will be formatted according to the following schema: + + .. asjson:: inventory_provider.routes.jobs.TASK_LOG_RESPONSE_SCHEMA + :return: + """ FINALIZATION_EVENTS = {'task-succeeded', 'task-failed', 'task-revoked'} config = current_app.config['INVENTORY_PROVIDER_CONFIG'] diff --git a/inventory_provider/routes/lg.py b/inventory_provider/routes/lg.py index bcb528ccc8252f806c3ce5b8b6366dea7e8da0f8..af1903837ae83e2d3d405b7eb9b470c47e5c357f 100644 --- a/inventory_provider/routes/lg.py +++ b/inventory_provider/routes/lg.py @@ -10,6 +10,52 @@ ACCESS_PUBLIC = 'public' ACCESS_INTERNAL = 'all' +LG_ROUTERS_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + + "definitions": { + "pop-info": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "abbreviation": {"type": "string"}, + "country": {"type": "string"}, + "country code": {"type": "string"}, + "city": {"type": "string"}, + "longitude": {"type": "number"}, + "latitude": {"type": "number"} + }, + "required": [ + "name", + "abbreviation", + "country", + "country code", + "city", + "longitude", + "latitude" + ], + "additionalProperties": False + }, + "router": { + "type": "object", + "properties": { + "equipment name": {"type": "string"}, + "type": { + "type": "string", + "enum": ["INTERNAL", "CORE"] + }, + "pop": {"$ref": "#/definitions/pop-info"} + }, + "required": ["equipment name", "type", "pop"], + "additionalProperties": False + } + }, + + "type": "array", + "items": {"$ref": "#/definitions/router"} +} + + @routes.after_request def after_request(resp): return common.after_request(resp) @@ -18,6 +64,22 @@ def after_request(resp): @routes.route("/routers/<string:access>", methods=['GET', 'POST']) @common.require_accepts_json def routers(access): + """ + Handler for `/lg/routers/[access]` that returns a list of + router information for use by LG. + + Endpoints `/lg/routers/public` and `/lg/routers/all` + are supported, and only publicly accessible routers + or all routers, respectively, are returned. + + The response will be formatted according to the following schema: + + .. asjson:: + inventory_provider.routes.lg.LG_ROUTERS_SCHEMA + + :param access: one of `public` or `all` + :return: + """ if access not in {ACCESS_INTERNAL, ACCESS_PUBLIC}: return Response( diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py index d109d656a51f83d2a3299ba6fa1f8d90a355d5f0..ce17ae63f3c5495d0ff38d189a42bbdddd663158 100644 --- a/inventory_provider/routes/msr.py +++ b/inventory_provider/routes/msr.py @@ -7,6 +7,37 @@ from inventory_provider.routes import common 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"} +} + + @routes.after_request def after_request(resp): return common.after_request(resp) @@ -15,7 +46,18 @@ def 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(): diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py index f51067f14762f96e2a91eb07f9a7e8f08868d9ca..228ae0b7772f27f2dd4484a03af68984c81b6b6f 100644 --- a/inventory_provider/routes/poller.py +++ b/inventory_provider/routes/poller.py @@ -9,6 +9,56 @@ 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): @@ -137,7 +187,23 @@ def _load_poller_interfaces(hostname=None): @routes.route("/interfaces", methods=['GET', 'POST']) @routes.route('/interfaces/<hostname>', methods=['GET', 'POST']) @common.require_accepts_json -def poller_interface_oids(hostname=None): +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' diff --git a/requirements.txt b/requirements.txt index 73ee0639eb07da9a58b076055cece5a86d79979e..3c1a2c7e14fc1376ef5076c55587b65c3120549d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,5 @@ netifaces pytest pytest-mock responses +sphinx +sphinx-rtd-theme diff --git a/test/per_router/test_poller_routes.py b/test/per_router/test_poller_routes.py index 07acce195af902dcd3b6ae40c2477d6674868e4b..d7351c09dd899ab785cf73568a2039c8c96a0d10 100644 --- a/test/per_router/test_poller_routes.py +++ b/test/per_router/test_poller_routes.py @@ -1,61 +1,12 @@ import json import jsonschema +from inventory_provider.routes.poller import INTERFACE_LIST_SCHEMA DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", "Accept": ["application/json"] } -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'} -} - def test_router_interfaces(router, client): rv = client.post( diff --git a/test/test_classifier_routes.py b/test/test_classifier_routes.py index 8b76c1b5368d1027b1623524ab309bfb1be78f24..83b358bf0fd8911e041612c45641b11cd3853538 100644 --- a/test/test_classifier_routes.py +++ b/test/test_classifier_routes.py @@ -3,191 +3,14 @@ import json import jsonschema import pytest +from inventory_provider.routes.classifier_schema \ + import JUNIPER_LINK_RESPONSE_SCHEMA, PEER_INFO_RESPONSE_SCHEMA + DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", "Accept": ["application/json"] } -LOCATIONS_DEFINITIONS = { - "location-endpoint": { - "type": "object", - "properties": { - "equipment": {"type": "string"}, - "name": {"type": "string"}, - "abbreviation": {"type": "string"} - }, - "required": ["equipment", "name", "abbreviation"], - "additionalProperties": False - }, - "location": { - "type": "object", - "properties": { - "a": {"$ref": "#/definitions/location-endpoint"}, - "b": {"$ref": "#/definitions/location-endpoint"} - }, - "required": ["a"], - "additionalProperties": False - }, - "locations-list": { - "type": "array", - "items": {"$ref": "#/definitions/location"} - } -} - -JUNIPER_LINK_METADATA_DEFINITIONS = { - "ip-address": { - "type": "string", - "oneOf": [ - {"pattern": r'^(\d+\.){3}\d+$'}, - {"pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$'} - ] - }, - "ipv4-interface-address": { - "type": "string", - "pattern": r'^(\d+\.){3}\d+/\d+$' - }, - "ipv6-interface-address": { - "type": "string", - "pattern": r'^[a-f\d:]+/\d+$' - }, - "snmp-info": { - "type": "object", - "properties": { - "community": {"type": "string"}, - "index": {"type": "integer"} - }, - "required": ["community", "index"], - "additionalProperties": False - }, - "interface-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "description": {"type": "string"}, - "ipv4": { - "type": "array", - "items": {"$ref": "#/definitions/ipv4-interface-address"} - }, - "ipv6": { - "type": "array", - "items": {"$ref": "#/definitions/ipv6-interface-address"} - }, - - # TODO: check what's changed: added to make tests pass - 'bundle': {"type": "array"}, - 'bundle_members': {"type": "array"}, - 'snmp': {"$ref": "#/definitions/snmp-info"} - }, - "required": ["name", "description", "ipv4", "ipv6"], - "additionalProperties": False - }, - "service-info": { - "type": "object", - "properties": { - "id": {"type": "integer"}, - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "service_type": {"type": "string"}, - "project": {"type": "string"}, - "equipment": {"type": "string"}, - "pop": {"type": "string"}, - "pop_abbreviation": {"type": "string"}, - - "other_end_pop": {"type": "string"}, - "other_end_pop_abbreviation": {"type": "string"}, - "other_end_equipment": {"type": "string"}, - "port": {"type": "string"}, - "other_end_port": {"type": "string"}, - "logical_unit": { - "oneOf": [ - {"type": "integer"}, - {"type": "string", "maxLength": 0} - ] - }, - "other_end_logical_unit": { - "oneOf": [ - {"type": "integer"}, - {"type": "string", "maxLength": 0} - ] - }, - "manufacturer": { - "type": "string", - "enum": ["juniper", "coriant", "infinera", - "cisco", "hewlett packard", - "corsa", "graham smith uk ltd", - "unknown", ""] - }, - "card_id": {"type": "string"}, - "other_end_card_id": {"type": "string"}, - "interface_name": {"type": "string"}, - "other_end_interface_name": {"type": "string"}, - - # TODO: check what's changed: added to make tests pass - 'other_end_pop_name': {"type": "string"}, - 'pop_name': {"type": "string"} - }, - # TODO: modify service-info so that "" entries are just omitted - # (... rather than requiring 'oneOf') - # TODO: put 'other_end_*' params in a sub dictionary - # "required": [ - # "id", "name", "status", - # "circuit_type", "service_type", - # "project", "port", "manufacturer", - # "equipment", "logical_unit", "card_id", "interface_name" - # ], - "additionalProperties": False - }, - "related-service-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "project": {"type": "string"} - }, - "required": ["name", "status", "circuit_type", "project"], - "additionalProperties": False - } -} - -JUNIPER_LINK_METADATA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - - "definitions": { - **JUNIPER_LINK_METADATA_DEFINITIONS, **LOCATIONS_DEFINITIONS - }, - - "type": "object", - "properties": { - "services": { - "type": "array", - "items": {"$ref": "#/definitions/service-info"} - }, - "interface": {"$ref": "#/definitions/interface-info"}, - "related-services": { - "type": "array", - "items": {"$ref": "#/definitions/related-service-info"} - }, - "locations": {"$ref": "#/definitions/locations-list"} - }, - "required": ["interface", "locations"], - "additionalProperties": False -} - def test_juniper_link_info(client): rv = client.get( @@ -196,7 +19,7 @@ def test_juniper_link_info(client): assert rv.status_code == 200 assert rv.is_json response_data = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(response_data, JUNIPER_LINK_METADATA) + jsonschema.validate(response_data, JUNIPER_LINK_RESPONSE_SCHEMA) def test_juniper_link_info_not_found(client): @@ -207,7 +30,7 @@ def test_juniper_link_info_not_found(client): assert rv.status_code == 200 assert rv.is_json response_data = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(response_data, JUNIPER_LINK_METADATA) + jsonschema.validate(response_data, JUNIPER_LINK_RESPONSE_SCHEMA) assert response_data == { 'interface': { 'name': 'unknown-interface-name', @@ -234,7 +57,7 @@ def test_juniper_link_unknown_router(client): assert rv.status_code == 200 assert rv.is_json response_data = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(response_data, JUNIPER_LINK_METADATA) + jsonschema.validate(response_data, JUNIPER_LINK_RESPONSE_SCHEMA) assert response_data == { 'interface': { 'name': 'unknown-interface-name', @@ -266,150 +89,6 @@ IX_PUBLIC_PEER_INFO_KEYS = { ]) def test_peer_info( client, peer_address, expected_response_keys): - response_schema_definitions = { - "ip-address": { - "type": "string", - "oneOf": [ - {"pattern": r'^(\d+\.){3}\d+$'}, - {"pattern": r'^([a-f\d]{4}:){7}[a-f\d]{4}$'} - ] - }, - "interface-address": { - "type": "string", - "oneOf": [ - {"pattern": r'^(\d+\.){3}\d+/\d+$'}, - {"pattern": r'^[a-f\d:]+/\d+$'} - ] - }, - "vpn-rr-peer": { - "type": "object", - "properties": { - "name": {"$ref": "#/definitions/ip-address"}, - "description": {"type": "string"}, - "peer-as": {"type": "integer"}, - "router": {"type": "string"} - }, - "required": ["name", "description"], - "additionalProperties": False - }, - "ix-public-peer": { - "type": "object", - "properties": { - "name": {"$ref": "#/definitions/ip-address"}, - "description": {"type": "string"}, - "router": {"type": "string"}, - "as": { - "type": "object", - "properties": { - "local": {"type": "integer"}, - "peer": {"type": "integer"}, - }, - "required": ["local", "peer"], - "additionalProperties": False - } - }, - "required": ["name", "description", "as"], - "additionalProperties": False - }, - "ix-public-peer-list": { - "type": "array", - "items": {"$ref": "#/definitions/ip-address"} - }, - "ix-public-peer-info": { - "type": "object", - "properties": { - "peer": {"$ref": "#/definitions/ix-public-peer"}, - "group": {"$ref": "#/definitions/ix-public-peer-list"}, - "router": {"$ref": "#/definitions/ix-public-peer-list"} - }, - "required": ["peer", "group", "router"], - "additionalProperties": False - }, - "interface-info": { - "type": "object", - "properties": { - "name": {"$ref": "#/definitions/ip-address"}, - "interface address": { - "$ref": "#/definitions/interface-address"}, - "interface name": {"type": "string"}, - "router": {"type": "string"} - }, - "required": [ - "name", "interface address", "interface name", "router"], - "additionalProperties": False - }, - "service-info": { - "type": "object" - }, - "interface-lookup-info": { - "type": "object", - "properties": { - "interface": {"$ref": "#/definitions/interface-info"}, - "services": { - "type": "array", - "items": {"$ref": "#/definitions/service-info"} - } - } - }, - "snmp-info": { - "type": "object", - "properties": { - "hostname": {"type": "string"}, - "oid": {"type": "string"}, - "community": {"type": "string"} - }, - "required": ["oid", "community", "hostname"], - "additionalProperties": False - }, - "asn-group-member": { - "type": "object", - "properties": { - "router": {"type": "string"}, - "address": {"type": "string"}, - "group": {"type": "string"} - }, - "required": ["router", "address", "group"], - "additionalProperties": False - }, - "asn-group": { - "type": "object", - "properties": { - "asn": {"type": "integer"}, - "peers": { - "type": "array", - "items": {"$ref": "#/definitions/asn-group-member"} - } - }, - "required": ["asn", "peers"], - "additionalProperties": False - } - } - response_schema = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - - "definitions": { - **response_schema_definitions, **LOCATIONS_DEFINITIONS - }, - - "type": "object", - "properties": { - "ix-public-peer-info": { - "$ref": "#/definitions/ix-public-peer-info"}, - "vpn-rr-peer-info": {"$ref": "#/definitions/vpn-rr-peer"}, - "interfaces": { - "type": "array", - "items": {"$ref": "#/definitions/interface-lookup-info"} - }, - "locations": {"$ref": "#/definitions/locations-list"}, - "snmp": { - "type": "array", - "items": {"$ref": "#/definitions/snmp-info"} - }, - "asn": {"$ref": "#/definitions/asn-group"} - }, - "additionalProperties": False - } rv = client.get( '/classifier/peer-info/%s' % peer_address, @@ -417,7 +96,7 @@ def test_peer_info( assert rv.status_code == 200 assert rv.is_json response_data = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(response_data, response_schema) + jsonschema.validate(response_data, PEER_INFO_RESPONSE_SCHEMA) assert set(response_data.keys()) == expected_response_keys @@ -448,6 +127,8 @@ def test_coriant_info( """ just check that entity_name is correctly parsed and the correct method is called, but mock out all sql access + + # TODO: schema validation """ CONNECTION = 'bogus connection' diff --git a/test/test_general_poller_routes.py b/test/test_general_poller_routes.py index 5241978808dacd44b87e31b778c2889ededc9720..f3974eec19594c51da31a76fbece6b279e79959b 100644 --- a/test/test_general_poller_routes.py +++ b/test/test_general_poller_routes.py @@ -1,61 +1,12 @@ import json import jsonschema +from inventory_provider.routes.poller import INTERFACE_LIST_SCHEMA DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", "Accept": ["application/json"] } -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'} -} - def test_get_all_interfaces(client): rv = client.get( diff --git a/test/test_general_routes.py b/test/test_general_routes.py index 116e89f412329b2d053bcafe6531f55e12398e40..228a5db470bc2d3a5607d8410b4e706bed31d1be 100644 --- a/test/test_general_routes.py +++ b/test/test_general_routes.py @@ -2,6 +2,7 @@ import json import jsonschema from inventory_provider.routes import common +from inventory_provider.routes.default import VERSION_SCHEMA DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", @@ -11,47 +12,13 @@ DEFAULT_REQUEST_HEADERS = { def test_version_request(client, mocked_redis): - version_schema = { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "latch": { - "type": "object", - "properties": { - "current": {"type": "integer"}, - "next": {"type": "integer"}, - "this": {"type": "integer"}, - "failure": {"type": "boolean"}, - "pending": {"type": "boolean"}, - }, - "required": ["current", "next", "this", "pending", "failure"], - "additionalProperties": False - } - }, - - "type": "object", - "properties": { - "api": { - "type": "string", - "pattern": r'\d+\.\d+' - }, - "module": { - "type": "string", - "pattern": r'\d+\.\d+' - }, - "latch": {"$ref": "#/definitions/latch"} - }, - "required": ["api", "module"], - "additionalProperties": False - } - rv = client.post( "version", headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 200 jsonschema.validate( json.loads(rv.data.decode("utf-8")), - version_schema) + VERSION_SCHEMA) def test_load_json_docs(data_config, mocked_redis): diff --git a/test/test_infinera_classifier.py b/test/test_infinera_classifier.py index 6e0621a08d20abdc737f95415bbbbca1cf8966b8..c4d2488fc41e91d9b2f782b048da16f0b2547553 100644 --- a/test/test_infinera_classifier.py +++ b/test/test_infinera_classifier.py @@ -1,145 +1,13 @@ import json import jsonschema +from inventory_provider.routes.classifier_schema \ + import INFINERA_LAMBDA_INFO_RESPONSE_SCHEMA DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", "Accept": ["application/json"] } -LOCATIONS_DEFINITIONS = { - "location-endpoint": { - "type": "object", - "properties": { - "equipment": {"type": "string"}, - "name": {"type": "string"}, - "abbreviation": {"type": "string"} - }, - "required": ["equipment", "name", "abbreviation"], - "additionalProperties": False - }, - "location": { - "type": "object", - "properties": { - "a": {"$ref": "#/definitions/location-endpoint"}, - "b": {"$ref": "#/definitions/location-endpoint"} - }, - "required": ["a"], - "additionalProperties": False - }, - "locations-list": { - "type": "array", - "items": {"$ref": "#/definitions/location"} - } -} - -INFINERA_LINK_METADATA_DEFINITIONS = { - "service-info": { - "type": "object", - "properties": { - "id": {"type": "integer"}, - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "service_type": {"type": "string"}, - "project": {"type": "string"}, - "pop_name": {"type": "string"}, - "pop_abbreviation": {"type": "string"}, - "other_end_pop_name": {"type": "string"}, - "other_end_pop_abbreviation": {"type": "string"}, - "equipment": {"type": "string"}, - "other_end_equipment": {"type": "string"}, - "port": {"type": "string"}, - "other_end_port": {"type": "string"}, - "logical_unit": { - "oneOf": [ - {"type": "integer"}, - {"type": "string", "maxLength": 0} - ] - }, - "other_end_logical_unit": { - "oneOf": [ - {"type": "integer"}, - {"type": "string", "maxLength": 0} - ] - }, - "manufacturer": { - "type": "string", - "enum": ["juniper", "coriant", "infinera", - "cisco", "hewlett packard", - "corsa", "graham smith uk ltd", - "unknown", ""] - }, - "card_id": {"type": "string"}, - "other_end_card_id": {"type": "string"}, - "interface_name": {"type": "string"}, - "other_end_interface_name": {"type": "string"} - }, - "additionalProperties": False - }, - "related-service-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - "circuit_type": { - "type": "string", - "enum": ["path", "service", "l2circuit"] - }, - "project": {"type": "string"} - }, - "required": ["name", "status", "circuit_type", "project"], - "additionalProperties": False - }, - "geant-lambda": { - "type": "object", - "properties": { - "id": {"type": "integer"}, - "name": {"type": "string"}, - "project": {"type": "string"}, - "status": { - "type": "string", - "enum": ["operational", "installed", "planned", "ordered"] - }, - }, - "additionalProperties": False - } -} - -INFINERA_LINK_METADATA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - - "definitions": { - **INFINERA_LINK_METADATA_DEFINITIONS, **LOCATIONS_DEFINITIONS - }, - - "type": "object", - "properties": { - "services": { - "type": "array", - "items": {"$ref": "#/definitions/service-info"} - }, - "related-services": { - "type": "array", - "items": {"$ref": "#/definitions/related-service-info"} - }, - "geant-lambda": { - "$ref": "#/definitions/geant-lambda" - }, - "locations": {"$ref": "#/definitions/locations-list"} - }, - "additionalProperties": False -} - def test_trap_metadata(client): rv = client.get( @@ -149,4 +17,4 @@ def test_trap_metadata(client): assert rv.status_code == 200 assert rv.is_json response_data = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(response_data, INFINERA_LINK_METADATA) + jsonschema.validate(response_data, INFINERA_LAMBDA_INFO_RESPONSE_SCHEMA) diff --git a/test/test_job_routes.py b/test/test_job_routes.py index 33bbde510defb4d7ca6642aa77a1292e2531c711..f3a4df17d8541322bda4af2634d91ab59e90bb23 100644 --- a/test/test_job_routes.py +++ b/test/test_job_routes.py @@ -2,59 +2,15 @@ import json import jsonschema from inventory_provider.tasks.common import _get_redis, DB_LATCH_SCHEMA +from inventory_provider.routes.jobs \ + import INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA, \ + TASK_LOG_RESPONSE_SCHEMA, TASK_ID_RESPONSE_SCHEMA DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", "Accept": ["application/json"] } -TASK_ID_RESPONSE_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "task id": {"type": "string"} - }, - "required": ["task id"], - "additionalProperties": False -} - -TASK_STATUS_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "task": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "status": {"type": "string"}, - "exception": {"type": "boolean"}, - "ready": {"type": "boolean"}, - "success": {"type": "boolean"}, - "result": {"type": ["object", "null"]}, - "parent": {"type": ["string", "null"]} - }, - "required": [ - "id", "status", "exception", "ready", "success", "parent"], - "additionalProperties": False - } - }, - - "type": "array", - "items": {"$ref": "#/definitions/task"} -} - -TASK_LOG_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "pending": {"type": "array", "items": {"type": "string"}}, - "errors": {"type": "array", "items": {"type": "string"}}, - "failed": {"type": "array", "items": {"type": "string"}}, - "warnings": {"type": "array", "items": {"type": "string"}}, - }, - "required": ["pending", "errors", "failed", "warnings"], - "additionalProperties": False -} - def backend_db(): return _get_redis({ @@ -175,7 +131,7 @@ def test_check_update_status(client, mocker): headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 200 result = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(result, TASK_STATUS_SCHEMA) + jsonschema.validate(result, INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA) for status in result: assert status['id'] == 'zz55' assert status['status'] == 'SUCCESS' @@ -207,7 +163,7 @@ def test_check_task_status_success(client, mocker): headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 200 result = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(result, TASK_STATUS_SCHEMA) + jsonschema.validate(result, INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA) for status in result: assert status['id'] == 'abc' assert status['status'] == 'SUCCESS' @@ -228,7 +184,7 @@ def test_check_task_status_custom_status(client, mocker): headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 200 result = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(result, TASK_STATUS_SCHEMA) + jsonschema.validate(result, INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA) for status in result: assert status['id'] == 'xyz' assert status['status'] == 'custom' @@ -248,7 +204,7 @@ def test_check_task_status_exception(client, mocker): headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 200 result = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(result, TASK_STATUS_SCHEMA) + jsonschema.validate(result, INDIVIDUAL_TASK_STATUS_RESPONSE_SCHEMA) for status in result: assert status['id'] == '123-xyz.ABC' assert status['status'] == 'FAILURE' @@ -380,7 +336,7 @@ def test_job_log(client): headers=DEFAULT_REQUEST_HEADERS) assert rv.status_code == 200 result = json.loads(rv.data.decode('utf-8')) - jsonschema.validate(result, TASK_LOG_SCHEMA) + jsonschema.validate(result, TASK_LOG_RESPONSE_SCHEMA) assert len(result['errors']) == 3 assert len(result['pending']) == 3 diff --git a/test/test_lg_routes.py b/test/test_lg_routes.py index e45e3b599bc0d4a2bc293e78ba95e01af9fb91e8..add33c4b9b1008931fc90fd1d6e0683dd36b3cb4 100644 --- a/test/test_lg_routes.py +++ b/test/test_lg_routes.py @@ -1,57 +1,13 @@ import json import jsonschema import pytest +from inventory_provider.routes.lg import LG_ROUTERS_SCHEMA DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", "Accept": ["application/json"] } -LG_ROUTERS_SCHEMA = { - "$schema": "http://json-schema.org/draft-07/schema#", - - "definitions": { - "pop-info": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "abbreviation": {"type": "string"}, - "country": {"type": "string"}, - "country code": {"type": "string"}, - "city": {"type": "string"}, - "longitude": {"type": "number"}, - "latitude": {"type": "number"} - }, - "required": [ - "name", - "abbreviation", - "country", - "country code", - "city", - "longitude", - "latitude" - ], - "additionalProperties": False - }, - "router": { - "type": "object", - "properties": { - "equipment name": {"type": "string"}, - "type": { - "type": "string", - "enum": ["INTERNAL", "CORE"] - }, - "pop": {"$ref": "#/definitions/pop-info"} - }, - "required": ["equipment name", "type", "pop"], - "additionalProperties": False - } - }, - - "type": "array", - "items": {"$ref": "#/definitions/router"} -} - def test_public_routers(client): rv = client.get( diff --git a/test/test_msr_routes.py b/test/test_msr_routes.py index 47a9d42e304b2d270c5ea60b6d7a2df09d3a13d8..b842938d24d6e89679f58c853cacf03d92f22624 100644 --- a/test/test_msr_routes.py +++ b/test/test_msr_routes.py @@ -1,41 +1,12 @@ import json import jsonschema +from inventory_provider.routes.msr import ACCESS_SERVICES_LIST_SCHEMA DEFAULT_REQUEST_HEADERS = { "Content-type": "application/json", "Accept": ["application/json"] } -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"} -} - def test_access_services(client): rv = client.get( diff --git a/tox.ini b/tox.ini index f08e8f1c3512dd36edca87ebb6b20de3a3212053..8d569cbc25d09e7397d4dc13d97de759cdcbf228 100644 --- a/tox.ini +++ b/tox.ini @@ -17,4 +17,5 @@ commands = coverage report --fail-under 75 # coverage report --fail-under 80 flake8 + sphinx-build -M html docs/source docs/build