diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..941c7b4ddfb4b2710d7812ac831b97cf6e8603b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# IDE related +.idea +.vscode + +# config +config.json + +# dev / builds +venv/ +*.egg-info +__pycache__ +coverage.xml +.coverage +htmlcov +.tox +dist + + +# logs +*.log diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000000000000000000000000000000000..c9ad86290416cde3e77c618bb77bc574c5482b09 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include brian_dashboard_manager/logging_default_config.json +include brian_dashboard_manager/dashboards/* +include brian_dashboard_manager/datasources/* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8cae3b99e4e03cbeb9198d8f2eee1d6e3b051617 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# Skeleton Web App + +## Overview + +This module implements a skeleton Flask-based webservice. + +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 + +This app allows specification of a few +example configuration parameters. These +parameters should stored in a file formatted +similarly to `config.json.example`, and the name +of this file should be stored in the environment +variable `CONFIG_FILENAME` when running the service. + +## 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: + +```bash +$ export FLASK_APP=app.py +$ export CONFIG_FILENAME=config.json +$ 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 + + * /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 + } + ``` + +#### /test/test1 + +The response will be some json data, as an example ... diff --git a/brian_dashboard_manager/__init__.py b/brian_dashboard_manager/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e929766837764b2469403562629410388d702957 --- /dev/null +++ b/brian_dashboard_manager/__init__.py @@ -0,0 +1,42 @@ +""" +automatically invoked app factory +""" +import logging +import os + +from flask import Flask + +from brian_dashboard_manager import environment +from brian_dashboard_manager import config + +CONFIG_KEY = 'CONFIG_PARAMS' + + +def create_app(): + """ + overrides default settings with those found + in the file read from env var CONFIG_FILENAME + + :return: a new flask app instance + """ + + required_env_vars = ['CONFIG_FILENAME'] + + assert all([n in os.environ for n in required_env_vars]), \ + 'environment variables %r must be defined' % required_env_vars + + app_config = config.defaults() + if 'CONFIG_FILENAME' in os.environ: + with open(os.environ['CONFIG_FILENAME']) as f: + app_config.update(config.load(f)) + + app = Flask(__name__) + app.secret_key = os.environ.get('SECRET_KEY') or 'super secret session key' + app.config[CONFIG_KEY] = app_config + + from brian_dashboard_manager.routes import update + app.register_blueprint(update.routes, url_prefix='/update') + + logging.info('Flask app initialized') + environment.setup_logging() + return app diff --git a/brian_dashboard_manager/app.py b/brian_dashboard_manager/app.py new file mode 100644 index 0000000000000000000000000000000000000000..5bc14b921b5f95fb5c68b08e83b5491952de018e --- /dev/null +++ b/brian_dashboard_manager/app.py @@ -0,0 +1,11 @@ +""" +default app creation +""" +import brian_dashboard_manager +from brian_dashboard_manager import environment, CONFIG_KEY + +environment.setup_logging() +app = brian_dashboard_manager.create_app() + +if __name__ == "__main__": + app.run(host="::", port=f"{app.config[CONFIG_KEY]['listen_port']}") diff --git a/brian_dashboard_manager/config.py b/brian_dashboard_manager/config.py new file mode 100644 index 0000000000000000000000000000000000000000..83840e871392e7ed12406bdd43adc6740ffcaff8 --- /dev/null +++ b/brian_dashboard_manager/config.py @@ -0,0 +1,139 @@ +import json +import jsonschema + +DEFAULT_ORGANIZATIONS = [ + { + "name": "GÉANT Staff", + "excluded_nrens": [], + "excluded_dashboards": [] + }, + { + "name": "NRENs", + "excluded_nrens": [], + "excluded_dashboards": [ + "GÉANT Office devices", + "GÉANT VM" + ] + }, + { + "name": "General Public", + "excluded_nrens": [ + "CARNET", + "PSNC" + ], + "excluded_dashboards": [ + "GÉANT Office devices", + "GÉANT VM" + ] + }, + { + "name": "CAE1 - Europe", + "excluded_nrens": [], + "excluded_dashboards": [ + "GÉANT Office devices", + "GÉANT VM" + ] + }, + { + "name": "CAE1 - Asia", + "excluded_nrens": [ + "CARNET", + "PSNC" + ], + "excluded_dashboards": [ + "GÉANT Office devices", + "GÉANT VM" + ] + } +] + +CONFIG_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + + "definitions": { + "influx-datasource": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "username": {"type": "string"}, + "password": {"type": "string"}, + "type": {"type": "string"}, + "url": {"type": "string"}, + "database": {"type": "string"}, + "basicAuth": {"type": "boolean"}, + "access": {"type": "string"}, + "isDefault": {"type": "boolean"}, + "readOnly": {"type": "boolean"} + }, + "required": [ + "name", + "type", + "url", + "database", + "basicAuth", + "access", + "isDefault", + "readOnly" + ] + }, + "organization": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "excluded_nrens": { + "type": "array", + "items": {"type": "string"} + }, + }, + "required": [ + "name", + "excluded_nrens", + ] + } + }, + + "type": "object", + "properties": { + "admin_username": {"type": "string"}, + "admin_password": {"type": "string"}, + "hostname": {"type": "string"}, + "listen_port": {"type": "integer"}, + "inventory_provider": {"type": "string"}, + "datasources": { + "type": "object", + "properties": { + "influxdb": {"$ref": "#definitions/influx-datasource"} + }, + "additionalProperties": False + }, + }, + "required": [ + "admin_username", + "admin_password", + "hostname", + "inventory_provider", + "datasources" + ] +} + + +def defaults(): + return { + "admin_username": "admin", + "admin_password": "admin", + "hostname": "localhost:3000", + "listen_port": 3001, + "datasources": {} + } + + +def load(f): + """ + loads, validates and returns configuration parameters + + :param f: file-like object that produces the config file + :return: + """ + config = json.loads(f.read()) + jsonschema.validate(config, CONFIG_SCHEMA) + return config diff --git a/brian_dashboard_manager/dashboards/cls_peers.json b/brian_dashboard_manager/dashboards/cls_peers.json new file mode 100755 index 0000000000000000000000000000000000000000..aea75fe68423544c7810eec820fad5860ec93aa5 --- /dev/null +++ b/brian_dashboard_manager/dashboards/cls_peers.json @@ -0,0 +1,128 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [ + { + "icon": "external link", + "tags": [ + "cls_peers" + ], + "type": "dashboards", + "targetBlank": true + } + ], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 24, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 1, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "CLS" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Cloud Services (CLS)", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "peers" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "CLS Peers", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/home.json b/brian_dashboard_manager/dashboards/home.json new file mode 100755 index 0000000000000000000000000000000000000000..15fa12fd39b2d2a601962f669fc0ac70b28aff68 --- /dev/null +++ b/brian_dashboard_manager/dashboards/home.json @@ -0,0 +1,709 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 49, + "iteration": 1595947519970, + "links": [ + { + "asDropdown": true, + "icon": "external link", + "tags": [ + "services" + ], + "targetBlank": true, + "title": "Services", + "type": "dashboards" + }, + { + "asDropdown": true, + "icon": "external link", + "tags": [ + "infrastructure" + ], + "targetBlank": true, + "title": "Infrastructure", + "type": "dashboards" + }, + { + "asDropdown": true, + "icon": "external link", + "tags": [ + "customers" + ], + "targetBlank": true, + "title": "NREN Access", + "type": "dashboards" + }, + { + "asDropdown": true, + "icon": "external link", + "tags": [ + "peers" + ], + "targetBlank": true, + "title": "Peers", + "type": "dashboards" + } + ], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "PollerInfluxDB", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 3, + "gridPos": { + "h": 14, + "w": 12, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pluginVersion": "7.1.1", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Ingress Traffic", + "groupBy": [ + { + "params": [ + "5m" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "ingress" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + }, + { + "params": [ + "*8" + ], + "type": "math" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Egress Traffic", + "groupBy": [ + { + "params": [ + "5m" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "egress" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + }, + { + "params": [ + "*8" + ], + "type": "math" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Ingress 95th Percentile", + "groupBy": [], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "C", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "ingress" + ], + "type": "field" + }, + { + "params": [ + 95 + ], + "type": "percentile" + }, + { + "params": [ + "*8" + ], + "type": "math" + } + ] + ], + "tags": [ + { + "condition": null, + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Egress 95th Percentile", + "groupBy": [], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "D", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "egress" + ], + "type": "field" + }, + { + "params": [ + 95 + ], + "type": "percentile" + }, + { + "params": [ + "*8" + ], + "type": "math" + } + ] + ], + "tags": [ + { + "condition": null, + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$hostname - $interface_name - Traffic", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "PollerInfluxDB", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 3, + "gridPos": { + "h": 14, + "w": 12, + "x": 12, + "y": 0 + }, + "hiddenSeries": false, + "id": 3, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pluginVersion": "7.1.1", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Inbound", + "groupBy": [ + { + "params": [ + "5m" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "ingressv6" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + }, + { + "params": [ + "*8" + ], + "type": "math" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Outbound", + "groupBy": [ + { + "params": [ + "5m" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "egressv6" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + }, + { + "params": [ + "*8" + ], + "type": "math" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Ingress 95th Percentile", + "groupBy": [], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "C", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "ingressv6" + ], + "type": "field" + }, + { + "params": [ + 95 + ], + "type": "percentile" + }, + { + "params": [ + "*8" + ], + "type": "math" + } + ] + ], + "tags": [ + { + "condition": null, + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Egress 95th Percentile", + "groupBy": [], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "D", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "egressv6" + ], + "type": "field" + }, + { + "params": [ + 95 + ], + "type": "percentile" + }, + { + "params": [ + "*8" + ], + "type": "math" + } + ] + ], + "tags": [ + { + "condition": null, + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$hostname - $interface_name - IPv6 Traffic", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "datasource": "PollerInfluxDB", + "definition": "SHOW TAG VALUES ON poller WITH KEY=hostname", + "hide": 0, + "includeAll": false, + "label": "Router:", + "multi": false, + "name": "hostname", + "options": [], + "query": "SHOW TAG VALUES ON poller WITH KEY=hostname", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "datasource": "PollerInfluxDB", + "definition": "SHOW TAG VALUES ON poller WITH KEY IN (interface_name) WHERE hostname =~ /$hostname/ ", + "hide": 0, + "includeAll": false, + "label": "Interface :", + "multi": false, + "name": "interface_name", + "options": [], + "query": "SHOW TAG VALUES ON poller WITH KEY IN (interface_name) WHERE hostname =~ /$hostname/ ", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Home", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/ias_peers.json b/brian_dashboard_manager/dashboards/ias_peers.json new file mode 100755 index 0000000000000000000000000000000000000000..7554df09d51de5578cee4adab3a10952a06a37e8 --- /dev/null +++ b/brian_dashboard_manager/dashboards/ias_peers.json @@ -0,0 +1,196 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [ + { + "icon": "external link", + "tags": [ + "gws_upstreams" + ], + "type": "dashboards", + "targetBlank": true + } + ], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 12, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 1, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "IAS_PUBLIC" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "IAS Public", + "type": "dashlist" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 12, + "x": 12, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "IAS_PRIVATE" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "IAS Private", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "peers" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "IAS Peers", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/infrastructure_backbone.json b/brian_dashboard_manager/dashboards/infrastructure_backbone.json new file mode 100755 index 0000000000000000000000000000000000000000..56d7e9bf2101a34c011ffb84e4fd22ce33c33274 --- /dev/null +++ b/brian_dashboard_manager/dashboards/infrastructure_backbone.json @@ -0,0 +1,119 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 24, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "BACKBONE" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "infrastructure" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "GÉANT Backbone", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/infrastructure_office.json b/brian_dashboard_manager/dashboards/infrastructure_office.json new file mode 100644 index 0000000000000000000000000000000000000000..2255dce494d57a8affc8acabb64eab38537bedfa --- /dev/null +++ b/brian_dashboard_manager/dashboards/infrastructure_office.json @@ -0,0 +1,712 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 6858, + "iteration": 1607526562704, + "links": [], + "panels": [ + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 12, + "panels": [], + "repeat": "interface_name", + "scopedVars": { + "interface_name": { + "selected": true, + "text": "fxp0", + "value": "fxp0" + } + }, + "title": "$hostname-$interface_name", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 1 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatDirection": "v", + "scopedVars": { + "interface_name": { + "selected": true, + "text": "fxp0", + "value": "fxp0" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Ingress", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "ingress" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Egress", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "egress" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Traffic $hostname-$interface_name", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:211", + "format": "bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:212", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 12 + }, + "hiddenSeries": false, + "id": 30, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "interface_name": { + "selected": true, + "text": "fxp0", + "value": "fxp0" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Ingress", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "ingressv6" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Egress", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "egressv6" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "IPV6 - $hostname-$interface_name", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:391", + "format": "bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:392", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 12 + }, + "hiddenSeries": false, + "id": 56, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "interface_name": { + "selected": true, + "text": "fxp0", + "value": "fxp0" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "In", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "errorsIn" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + } + ] + }, + { + "alias": "Out", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "errorsOut" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Errors - $hostname-$interface_name", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:547", + "format": "err/s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:548", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "infrastructure" + ], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": false, + "text": "srx1.am.office.geant.net", + "value": "srx1.am.office.geant.net" + }, + "error": null, + "hide": 0, + "includeAll": false, + "label": "Hostname", + "multi": false, + "name": "hostname", + "options": [ + { + "selected": true, + "text": "srx1.am.office.geant.net", + "value": "srx1.am.office.geant.net" + }, + { + "selected": false, + "text": "srx2.am.office.geant.net", + "value": "srx2.am.office.geant.net" + }, + { + "selected": false, + "text": "srx1.ch.office.geant.net", + "value": "srx1.ch.office.geant.net" + }, + { + "selected": false, + "text": "srx2.ch.office.geant.net", + "value": "srx2.ch.office.geant.net" + } + ], + "query": "srx1.am.office.geant.net,srx2.am.office.geant.net,srx1.ch.office.geant.net,srx2.ch.office.geant.net", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "allValue": null, + "current": { + "selected": true, + "text": [ + "fxp0" + ], + "value": [ + "fxp0" + ] + }, + "datasource": "PollerInfluxDB", + "definition": "", + "error": null, + "hide": 0, + "includeAll": true, + "label": "Interface Name", + "multi": true, + "name": "interface_name", + "options": [], + "query": "SHOW TAG VALUES ON poller WITH KEY IN (interface_name) WHERE hostname =~ /$hostname/ ", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "GÉANT Office devices", + "uid": "WtN6DG1Gk", + "version": 17 + } \ No newline at end of file diff --git a/brian_dashboard_manager/dashboards/infrastructure_vm.json b/brian_dashboard_manager/dashboards/infrastructure_vm.json new file mode 100644 index 0000000000000000000000000000000000000000..3cea9a9f32a8acaced302f357626dd070d01ec00 --- /dev/null +++ b/brian_dashboard_manager/dashboards/infrastructure_vm.json @@ -0,0 +1,715 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 6859, + "iteration": 1607524936465, + "links": [], + "panels": [ + { + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "panels": [], + "repeat": "interface_name", + "scopedVars": { + "interface_name": { + "selected": true, + "text": "ae10", + "value": "ae10" + } + }, + "title": "$hostname-$interface_name", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 1 + }, + "hiddenSeries": false, + "id": 4, + "legend": { + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "interface_name": { + "selected": true, + "text": "ae10", + "value": "ae10" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Ingress", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "ingress" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + }, + { + "condition": "AND", + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + } + ] + }, + { + "alias": "Egress", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "egress" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Traffic $hostname-$interface_name", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:516", + "format": "bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:517", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 11 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "interface_name": { + "selected": true, + "text": "ae10", + "value": "ae10" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "Ingress", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "ingressv6" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Egress", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "egressv6" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "IPV6 - $hostname-$interface_name", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:675", + "format": "bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:676", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 11 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.4", + "pointradius": 2, + "points": false, + "renderer": "flot", + "scopedVars": { + "interface_name": { + "selected": true, + "text": "ae10", + "value": "ae10" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "In", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "errorsIn" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + }, + { + "alias": "Out", + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "linear" + ], + "type": "fill" + } + ], + "measurement": "interface_rates", + "orderByTime": "ASC", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "errorsOut" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "hostname", + "operator": "=~", + "value": "/^$hostname$/" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=~", + "value": "/^$interface_name$/" + } + ] + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Errors - $hostname-$interface_name", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "$$hashKey": "object:865", + "format": "err/s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "$$hashKey": "object:866", + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "infrastructure" + ], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "selected": false, + "text": "qfx.fra.de.geant.net", + "value": "qfx.fra.de.geant.net" + }, + "error": null, + "hide": 0, + "includeAll": false, + "label": "Hostname", + "multi": false, + "name": "hostname", + "options": [ + { + "selected": true, + "text": "qfx.fra.de.geant.net", + "value": "qfx.fra.de.geant.net" + }, + { + "selected": false, + "text": "qfx.par.fr.geant.net", + "value": "qfx.par.fr.geant.net" + }, + { + "selected": false, + "text": "qfx.lon2.2.uk.geant.net", + "value": "qfx.lon2.2.uk.geant.net" + } + ], + "query": "qfx.fra.de.geant.net,qfx.par.fr.geant.net,qfx.lon2.2.uk.geant.net", + "skipUrlSync": false, + "type": "custom" + }, + { + "allValue": null, + "current": { + "selected": true, + "tags": [], + "text": [ + "ae10" + ], + "value": [ + "ae10" + ] + }, + "datasource": "PollerInfluxDB", + "definition": "", + "error": null, + "hide": 0, + "includeAll": true, + "label": "Interface Name", + "multi": true, + "name": "interface_name", + "query": "SHOW TAG VALUES ON poller WITH KEY IN (interface_name) WHERE hostname =~ /$hostname/ ", + "refresh": 0, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "GÉANT VM", + "uid": "tCEkFG1Mk", + "version": 7 + } \ No newline at end of file diff --git a/brian_dashboard_manager/dashboards/peers.json b/brian_dashboard_manager/dashboards/peers.json new file mode 100755 index 0000000000000000000000000000000000000000..d4fb2c721e05f3b5e3054020726119f4b1b759cf --- /dev/null +++ b/brian_dashboard_manager/dashboards/peers.json @@ -0,0 +1,119 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 24, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 1, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "RE_PEER" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "R&E Peers", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "peers" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "R&E Peers", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/services_automated_l2_circuits.json b/brian_dashboard_manager/dashboards/services_automated_l2_circuits.json new file mode 100755 index 0000000000000000000000000000000000000000..30135ec565bbca2af2e78fd9ccaef6610dd621b3 --- /dev/null +++ b/brian_dashboard_manager/dashboards/services_automated_l2_circuits.json @@ -0,0 +1,119 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 24, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "AUTOMATED_L2_CIRCUITS" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "services" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Automated L2 Circuits", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/services_cls.json b/brian_dashboard_manager/dashboards/services_cls.json new file mode 100755 index 0000000000000000000000000000000000000000..4e142c5114ecd11e0c6d22a90a0f3355a7b06e49 --- /dev/null +++ b/brian_dashboard_manager/dashboards/services_cls.json @@ -0,0 +1,119 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 24, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "CLS" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "services" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Cloud Services (CLS)", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/services_geant_open.json b/brian_dashboard_manager/dashboards/services_geant_open.json new file mode 100755 index 0000000000000000000000000000000000000000..6e16f6d9e68b58df9b28750e182f4c78b2a9bd91 --- /dev/null +++ b/brian_dashboard_manager/dashboards/services_geant_open.json @@ -0,0 +1,128 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [ + { + "icon": "external link", + "tags": [ + "cae" + ], + "type": "dashboards", + "targetBlank": true + } + ], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 24, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "GEANTOPEN" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "services" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "GÉANT Open", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/services_ias.json b/brian_dashboard_manager/dashboards/services_ias.json new file mode 100755 index 0000000000000000000000000000000000000000..94964d84958a26724cf70b3a31f88a487614c1b0 --- /dev/null +++ b/brian_dashboard_manager/dashboards/services_ias.json @@ -0,0 +1,340 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [ + { + "icon": "external link", + "tags": [ + "ias_peers" + ], + "type": "dashboards", + "targetBlank": true + }, + { + "icon": "external link", + "tags": [ + "gws_upstreams" + ], + "type": "dashboards", + "targetBlank": true + } + ], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 6, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "IAS_UPSTREAM" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "IAS UPSTREAM", + "type": "dashlist" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 6, + "x": 6, + "y": 0 + }, + "headings": false, + "id": 3, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "IAS_CUSTOMER" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "IAS CUSTOMER", + "type": "dashlist" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 6, + "x": 12, + "y": 0 + }, + "headings": false, + "id": 4, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "IAS_PUBLIC" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "IAS PUBLIC", + "type": "dashlist" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 6, + "x": 18, + "y": 0 + }, + "headings": false, + "id": 5, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "IAS_PRIVATE" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "IAS PRIVATE", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "services" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "IAS", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/services_l2_circuits.json b/brian_dashboard_manager/dashboards/services_l2_circuits.json new file mode 100755 index 0000000000000000000000000000000000000000..7546e992998cd8dd3a6827f89e7972d1f51b17ac --- /dev/null +++ b/brian_dashboard_manager/dashboards/services_l2_circuits.json @@ -0,0 +1,119 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 24, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "L2_CIRCUITS" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "services" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "L2 Circuits", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/services_lhcone.json b/brian_dashboard_manager/dashboards/services_lhcone.json new file mode 100755 index 0000000000000000000000000000000000000000..b2d5c09f4b350f291382ff7f866ab0a27f1d7149 --- /dev/null +++ b/brian_dashboard_manager/dashboards/services_lhcone.json @@ -0,0 +1,196 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [ + { + "icon": "external link", + "tags": [ + "lhcone" + ], + "type": "dashboards", + "targetBlank": true + } + ], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 12, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "LHCONE_CUST" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "LHCONE CUST", + "type": "dashlist" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 12, + "x": 12, + "y": 0 + }, + "headings": false, + "id": 3, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "LHCONE_PEER" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "LHCONE PEER", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "services" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "LHCONE", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/services_mdvpn.json b/brian_dashboard_manager/dashboards/services_mdvpn.json new file mode 100755 index 0000000000000000000000000000000000000000..a8e4880c3ef8be1698ba89edc5b628b2a7b3bc2b --- /dev/null +++ b/brian_dashboard_manager/dashboards/services_mdvpn.json @@ -0,0 +1,119 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 24, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "MDVPN" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "services" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "MDVPN", + "version": 1 +} diff --git a/brian_dashboard_manager/dashboards/services_re.json b/brian_dashboard_manager/dashboards/services_re.json new file mode 100755 index 0000000000000000000000000000000000000000..3cb1932056ef2b84f94a4a5ec9dd56116b7a6c1c --- /dev/null +++ b/brian_dashboard_manager/dashboards/services_re.json @@ -0,0 +1,187 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 454, + "links": [], + "panels": [ + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 12, + "x": 0, + "y": 0 + }, + "headings": false, + "id": 1, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "RE_CUST" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "RE CUST", + "type": "dashlist" + }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "folderId": null, + "gridPos": { + "h": 25, + "w": 12, + "x": 12, + "y": 0 + }, + "headings": false, + "id": 2, + "limit": 100, + "pluginVersion": "7.1.4", + "query": "", + "recent": false, + "search": true, + "starred": false, + "tags": [ + "RE_PEER" + ], + "targets": [ + { + "groupBy": [ + { + "params": [ + "$__interval" + ], + "type": "time" + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "orderByTime": "ASC", + "policy": "default", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [] + } + ], + "timeFrom": null, + "timeShift": null, + "title": "RE PEER", + "type": "dashlist" + } + ], + "schemaVersion": 26, + "style": "dark", + "tags": [ + "services" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "R&E", + "version": 1 +} diff --git a/readme.md b/brian_dashboard_manager/datasources/.gitkeep similarity index 100% rename from readme.md rename to brian_dashboard_manager/datasources/.gitkeep diff --git a/brian_dashboard_manager/environment.py b/brian_dashboard_manager/environment.py new file mode 100644 index 0000000000000000000000000000000000000000..b5057098e019cdb840eaf1b3599ad3053a21c529 --- /dev/null +++ b/brian_dashboard_manager/environment.py @@ -0,0 +1,23 @@ +import json +import logging.config +import os + + +def setup_logging(): + """ + set up logging using the configured filename + + if LOGGING_CONFIG is defined in the environment, use this for + the filename, otherwise use logging_default_config.json + """ + default_filename = os.path.join( + os.path.dirname(__file__), 'logging_default_config.json') + filename = os.getenv('LOGGING_CONFIG', default_filename) + with open(filename) as f: + # TODO: this mac workaround should be removed ... + d = json.loads(f.read()) + import platform + if platform.system() == 'Darwin': + d['handlers']['syslog_handler']['address'] = '/var/run/syslog' + logging.config.dictConfig(d) + # logging.config.dictConfig(json.loads(f.read())) diff --git a/brian_dashboard_manager/grafana/__init__.py b/brian_dashboard_manager/grafana/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/brian_dashboard_manager/grafana/dashboard.py b/brian_dashboard_manager/grafana/dashboard.py new file mode 100644 index 0000000000000000000000000000000000000000..bac998e7b23299eec91ff8d3c81b0cd66355dc85 --- /dev/null +++ b/brian_dashboard_manager/grafana/dashboard.py @@ -0,0 +1,131 @@ +import logging +import os +import json +from typing import Dict + +from requests.models import HTTPError +from brian_dashboard_manager.grafana.utils.request import TokenRequest + +logger = logging.getLogger(__name__) + + +# Returns dictionary for each dashboard JSON definition in supplied directory +def get_dashboard_definitions(dir=None): # pragma: no cover + dashboard_dir = dir or os.path.join( + os.path.dirname(__file__), '../dashboards/') + for (dirpath, _, filenames) in os.walk(dashboard_dir): + for file in filenames: + if file.endswith('.json'): + filename = os.path.join(dirpath, file) + dashboard = json.load(open(filename, 'r')) + yield dashboard + + +# Deletes a single dashboard for the organization +# the API token is registered to. +def _delete_dashboard(request: TokenRequest, uid: int): + try: + r = request.delete(f'api/dashboards/uid/{uid}') + if r and 'deleted' in r.get('message', ''): + return r + except HTTPError: + logger.exception(f'Error when deleting dashboard with UID #{uid}') + return None + + +# Deletes all dashboards for the organization +# the API token is registered to. +def delete_dashboards(request: TokenRequest): + r = request.get('api/search') + if r and len(r) > 0: + for dash in r: + _delete_dashboard(request, dash['uid']) + return True + + +# Searches for a dashboard with given title +def find_dashboard(request: TokenRequest, title): + r = request.get('api/search', params={ + 'query': title + }) + if r and len(r) > 0: + return r[0] + return None + +# Searches Grafana for a dashboard +# matching the title of the provided dashboard. + + +def _search_dashboard(request: TokenRequest, dashboard: Dict, folder_id=None): + try: + r = request.get('api/search', params={ + 'query': dashboard["title"] + }) + if r and isinstance(r, list): + if len(r) >= 1: + for dash in r: + if folder_id: + if folder_id != dash.get('folderId'): + continue + if dash['title'] == dashboard['title']: + definition = _get_dashboard(request, dash['uid']) + return definition + return None + except HTTPError: + return None + + +# Fetches dashboard with given UID for the token's organization. +def _get_dashboard(request: TokenRequest, uid: int): + + try: + r = request.get(f'api/dashboards/uid/{uid}') + except HTTPError: + return None + return r + + +# Creates or updates (if exists) given dashboard for the token's organization. +# supplied dashboards are JSON blobs exported from GUI with a UID. +def create_dashboard(request: TokenRequest, dashboard: Dict, folder_id=None): + + title = dashboard['title'] + existing_dashboard = None + has_uid = dashboard.get('uid') is not None + if has_uid: + existing_dashboard = _get_dashboard(request, uid=dashboard['uid']) + + # The title might not match the one that's provisioned with that UID. + # Try to find it by searching for the title instead. + if existing_dashboard is not None: + grafana_title = existing_dashboard['dashboard']['title'] + different = grafana_title != title + else: + different = False + + if existing_dashboard is None or different: + existing_dashboard = _search_dashboard(request, dashboard, folder_id) + + if existing_dashboard: + dashboard['uid'] = existing_dashboard['dashboard']['uid'] + dashboard['id'] = existing_dashboard['dashboard']['id'] + dashboard['version'] = existing_dashboard['dashboard']['version'] + else: + # We are creating a new dashboard, delete ID if it exists. + del dashboard['id'] + + payload = { + 'dashboard': dashboard, + 'overwrite': False + } + if folder_id: + payload['folderId'] = folder_id + + try: + action = "Updating" if existing_dashboard else "Creating" + logger.info(f'{action} dashboard: {title}') + r = request.post('api/dashboards/db', json=payload) + return r + except HTTPError: + logger.exception(f'Error when provisioning dashboard {title}') + return None diff --git a/brian_dashboard_manager/grafana/datasource.py b/brian_dashboard_manager/grafana/datasource.py new file mode 100644 index 0000000000000000000000000000000000000000..31dfa3e5b13f4963e94b067cc2a6a47c9218ffaf --- /dev/null +++ b/brian_dashboard_manager/grafana/datasource.py @@ -0,0 +1,70 @@ +import logging +import os +import json +from typing import Dict + +from requests.exceptions import HTTPError +from brian_dashboard_manager.grafana.utils.request import Request, TokenRequest + + +logger = logging.getLogger(__name__) + + +def _datasource_provisioned(datasource_to_check, provisioned_datasources): + if len(datasource_to_check.keys()) == 0: + return True + for datasource in provisioned_datasources: + exists = all(datasource.get(key) == datasource_to_check.get(key) + for key in datasource_to_check) + if exists: + return True + return False + + +def get_missing_datasource_definitions(request: Request, dir=None): + datasource_dir = dir or os.path.join( + os.path.dirname(__file__), '../datasources/') + existing_datasources = get_datasources(request) + + def check_ds_not_provisioned(filename): + datasource = json.load(open(filename, 'r')) + if not _datasource_provisioned(datasource, existing_datasources): + return datasource + + for (dirpath, _, filenames) in os.walk(datasource_dir): # pragma: no cover + for file in filenames: + if not file.endswith('.json'): + continue + filename = os.path.join(dirpath, file) + yield check_ds_not_provisioned(filename) + + +def check_provisioned(request: TokenRequest, datasource): + existing_datasources = get_datasources(request) + return _datasource_provisioned(datasource, existing_datasources) + + +def get_datasources(request: Request): + return request.get('api/datasources') + + +def create_datasource(request: TokenRequest, datasource: Dict, datasources): + try: + ds_type = datasource["type"] + # find out which params + # we need to configure for this datasource type + config = datasources.get(ds_type, None) + if config is None: + logger.exception( + f'No datasource config could be found for {ds_type}') + return None + datasource.update(config) + r = request.post('api/datasources', json=datasource) + except HTTPError: + logger.exception('Error when provisioning datasource') + return None + return r + + +def delete_datasource(request: TokenRequest, name: str): + return request.delete(f'api/datasources/name/{name}') diff --git a/brian_dashboard_manager/grafana/folder.py b/brian_dashboard_manager/grafana/folder.py new file mode 100644 index 0000000000000000000000000000000000000000..181ea8e3171ad2863e7bbe13bf364496c942ae41 --- /dev/null +++ b/brian_dashboard_manager/grafana/folder.py @@ -0,0 +1,22 @@ +import logging +from requests.exceptions import HTTPError +from brian_dashboard_manager.grafana.utils.request import TokenRequest + + +logger = logging.getLogger(__name__) + + +def get_folders(request: TokenRequest): + return request.get('api/folders') + + +def create_folder(request: TokenRequest, title): + try: + data = {'title': title, 'uid': title.replace(' ', '_')} + r = request.post('api/folders', json=data) + except HTTPError as e: + message = e.content.get("message", "") + logger.exception( + f'Error when creating folder {title} ({message})') + return None + return r diff --git a/brian_dashboard_manager/grafana/organization.py b/brian_dashboard_manager/grafana/organization.py new file mode 100644 index 0000000000000000000000000000000000000000..4c01096f279801cbbbe7730cd0281a28c112c746 --- /dev/null +++ b/brian_dashboard_manager/grafana/organization.py @@ -0,0 +1,97 @@ +import random +import string +import logging +from typing import Dict, List, Union +from datetime import datetime + +from brian_dashboard_manager.grafana.utils.request import AdminRequest, \ + TokenRequest + +logger = logging.getLogger(__name__) + + +def switch_active_organization(request: AdminRequest, org_id: int): + assert org_id + + logger.debug(f'Switched {str(request)} active organization to #{org_id}') + return request.post(f'api/user/using/{org_id}', {}) + + +def get_organizations(request: AdminRequest) -> List: + return request.get('api/orgs') + + +def create_organization(request: AdminRequest, name: str) -> Union[Dict, None]: + assert name + + result = request.post('api/orgs', json={ + 'name': name + }) + + if result.get('message', '').lower() == 'organization created': + id = result.get('orgId') + logger.info(f'Created organization `{name}` with ID #{id}') + return {'id': id, 'name': name} + else: + return None + + +def delete_organization(request: AdminRequest, id: int) -> bool: + + result = request.delete(f'api/orgs/{id}') + + return result.get('message', '').lower() == 'organization deleted' + + +def create_api_token(request: AdminRequest, org_id: int, key_data=None): + characters = string.ascii_uppercase + string.digits + name = ''.join(random.choices(characters, k=16)) + data = { + 'name': name, + 'role': 'Admin', + 'secondsToLive': 3600 # 60 minutes + } + if key_data: + data.update(key_data) + + switch_active_organization(request, org_id) + result = request.post('api/auth/keys', json=data) + token_id = result.get('id') + + logger.debug(f'Created API token #{token_id} for organization #{org_id}') + + return result + + +def delete_api_token(request: AdminRequest, org_id: int, token_id: int): + assert token_id + + switch_active_organization(request, org_id) + result = request.delete(f'api/auth/keys/{token_id}') + logger.debug(f'Deleted API token #{token_id} for organization #{org_id}') + return result + + +def delete_expired_api_tokens(request: AdminRequest, org_id: int) -> bool: + assert org_id + + tokens = request.get('api/auth/keys', params={'includeExpired': True}) + + now = datetime.now() + + def is_expired(token): + date = datetime.strptime(token['expiration'], '%Y-%m-%dT%H:%M:%SZ') + return date < now + + expired_tokens = [t for t in tokens if 'expiration' in t and is_expired(t)] + + for token in expired_tokens: + delete_api_token(request, org_id, token['id']) + return True + + +def set_home_dashboard(request: TokenRequest, dashboard_id: int): + r = request.put('api/org/preferences', json={ + 'homeDashboardId': dashboard_id + }) + return r and r.get('message') == 'Preferences updated' diff --git a/brian_dashboard_manager/grafana/provision.py b/brian_dashboard_manager/grafana/provision.py new file mode 100644 index 0000000000000000000000000000000000000000..bafbbd1c79cbb73e5679ab8b55b97adbb8e61c2b --- /dev/null +++ b/brian_dashboard_manager/grafana/provision.py @@ -0,0 +1,189 @@ +import logging +from brian_dashboard_manager.config import DEFAULT_ORGANIZATIONS +from brian_dashboard_manager.grafana.utils.request import \ + AdminRequest, \ + TokenRequest +from brian_dashboard_manager.grafana.organization import \ + get_organizations, create_organization, create_api_token, \ + delete_api_token, delete_expired_api_tokens, set_home_dashboard +from brian_dashboard_manager.grafana.dashboard import \ + get_dashboard_definitions, create_dashboard, find_dashboard +from brian_dashboard_manager.grafana.datasource import \ + check_provisioned, create_datasource +from brian_dashboard_manager.grafana.folder import \ + get_folders, create_folder +from brian_dashboard_manager.inventory_provider.interfaces import \ + get_interfaces +from brian_dashboard_manager.templating.nren_access import generate_nrens + +from brian_dashboard_manager.templating.helpers import is_re_customer, \ + is_cls, is_ias_customer, is_ias_private, is_ias_public, is_ias_upstream, \ + is_lag_backbone, is_nren, is_phy_upstream, is_re_peer, is_gcs, \ + is_geantopen, is_l2circuit, is_lhcone_peer, is_lhcone_customer, is_mdvpn,\ + get_interface_data, parse_backbone_name, parse_phy_upstream_name, \ + get_dashboard_data + +from brian_dashboard_manager.templating.render import render_dashboard + +logger = logging.getLogger(__name__) + + +def provision(config): + + request = AdminRequest(**config) + all_orgs = get_organizations(request) + + orgs_to_provision = config.get('organizations', DEFAULT_ORGANIZATIONS) + + missing = (org['name'] for org in orgs_to_provision + if org['name'] not in [org['name'] for org in all_orgs]) + + for org_name in missing: + org_data = create_organization(request, org_name) + all_orgs.append(org_data) + + interfaces = get_interfaces(config['inventory_provider']) + for org in all_orgs: + org_id = org['id'] + delete_expired_api_tokens(request, org_id) + token = create_api_token(request, org_id) + token_request = TokenRequest(token=token['key'], **config) + + folders = get_folders(token_request) + + def find_folder(title): + + try: + folder = next( + f for f in folders if f['title'].lower() == title.lower()) + except StopIteration: + folder = None + + if not folder: + logger.info(f'Created folder: {title}') + folder = create_folder(token_request, title) + folders.append(folder) + return folder + + logger.info( + f'--- Provisioning org {org["name"]} (ID #{org_id}) ---') + + try: + org_config = next( + o for o in orgs_to_provision if o['name'] == org['name']) + except StopIteration: + org_config = None + + if not org_config: + logger.error( + f'Org {org["name"]} does not have valid configuration.') + org['info'] = 'Org exists in grafana but is not configured' + continue + + # Only provision influxdb datasource for now + datasource = config.get('datasources').get('influxdb') + + # Provision missing data sources + if not check_provisioned(token_request, datasource): + ds = create_datasource(token_request, + datasource, + config.get('datasources')) + if ds: + logger.info( + f'Provisioned datasource: {datasource["name"]}') + + excluded_nrens = org_config.get('excluded_nrens', []) + excluded_nrens = list(map(lambda f: f.lower(), excluded_nrens)) + + def excluded(interface): + desc = interface.get('description', '').lower() + return not any(nren.lower() in desc for nren in excluded_nrens) + + excluded_interfaces = list(filter(excluded, interfaces)) + + dashboards = { + 'CLS': {'predicate': is_cls, 'tag': 'CLS'}, + 'RE PEER': {'predicate': is_re_peer, 'tag': 'RE_PEER'}, + 'RE CUST': {'predicate': is_re_customer, 'tag': 'RE_CUST'}, + 'GEANTOPEN': {'predicate': is_geantopen, 'tag': 'GEANTOPEN'}, + 'GCS': {'predicate': is_gcs, 'tag': 'AUTOMATED_L2_CIRCUITS'}, + 'L2 CIRCUIT': {'predicate': is_l2circuit, 'tag': 'L2_CIRCUITS'}, + 'LHCONE PEER': {'predicate': is_lhcone_peer, 'tag': 'LHCONE_PEER'}, + 'LHCONE CUST': { + 'predicate': is_lhcone_customer, + 'tag': 'LHCONE_CUST' + }, + 'MDVPN Customers': {'predicate': is_mdvpn, 'tag': 'MDVPN'}, + 'Infrastructure Backbone': { + 'predicate': is_lag_backbone, + 'tag': 'BACKBONE', + 'errors': True, + 'parse_func': parse_backbone_name + }, + 'IAS PRIVATE': {'predicate': is_ias_private, 'tag': 'IAS_PRIVATE'}, + 'IAS PUBLIC': {'predicate': is_ias_public, 'tag': 'IAS_PUBLIC'}, + 'IAS CUSTOMER': { + 'predicate': is_ias_customer, + 'tag': 'IAS_CUSTOMER' + }, + 'IAS UPSTREAM': { + 'predicate': is_ias_upstream, + 'tag': 'IAS_UPSTREAM' + }, + 'GWS PHY Upstream': { + 'predicate': is_phy_upstream, + 'tag': 'GWS_UPSTREAM', + 'errors': True, + 'parse_func': parse_phy_upstream_name + } + + } + # Provision dashboards, overwriting existing ones. + + datasource_name = datasource.get('name', 'PollerInfluxDB') + for folder_name, dash in dashboards.items(): + folder = find_folder(folder_name) + predicate = dash['predicate'] + tag = dash['tag'] + + # dashboard will include error panel + errors = dash.get('errors') + + # custom parsing function for description to dashboard name + parse_func = dash.get('parse_func') + + logger.info(f'Provisioning {folder_name} dashboards') + + relevant_interfaces = filter(predicate, excluded_interfaces) + data = get_interface_data(relevant_interfaces, parse_func) + dash_data = get_dashboard_data(data, datasource_name, tag, errors) + for dashboard in dash_data: + rendered = render_dashboard(dashboard) + create_dashboard(token_request, rendered, folder['id']) + + # NREN Access dashboards + # uses a different template than the above. + logger.info('Provisioning NREN Access dashboards') + folder = find_folder('NREN Access') + nrens = filter(is_nren, excluded_interfaces) + for dashboard in generate_nrens(nrens, datasource_name): + create_dashboard(token_request, dashboard, folder['id']) + + # Non-generated dashboards + excluded_dashboards = org_config.get('excluded_dashboards', []) + logger.info('Provisioning static dashboards') + for dashboard in get_dashboard_definitions(): + if dashboard['title'] not in excluded_dashboards: + if dashboard['title'].lower() == 'home': + dashboard['uid'] = 'home' + create_dashboard(token_request, dashboard) + + # Home dashboard is always called "Home" + # Make sure it's set for the organization + home_dashboard = find_dashboard(token_request, 'Home') + if home_dashboard: + set_home_dashboard(token_request, home_dashboard['id']) + + delete_api_token(request, org_id, token['id']) + + return all_orgs diff --git a/brian_dashboard_manager/grafana/utils/__init__.py b/brian_dashboard_manager/grafana/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/brian_dashboard_manager/grafana/utils/request.py b/brian_dashboard_manager/grafana/utils/request.py new file mode 100644 index 0000000000000000000000000000000000000000..0d128cb5c6c2577e30e6b2732860ed73642f0190 --- /dev/null +++ b/brian_dashboard_manager/grafana/utils/request.py @@ -0,0 +1,88 @@ +import requests +import json + + +class Request(object): + def __init__(self, url, headers=None): + self.headers = { + 'Accept': 'application/json' + } + if headers: + self.headers.update(headers) + + self.BASE_URL = url + + def get(self, endpoint: str, headers=None, **kwargs): + + r = requests.get( + self.BASE_URL + endpoint, + headers={**headers, **self.headers} if headers else self.headers, + **kwargs + ) + r.raise_for_status() + try: + return r.json() + except json.JSONDecodeError: + return None + + def post(self, endpoint: str, headers=None, **kwargs): + + r = requests.post( + self.BASE_URL + endpoint, + headers={**headers, **self.headers} if headers else self.headers, + **kwargs + ) + r.raise_for_status() + try: + return r.json() + except json.JSONDecodeError: + return None + + def put(self, endpoint: str, headers=None, **kwargs): + + r = requests.put( + self.BASE_URL + endpoint, + headers={**headers, **self.headers} if headers else self.headers, + **kwargs + ) + r.raise_for_status() + try: + return r.json() + except json.JSONDecodeError: + return None + + def delete(self, endpoint: str, headers=None, **kwargs): + + r = requests.delete( + self.BASE_URL + endpoint, + headers={**headers, **self.headers} if headers else self.headers, + **kwargs + ) + r.raise_for_status() + try: + return r.json() + except json.JSONDecodeError: + return None + + +class AdminRequest(Request): + def __init__(self, hostname, admin_username, admin_password, + **kwargs): + self.username = admin_username + url = f'{admin_username}:{admin_password}@{hostname}/' + super().__init__('http://' + url) + + def __str__(self): + return f'admin user: {self.username}' + + +class TokenRequest(Request): + def __init__(self, hostname, token: str, **kwargs): + self.token = token + + super().__init__(f'http://{hostname}/', { + 'Authorization': 'Bearer ' + token + }) + + def __str__(self): + return f'token {self.token}' diff --git a/brian_dashboard_manager/inventory_provider/__init__.py b/brian_dashboard_manager/inventory_provider/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/brian_dashboard_manager/inventory_provider/interfaces.py b/brian_dashboard_manager/inventory_provider/interfaces.py new file mode 100644 index 0000000000000000000000000000000000000000..8368d0c02f29d08cc0c4c6e21cba1aed318b4659 --- /dev/null +++ b/brian_dashboard_manager/inventory_provider/interfaces.py @@ -0,0 +1,10 @@ +import requests +import logging + +logger = logging.getLogger(__name__) + + +def get_interfaces(host): + r = requests.get(f'http://{host}/poller/interfaces') + r.raise_for_status() + return r.json() diff --git a/brian_dashboard_manager/logging_default_config.json b/brian_dashboard_manager/logging_default_config.json new file mode 100644 index 0000000000000000000000000000000000000000..c100d56cad34389f015313d906c69f1a85c6417e --- /dev/null +++ b/brian_dashboard_manager/logging_default_config.json @@ -0,0 +1,59 @@ +{ + "version": 1, + "disable_existing_loggers": false, + "formatters": { + "simple": { + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + } + }, + + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout" + }, + + "syslog_handler": { + "class": "logging.handlers.SysLogHandler", + "level": "DEBUG", + "address": "/dev/log", + "facility": "user", + "formatter": "simple" + }, + + "info_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "simple", + "filename": "info.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + }, + + "error_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "ERROR", + "formatter": "simple", + "filename": "errors.log", + "maxBytes": 10485760, + "backupCount": 20, + "encoding": "utf8" + } + }, + + "loggers": { + "api": { + "level": "DEBUG", + "handlers": ["console", "syslog_handler"], + "propagate": false + } + }, + + "root": { + "level": "INFO", + "handlers": ["console", "syslog_handler"] + } +} \ No newline at end of file diff --git a/brian_dashboard_manager/routes/__init__.py b/brian_dashboard_manager/routes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/brian_dashboard_manager/routes/common.py b/brian_dashboard_manager/routes/common.py new file mode 100644 index 0000000000000000000000000000000000000000..b1a2720ff96d6cd9383a121ea860bac78c919d81 --- /dev/null +++ b/brian_dashboard_manager/routes/common.py @@ -0,0 +1,51 @@ +""" +Utilities used by multiple route blueprints. +""" +import functools +import logging +from flask import request, Response + +logger = logging.getLogger(__name__) + + +def require_accepts_json(f): + """ + used as a route handler decorator to return an error + unless the request allows responses with type "application/json" + :param f: the function to be decorated + :return: the decorated function + """ + @functools.wraps(f) + def decorated_function(*args, **kwargs): + # TODO: use best_match to disallow */* ...? + if not request.accept_mimetypes.accept_json: + return Response( + response="response will be json", + status=406, + mimetype="text/html") + return f(*args, **kwargs) + return decorated_function + + +def after_request(response): + """ + Generic function to do additional logging of requests & responses. + + :param response: + :return: + """ + if response.status_code != 200: + + try: + data = response.data.decode('utf-8') + except Exception: + # never expected to happen, but we don't want any failures here + logging.exception('INTERNAL DECODING ERROR') + data = 'decoding error (see logs)' + + logger.warning( + f'[{response.status_code}] {request.method} {request.path} {data}') + else: + logger.info( + f'[{response.status_code}] {request.method} {request.path}') + return response diff --git a/brian_dashboard_manager/routes/update.py b/brian_dashboard_manager/routes/update.py new file mode 100644 index 0000000000000000000000000000000000000000..eb7c2d5b935813c505c42b4d03688d320d5bb3b6 --- /dev/null +++ b/brian_dashboard_manager/routes/update.py @@ -0,0 +1,17 @@ +from flask import Blueprint, current_app +from brian_dashboard_manager.routes import common +from brian_dashboard_manager.grafana.provision import provision +from brian_dashboard_manager import CONFIG_KEY + +routes = Blueprint("update", __name__) + + +@routes.after_request +def after_request(resp): + return common.after_request(resp) + + +@routes.route('/', methods=['GET']) +def update(): + success = provision(current_app.config[CONFIG_KEY]) + return {'data': success} diff --git a/brian_dashboard_manager/templating/__init__.py b/brian_dashboard_manager/templating/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/brian_dashboard_manager/templating/helpers.py b/brian_dashboard_manager/templating/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..f808f8be95c4d177f3b152ade487ae8800935f55 --- /dev/null +++ b/brian_dashboard_manager/templating/helpers.py @@ -0,0 +1,242 @@ +import re +from itertools import product +from string import ascii_uppercase +from brian_dashboard_manager.templating.render import create_panel + +PANEL_HEIGHT = 12 +PANEL_WIDTH = 24 + + +def get_description(interface): + return interface.get('description', '').strip() + + +def is_physical_interface(interface): + return re.match('^PHY', get_description(interface)) + + +def is_aggregate_interface(interface): + return re.match('^LAG', get_description(interface)) + + +def is_logical_interface(interface): + return re.match('^SRV_', get_description(interface)) + + +def is_nren(interface): + regex = '(PHY|LAG|(SRV_(GLOBAL|LHCONE|MDVPN|IAS|CLS|L3VPN))) CUSTOMER' + return re.match(regex, get_description(interface)) + + +def is_cls(interface): + return 'SRV_CLS' in get_description(interface) + + +def is_ias_public(interface): + return 'SRV_IAS PUBLIC' in get_description(interface) + + +def is_ias_private(interface): + return 'SRV_IAS PRIVATE' in get_description(interface) + + +def is_ias_customer(interface): + return 'SRV_IAS CUSTOMER' in get_description(interface) + + +def is_ias_upstream(interface): + return 'SRV_IAS UPSTREAM' in get_description(interface) + + +def is_re_peer(interface): + return 'SRV_GLOBAL RE_INTERCONNECT' in get_description(interface) + + +def is_re_customer(interface): + regex = '(PHY|LAG|SRV_GLOBAL) CUSTOMER' + return re.match(regex, get_description(interface)) + + +def is_gcs(interface): + return re.match('^SRV_GCS', get_description(interface)) + + +def is_geantopen(interface): + return 'GEANTOPEN' in get_description(interface) + + +def is_l2circuit(interface): + return 'SRV_L2CIRCUIT' in get_description(interface) + + +def is_lhcone_peer(interface): + description = get_description(interface) + return 'LHCONE' in description and 'SRV_L3VPN RE' in description + + +def is_lhcone_customer(interface): + description = get_description(interface) + return 'LHCONE' in description and 'SRV_L3VPN CUSTOMER' in description + + +def is_mdvpn(interface): + return re.match('^SRV_MDVPN CUSTOMER', get_description(interface)) + + +def is_infrastructure_backbone(interface): + regex = '(SRV_GLOBAL|LAG|PHY) INFRASTRUCTURE BACKBONE' + return re.match(regex, get_description(interface)) + + +def is_lag_backbone(interface): + is_lag = 'LAG' in get_description(interface) + return is_infrastructure_backbone(interface) and is_lag + + +def parse_backbone_name(description, *args, **kwargs): + link = description.split('|')[1].strip() + link = link.replace('( ', '(') + return link + + +def is_phy_upstream(interface): + return re.match('^PHY UPSTREAM', get_description(interface)) + + +def parse_phy_upstream_name(description, host): + name = description.split(' ')[2].strip().upper() + location = host.split('.')[1].upper() + return f'{name} - {location}' + + +def num_generator(start=1): + num = start + while True: + yield num + num += 1 + + +def gridPos_generator(id_generator, start=0): + num = start + while True: + yield { + "height": PANEL_HEIGHT, + "width": PANEL_WIDTH, + "x": 0, + "y": num * PANEL_HEIGHT, + "id": next(id_generator) + } + num += 1 + + +def letter_generator(): + i = 0 + j = 0 + num_letters = len(ascii_uppercase) + while True: + result = ascii_uppercase[i % num_letters] + + # tack on an extra letter if we are out of them + if (i >= num_letters): + result += ascii_uppercase[j % num_letters] + j += 1 + if (j != 0 and j % num_letters == 0): + i += 1 + else: + i += 1 + + yield result + + +# peer_predicate is a function that is used to filter the interfaces +# parse_func receives interface information and returns a peer name. +def get_interface_data(interfaces, name_parse_func=None): + result = {} + for interface in interfaces: + if not name_parse_func: + # Most (but not all) descriptions use a format + # which has the peer name as the third element. + def name_parse_func(desc, *args, **kwargs): + return desc.split(' ')[2].upper() + + description = interface.get('description', '').strip() + interface_name = interface.get('name') + host = interface.get('router', '') + + dashboard_name = name_parse_func(description, host) + + peer = result.get(dashboard_name, []) + + router = host.replace('.geant.net', '') + panel_title = f"{router} - {{}} - {interface_name} - {description}" + + peer.append({ + 'title': panel_title, + 'interface': interface_name, + 'hostname': host + }) + result[dashboard_name] = peer + return result + + +# Helper used for generating all traffic/error panels +# with a single target field (ingress/egress or err/s) +def get_panel_fields(panel, panel_type, datasource): + letters = letter_generator() + + def get_target_data(alias, field): + return { + **panel, # panel has target hostname and interface + 'alias': alias, + 'refId': next(letters), + 'select_field': field, + 'percentile': 'percentile' in alias.lower(), + } + + error_fields = [('Ingress Errors', 'errorsIn'), + ('Egress Errors', 'errorsOut')] + + ingress = ['Ingress Traffic', 'Ingress 95th Percentile'] + egress = ['Egress Traffic', 'Egress 95th Percentile'] + + is_v6 = panel_type == 'IPv6' + is_error = panel_type == 'errors' + in_field = 'ingressv6' if is_v6 else 'ingress' + out_field = 'egressv6' if is_v6 else 'egress' + + fields = [*product(ingress, [in_field]), *product(egress, [out_field])] + + targets = error_fields if is_error else fields + + return create_panel({ + **panel, + 'datasource': datasource, + 'title': panel['title'].format(panel_type), + 'panel_targets': [get_target_data(*target) for target in targets], + 'y_axis_type': 'errors' if is_error else 'bits', + }) + + +def get_dashboard_data(data, datasource, tag, errors=False): + id_gen = num_generator() + gridPos = gridPos_generator(id_gen) + + def get_panel_definitions(panels, datasource): + result = [] + for panel in panels: + result.append(get_panel_fields( + {**panel, **next(gridPos)}, 'traffic', datasource)) + result.append(get_panel_fields( + {**panel, **next(gridPos)}, 'IPv6', datasource)) + if errors: + result.append(get_panel_fields( + {**panel, **next(gridPos)}, 'errors', datasource)) + return result + + for peer, panels in data.items(): + yield { + 'title': peer, + 'datasource': datasource, + 'panels': get_panel_definitions(panels, datasource), + 'tag': tag + } diff --git a/brian_dashboard_manager/templating/nren_access.py b/brian_dashboard_manager/templating/nren_access.py new file mode 100644 index 0000000000000000000000000000000000000000..4430515addc4c0b3ccb79da89a86038797e69da5 --- /dev/null +++ b/brian_dashboard_manager/templating/nren_access.py @@ -0,0 +1,155 @@ +import json +import os +import jinja2 +from brian_dashboard_manager.templating.render import create_dropdown_panel, \ + create_panel_target +from brian_dashboard_manager.templating.helpers import \ + is_aggregate_interface, is_logical_interface, is_physical_interface, \ + num_generator, gridPos_generator, letter_generator, get_panel_fields + + +def get_nrens(interfaces): + result = {} + for interface in interfaces: + + description = interface.get('description', '').strip() + + nren_name = description.split(' ')[2].upper() + + nren = result.get( + nren_name, {'AGGREGATES': [], 'SERVICES': [], 'PHYSICAL': []}) + + interface_name = interface.get('name') + host = interface.get('router', '') + router = host.replace('.geant.net', '') + panel_title = f"{router} - {{}} - {interface_name} - {description}" + + if is_aggregate_interface(interface): + nren['AGGREGATES'].append({ + 'interface': interface_name, + 'hostname': host, + 'alias': f"{host.split('.')[1].upper()} - {nren_name}" + }) + + # link aggregates are also shown + # under the physical dropdown + nren['PHYSICAL'].append({ + 'title': panel_title, + 'hostname': host, + 'interface': interface_name + }) + + elif is_logical_interface(interface): + nren['SERVICES'].append({ + 'title': panel_title, + 'hostname': host, + 'interface': interface_name + }) + elif is_physical_interface(interface): + nren['PHYSICAL'].append({ + 'title': panel_title, + 'hostname': host, + 'interface': interface_name + }) + + result[nren_name] = nren + return result + + +# start IDs from 3 since aggregate +# panels have hardcoded IDs (1, 2). +id_gen = num_generator(start=3) + +# aggregate panels have y=0, start generating at 1*height +gridPos = gridPos_generator(id_gen, start=1) + + +# Aggregate panels have unique targets, +# handle those here. +def get_aggregate_targets(aggregates): + ingress = [] + egress = [] + + # used to generate refIds + letters = letter_generator() + + for target in aggregates: + ref_id = next(letters) + in_data = { + **target, + 'alias': f"{target['alias']} - Ingress Traffic", + 'refId': ref_id, + 'select_field': 'ingress' + } + out_data = { + **target, + 'alias': f"{target['alias']} - Egress Traffic", + 'refId': ref_id, + 'select_field': 'egress' + } + ingress_target = create_panel_target(in_data) + egress_target = create_panel_target(out_data) + ingress.append(ingress_target) + egress.append(egress_target) + + return ingress, egress + + +def get_panel_definitions(panels, datasource, errors=False): + result = [] + for panel in panels: + result.append(get_panel_fields( + {**panel, **next(gridPos)}, 'traffic', datasource)) + result.append(get_panel_fields( + {**panel, **next(gridPos)}, 'IPv6', datasource)) + if errors: + result.append(get_panel_fields( + {**panel, **next(gridPos)}, 'errors', datasource)) + return result + + +def get_dashboard_data(interfaces, datasource): + + nren_data = get_nrens(interfaces) + for nren, data in nren_data.items(): + + agg_ingress, agg_egress = get_aggregate_targets(data['AGGREGATES']) + services_dropdown = create_dropdown_panel('Services', **next(gridPos)) + service_panels = get_panel_definitions(data['SERVICES'], datasource) + iface_dropdown = create_dropdown_panel('Interfaces', **next(gridPos)) + phys_panels = get_panel_definitions(data['PHYSICAL'], datasource, True) + + yield { + 'nren_name': nren, + 'datasource': datasource, + 'ingress_aggregate_targets': agg_ingress, + 'egress_aggregate_targets': agg_egress, + 'dropdown_groups': [ + { + 'dropdown': services_dropdown, + 'panels': service_panels, + }, + { + 'dropdown': iface_dropdown, + 'panels': phys_panels, + } + ] + } + + +def generate_nrens(interfaces, datasource): + file = os.path.abspath(os.path.join( + os.path.dirname(__file__), + 'templates', + 'nren_access', + 'nren-dashboard.json.j2')) + + with open(file) as f: + template = jinja2.Template(f.read()) + + for dashboard in get_dashboard_data(interfaces, datasource): + rendered = template.render(dashboard) + rendered = json.loads(rendered) + rendered['uid'] = None + rendered['id'] = None + yield rendered diff --git a/brian_dashboard_manager/templating/render.py b/brian_dashboard_manager/templating/render.py new file mode 100644 index 0000000000000000000000000000000000000000..cf263decc3f2facc483164588158a874ebd52eda --- /dev/null +++ b/brian_dashboard_manager/templating/render.py @@ -0,0 +1,68 @@ +import os +import json +import jinja2 + + +def create_dropdown_panel(title, **kwargs): + TEMPLATE_FILENAME = os.path.abspath(os.path.join( + os.path.dirname(__file__), + 'templates', + 'shared', + 'dropdown.json.j2')) + with open(TEMPLATE_FILENAME) as f: + template = jinja2.Template(f.read()) + return template.render({**kwargs, 'title': title}) + + +# wrapper around bits/s and err/s panel labels +def create_yaxes(type): + file = os.path.abspath(os.path.join( + os.path.dirname(__file__), + 'templates', + 'shared', + 'yaxes.json.j2')) + with open(file) as f: + template = jinja2.Template(f.read()) + return template.render({'type': type}) + + +def create_panel_target(data): + file = os.path.abspath(os.path.join( + os.path.dirname(__file__), + 'templates', + 'shared', + 'panel_target.json.j2')) + with open(file) as f: + template = jinja2.Template(f.read()) + return template.render(data) + + +def create_panel(data): + file = os.path.abspath(os.path.join( + os.path.dirname(__file__), + 'templates', + 'shared', + 'panel.json.j2')) + with open(file) as f: + template = jinja2.Template(f.read()) + yaxes = create_yaxes(data.get('y_axis_type', 'bits')) + targets = [] + for target in data.get('panel_targets', []): + targets.append(create_panel_target(target)) + return template.render({**data, 'yaxes': yaxes, 'targets': targets}) + + +def render_dashboard(dashboard): + file = os.path.abspath(os.path.join( + os.path.dirname(__file__), + 'templates', + 'shared', + 'dashboard.json.j2')) + + with open(file) as f: + template = jinja2.Template(f.read()) + rendered = template.render(dashboard) + rendered = json.loads(rendered) + rendered['uid'] = None + rendered['id'] = None + return rendered diff --git a/brian_dashboard_manager/templating/templates/nren_access/nren-dashboard.json.j2 b/brian_dashboard_manager/templating/templates/nren_access/nren-dashboard.json.j2 new file mode 100644 index 0000000000000000000000000000000000000000..bca5b473718ce97f5aa71b771cb6a1c374ad2c59 --- /dev/null +++ b/brian_dashboard_manager/templating/templates/nren_access/nren-dashboard.json.j2 @@ -0,0 +1,237 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "gnetId": null, + "graphTooltip": 0, + "schemaVersion": 27, + "style": "dark", + "tags": ["customers"], + "templating": { + "list": [] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "{{ nren_name }}", + "version": 1, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "collapsed": null, + "dashLength": 10, + "dashes": false, + "datasource": "{{ datasource }}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 10, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 0 + }, + "hiddenSeries": false, + "id": 1, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "search": null, + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "tags": null, + "targets": [ + {% for target in ingress_aggregate_targets %} + {{ target }}{{ "," if not loop.last }} + {% endfor %} + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Aggregate - {{ nren_name }} - ingress", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": null + }, + "yaxes": [ + { + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": "", + "show": true + }, + { + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": "", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "collapsed": null, + "dashLength": 10, + "dashes": false, + "datasource": "{{ datasource }}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 10, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 0 + }, + "hiddenSeries": false, + "id": 2, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "search": null, + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "tags": null, + "targets": [ + {% for target in egress_aggregate_targets %} + {{ target }}{{ "," if not loop.last }} + {% endfor %} + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Aggregate - {{ nren_name }} - egress", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": null + }, + "yaxes": [ + { + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": "", + "show": true + }, + { + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": "", + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + {% for group in dropdown_groups %} + {{ group.dropdown }} + {% if group.panels|length > 0 %} + , + {% endif %} + {% for panel in group.panels %} + {{ panel }}{{ "," if not loop.last }} + {% endfor %} + {{ "," if not loop.last }} + {% endfor %} + ] +} \ No newline at end of file diff --git a/brian_dashboard_manager/templating/templates/shared/dashboard.json.j2 b/brian_dashboard_manager/templating/templates/shared/dashboard.json.j2 new file mode 100644 index 0000000000000000000000000000000000000000..3201b6fc060a005e093810a2336acd90eabb340f --- /dev/null +++ b/brian_dashboard_manager/templating/templates/shared/dashboard.json.j2 @@ -0,0 +1,38 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": false, + "gnetId": null, + "graphTooltip": 0, + "schemaVersion": 27, + "style": "dark", + "tags": ["{{ tag }}"], + "templating": { + "list": [] + }, + "time": { + "from": "now-24h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "{{ title }}", + "version": 1, + "links": [], + "panels": [ + {% for panel in panels %} + {{ panel }}{{ "," if not loop.last }} + {% endfor %} + ] +} \ No newline at end of file diff --git a/brian_dashboard_manager/templating/templates/shared/dropdown.json.j2 b/brian_dashboard_manager/templating/templates/shared/dropdown.json.j2 new file mode 100644 index 0000000000000000000000000000000000000000..ec3c1b8e7b17644c3a4caa60b6d3a95f9af48ea1 --- /dev/null +++ b/brian_dashboard_manager/templating/templates/shared/dropdown.json.j2 @@ -0,0 +1,26 @@ +{ + "aliasColors": {}, + "collapsed": false, + "datasource": null, + "fill": null, + "fillGradient": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": {{ y }} + }, + "id": {{ id }}, + "legend": null, + "lines": null, + "linewidth": null, + "search": null, + "stack": null, + "tags": null, + "targets": null, + "title": "{{ title }}", + "type": "row", + "xaxis": null, + "yaxes": null, + "yaxis": null +} \ No newline at end of file diff --git a/brian_dashboard_manager/templating/templates/shared/panel.json.j2 b/brian_dashboard_manager/templating/templates/shared/panel.json.j2 new file mode 100644 index 0000000000000000000000000000000000000000..f3e2389ffbf47896e1bb04afa3a3d5c806964098 --- /dev/null +++ b/brian_dashboard_manager/templating/templates/shared/panel.json.j2 @@ -0,0 +1,81 @@ +{ + "aliasColors": {}, + "bars": false, + "collapsed": null, + "dashLength": 10, + "dashes": false, + "datasource": "{{ datasource }}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 5, + "gridPos": { + "h": {{ height }}, + "w": {{ width }}, + "x": 0, + "y": {{ y }} + }, + "hiddenSeries": false, + "id": {{ id }}, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": null, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "search": null, + "seriesOverrides": [], + "spaceLength": 10, + "stack": null, + "steppedLine": false, + "tags": null, + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "{{ title }}", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": null + }, + "yaxes": [ + {{ yaxes }} + ], + "yaxis": { + "align": false, + "alignLevel": null + }, + "targets": [ + {% for target in targets %} + {{ target }}{{ "," if not loop.last }} + {% endfor %} + ] +} \ No newline at end of file diff --git a/brian_dashboard_manager/templating/templates/shared/panel_target.json.j2 b/brian_dashboard_manager/templating/templates/shared/panel_target.json.j2 new file mode 100644 index 0000000000000000000000000000000000000000..b98bd3c3b5e335c6c7f798ee1ac2ad8eead80143 --- /dev/null +++ b/brian_dashboard_manager/templating/templates/shared/panel_target.json.j2 @@ -0,0 +1,57 @@ +{ + "alias": "{{ alias }}", + "groupBy": [ + {% if not percentile %} + { + "params": ["5m"], + "type": "time" + }, + { + "params": ["linear"], + "type": "fill" + } + {% endif %} + ], + "measurement": "interface_rates", + "orderByTime": null, + "policy": null, + "refId": "{{ refId }}", + "resultFormat": "time_series", + "select": [ + [ + { + "params": ["{{ select_field }}"], + "type": "field" + }, + {% if not percentile %} + { + "params": [], + "type": "mean" + }, + {% else %} + { + "params": [95], + "type": "percentile" + }, + {% endif %} + { + "params": ["*8"], + "type": "math" + } + ] + ], + "tags": [ + { + "condition": null, + "key": "hostname", + "operator": "=", + "value": "{{ hostname }}" + }, + { + "condition": "AND", + "key": "interface_name", + "operator": "=", + "value": "{{ interface }}" + } + ] +} \ No newline at end of file diff --git a/brian_dashboard_manager/templating/templates/shared/yaxes.json.j2 b/brian_dashboard_manager/templating/templates/shared/yaxes.json.j2 new file mode 100644 index 0000000000000000000000000000000000000000..aa25fccdcd908e6d885277d952e228eab8f8019d --- /dev/null +++ b/brian_dashboard_manager/templating/templates/shared/yaxes.json.j2 @@ -0,0 +1,35 @@ +{% if type == 'errors' %} +{ + "format": "none", + "label": "err/s", + "logBase": 1, + "max": null, + "min": 0, + "show": true +}, +{ + "format": "none", + "label": "err/s", + "logBase": 1, + "max": null, + "min": 0, + "show": true +} +{% else %} +{ + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": "", + "show": true +}, +{ + "format": "bps", + "label": "bits per second", + "logBase": 1, + "max": null, + "min": "", + "show": true +} +{% endif %} \ No newline at end of file diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000000000000000000000000000000000000..bee869ded4d5eb609bbcda3d6cc0470b0b168235 --- /dev/null +++ b/changelog.md @@ -0,0 +1,7 @@ + +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1] - 2021-02-24 +- initial skeleton diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..f90bc88a6934a897286fbd219cf8157f55ad5f6e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +requests +jsonschema +flask +pytest +pytest-mock +responses +jinja2 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..f27cce609b1e7bcf634d2af82eed1e3d4690100a --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup, find_packages + +setup( + name='brian-dashboard-manager', + version="0.1", + author='GEANT', + author_email='swd@geant.org', + description='', + url=('https://gitlab.geant.net/live-projects/brian-dashboard-manager/'), + packages=find_packages(), + install_requires=[ + 'requests', + 'jsonschema', + 'flask', + 'jinja2' + ], + include_package_data=True, +) diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..148685debfcb07b9863544c69595c9a85819730c --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,64 @@ +import json +import os +import tempfile +import pytest +import brian_dashboard_manager + + +@pytest.fixture +def data_config(): + return { + "admin_username": "fakeadmin", + "admin_password": "fakeadmin", + "hostname": "myfakehostname.org", + "inventory_provider": "inventory-provider01.geant.org:8080", + "organizations": [ + { + "name": "Testorg1", + "excluded_nrens": [], + "excluded_dashboards": [] + }, + { + "name": "GÉANT Testorg2", + "excluded_nrens": [], + "excluded_dashboards": [] + }, + { + "name": "NRENsTestorg3", + "excluded_nrens": [], + "excluded_dashboards": [] + }, + { + "name": "General Public", + "excluded_nrens": ["JISC", "PSNC"], + "excluded_dashboards": [] + } + ], + "datasources": { + "influxdb": { + "name": "PollerInfluxDB", + "type": "influxdb", + "access": "proxy", + "url": "http://prod-poller-ui01.geant.org:8086", + "database": "poller", + "basicAuth": False, + "isDefault": True, + "readOnly": False + } + } + } + + +@pytest.fixture +def data_config_filename(data_config): + with tempfile.NamedTemporaryFile() as f: + f.write(json.dumps(data_config).encode('utf-8')) + f.flush() + yield f.name + + +@pytest.fixture +def client(data_config_filename): + os.environ['CONFIG_FILENAME'] = data_config_filename + with brian_dashboard_manager.create_app().test_client() as c: + yield c diff --git a/test/test_grafana_dashboard.py b/test/test_grafana_dashboard.py new file mode 100644 index 0000000000000000000000000000000000000000..a9f0c9241497f4f930531dca6442a8f2d220d996 --- /dev/null +++ b/test/test_grafana_dashboard.py @@ -0,0 +1,196 @@ + +import json +import responses +from brian_dashboard_manager.grafana import dashboard, provision +from brian_dashboard_manager.grafana.utils.request import TokenRequest + + +@responses.activate +def test_get_dashboard(data_config): + + UID = 1 + + request = TokenRequest(**data_config, token='test') + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + + f'api/dashboards/uid/{UID}', + callback=lambda f: ( + 404, + {}, + '')) + + data = dashboard._get_dashboard(request, UID) + assert data is None + + responses.add_callback(method=responses.GET, + url=request.BASE_URL + + f'api/dashboards/uid/{UID+1}', + callback=lambda f: (200, + {}, + json.dumps({"uid": 1}))) + + data = dashboard._get_dashboard(request, UID + 1) + assert data['uid'] == 1 + + +@responses.activate +def test_delete_dashboards(data_config): + UID = 1 + dashboards = [{'uid': UID}] + + request = TokenRequest(**data_config, token='test') + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + + f'api/dashboards/uid/{UID}', + callback=lambda f: ( + 200, + {}, + json.dumps( + dashboards[0]))) + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + + 'api/search', + callback=lambda f: ( + 200, + {}, + json.dumps(dashboards))) + + def delete_callback(request): + uid = request.path_url.split('/')[-1] + assert int(uid) == UID + return 200, {}, json.dumps({'message': 'Dashboard has been deleted.'}) + + responses.add_callback( + method=responses.DELETE, + url=request.BASE_URL + + f'api/dashboards/uid/{UID}', + callback=delete_callback) + + data = dashboard.delete_dashboards(request) + assert data is True + + responses.add_callback( + method=responses.DELETE, + url=request.BASE_URL + + f'api/dashboards/uid/{UID+1}', + callback=lambda f: ( + 400, + {}, + '')) + + data = dashboard._delete_dashboard(request, UID + 1) + assert data is None + + +@responses.activate +def test_search_dashboard(data_config): + UID = 1 + TITLE = 'testdashboard' + dashboards = [{'uid': UID, 'title': TITLE}] + + request = TokenRequest(**data_config, token='test') + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + + 'api/search', + callback=lambda f: ( + 200, + {}, + json.dumps(dashboards))) + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + + f'api/dashboards/uid/{UID}', + callback=lambda f: ( + 200, + {}, + json.dumps( + dashboards[0]))) + + data = dashboard._search_dashboard( + request, {'title': dashboards[0]['title']}) + assert data['uid'] == UID + + data = dashboard._search_dashboard(request, {'title': 'DoesNotExist'}) + assert data is None + + +@responses.activate +def test_search_dashboard_error(data_config): + request = TokenRequest(**data_config, token='test') + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + 'api/search', callback=lambda f: (400, {}, '')) + + data = dashboard._search_dashboard(request, {'title': 'DoesNotExist'}) + assert data is None + + +@responses.activate +def test_create_dashboard(data_config): + UID = 1 + ID = 1 + VERSION = 1 + TITLE = 'testdashboard' + dashboard = {'id': ID, 'uid': UID, 'title': TITLE, 'version': VERSION} + request = TokenRequest(**data_config, token='test') + + def get_callback(request): + return 200, {}, json.dumps({'dashboard': dashboard}) + + responses.add_callback(method=responses.GET, + url=request.BASE_URL + f'api/dashboards/uid/{UID}', + callback=get_callback) + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + 'api/search', callback=lambda f: (400, {}, '')) + + def post_callback(request): + body = json.loads(request.body) + return 200, {}, json.dumps(body['dashboard']) + + responses.add_callback( + method=responses.POST, + url=request.BASE_URL + 'api/dashboards/db', callback=post_callback) + + data = provision.create_dashboard(request, dashboard) + assert data == dashboard + + +@responses.activate +def test_create_dashboard_no_uid_error(data_config): + ID = 1 + VERSION = 1 + TITLE = 'testdashboard' + dashboard = {'id': ID, 'title': TITLE, 'version': VERSION} + request = TokenRequest(**data_config, token='test') + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + 'api/search', callback=lambda f: (400, {}, '')) + + def post_callback(request): + body = json.loads(request.body) + # if a dashboard doesn't have an UID, the ID should not be sent to + # grafana. + assert 'id' not in body['dashboard'] + + # have already tested a successful response, respond with error here. + return 400, {}, '' + + responses.add_callback( + method=responses.POST, + url=request.BASE_URL + 'api/dashboards/db', callback=post_callback) + + data = provision.create_dashboard(request, dashboard) + assert data is None diff --git a/test/test_grafana_datasource.py b/test/test_grafana_datasource.py new file mode 100644 index 0000000000000000000000000000000000000000..488b7023c3a577082c5c37da0b7698e28de186b6 --- /dev/null +++ b/test/test_grafana_datasource.py @@ -0,0 +1,142 @@ +import json +import responses +from brian_dashboard_manager.grafana import datasource, provision +from brian_dashboard_manager.grafana.utils.request import AdminRequest + + +@responses.activate +def test_get_datasources(data_config): + + BODY = [] + + request = AdminRequest(**data_config) + + responses.add( + method=responses.GET, + url=request.BASE_URL + 'api/datasources', json=BODY) + + data = datasource.get_datasources(request) + assert data == BODY + + +@responses.activate +def test_get_missing_datasource_definitions(data_config): + # this only retrieves data from the filesystem and checks against + # what's configured in grafana.. just make sure + # we cover the part for fetching datasources + + request = AdminRequest(**data_config) + + responses.add(method=responses.GET, url=request.BASE_URL + + 'api/datasources') + + dir = '/tmp/dirthatreallyshouldnotexistsousealonganduniquestring' + # it returns a generator, so iterate :) + for data in datasource.get_missing_datasource_definitions(request, dir): + pass + + +def test_datasource_provisioned(): + val = datasource._datasource_provisioned({}, []) + assert val + + val = datasource._datasource_provisioned({'id': 1}, []) + assert val is False + + val = datasource._datasource_provisioned({'id': 1, "name": 'testcase2'}, + [{'id': -1, 'name': 'testcase1'}, + {'id': 1, 'name': 'testcase1'}]) + assert val is False + + val = datasource._datasource_provisioned({'id': 1}, + [{'id': -1, 'name': 'testcase1'}, + {'id': 1, 'name': 'testcase2'}]) + assert val + + val = datasource._datasource_provisioned({'id': 2, "name": 'testcase2'}, + [{'id': -1, 'name': 'testcase1'}, + {'id': 1, 'name': 'testcase1'}, + {'id': 2, 'name': 'testcase2'}]) + assert val + + +@responses.activate +def test_create_prod_datasource(data_config): + ORG_ID = 1 + + BODY = { + "name": "brian-influx-datasource", + "type": "influxdb", + "access": "proxy", + "url": "http://test-brian-datasource.geant.org:8086", + "database": "test-db", + "basicAuth": False, + "isDefault": True, + "readOnly": False + } + + request = AdminRequest(**data_config) + + def post_callback(request): + body = json.loads(request.body) + result = { + 'datasource': { + 'id': 1, + 'orgId': ORG_ID, + 'type': 'graphite', + 'typeLogoUrl': '', + 'password': '', + 'user': '', + 'basicAuthUser': '', + 'basicAuthPassword': '', + 'withCredentials': False, + 'jsonData': {}, + 'secureJsonFields': {}, + 'version': 1 + }, + 'id': 1, + 'message': 'Datasource added', + 'name': body['name'] + } + result['datasource'].update(body) + return 200, {}, json.dumps(result) + + responses.add_callback( + method=responses.POST, + url=request.BASE_URL + 'api/datasources', + callback=post_callback) + + data = provision.create_datasource( + request, BODY, datasources=data_config['datasources']) + + datasource_type = data['datasource']['type'] + datasource_config_url = data_config['datasources'][datasource_type]['url'] + assert data['datasource']['url'] == datasource_config_url + + +@responses.activate +def test_create_prod_datasource_fails(data_config): + BODY = { + "name": "brian-influx-datasource", + "type": "influxdb", + "access": "proxy", + "url": "http://test-brian-datasource.geant.org:8086", + "database": "test-db", + "basicAuth": False, + "isDefault": True, + "readOnly": False + } + + request = AdminRequest(**data_config) + + responses.add_callback( + method=responses.POST, + url=request.BASE_URL + 'api/datasources', + callback=lambda f: (400, {}, '')) + + data = provision.create_datasource( + request, BODY, datasources=data_config['datasources']) + + # if an error occured when provisioning a datasource, we log the response + # but return None + assert data is None diff --git a/test/test_grafana_organization.py b/test/test_grafana_organization.py new file mode 100644 index 0000000000000000000000000000000000000000..4528594eec9c679d1e43193a0124144b8593f83d --- /dev/null +++ b/test/test_grafana_organization.py @@ -0,0 +1,111 @@ +import json +import responses +from datetime import datetime, timedelta +from brian_dashboard_manager.grafana import provision +from brian_dashboard_manager.grafana.utils.request import AdminRequest + + +@responses.activate +def test_get_organizations(data_config): + request = AdminRequest(**data_config) + + responses.add(method=responses.GET, + url=request.BASE_URL + 'api/orgs', + json=[{'id': 91, + 'name': 'Testorg1'}, + {'id': 92, + 'name': 'GÉANT Testorg2'}, + {'id': 93, + 'name': 'NRENsTestorg3'}, + {'id': 94, + 'name': 'General Public'}]) + + data = provision.get_organizations(request) + assert data is not None + + +@responses.activate +def test_create_organization(data_config): + ORG_NAME = 'fakeorg123' + + def post_callback(request): + body = json.loads(request.body) + assert body['name'] == ORG_NAME + return 200, {}, json.dumps( + {'orgId': 1, 'message': 'Organization created'}) + + request = AdminRequest(**data_config) + + responses.add_callback( + method=responses.POST, + url=request.BASE_URL + 'api/orgs', + callback=post_callback) + + data = provision.create_organization(request, ORG_NAME) + assert data is not None + + +@responses.activate +def test_delete_expired_api_tokens(data_config): + ORG_ID = 1 + KEY_ID = 1 + + def post_callback(request): + assert request.params['includeExpired'] == 'True' + time = (datetime.now() - timedelta(seconds=60) + ).strftime('%Y-%m-%dT%H:%M:%SZ') + return 200, {}, json.dumps([{'expiration': time, 'id': KEY_ID}]) + + request = AdminRequest(**data_config) + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + 'api/auth/keys', + callback=post_callback) + + responses.add( + method=responses.POST, + url=request.BASE_URL + + f'api/user/using/{ORG_ID}', + json={ + "message": "Active organization changed"}) + responses.add( + method=responses.DELETE, + url=request.BASE_URL + + f'api/auth/keys/{KEY_ID}', + json={ + "message": "API key deleted"}) + + provision.delete_expired_api_tokens(request, ORG_ID) + + +@responses.activate +def test_create_api_token(data_config): + ORG_ID = 1 + TOKEN_ID = 1 + BODY = { + 'name': 'test-token', + 'role': 'Admin', + 'secondsToLive': 3600 + } + + request = AdminRequest(**data_config) + + def post_callback(request): + body = json.loads(request.body) + assert body == BODY + return 200, {}, json.dumps({'id': TOKEN_ID}) + + responses.add_callback( + method=responses.POST, + url=request.BASE_URL + 'api/auth/keys', + callback=post_callback) + + responses.add( + method=responses.POST, + url=request.BASE_URL + + f'api/user/using/{ORG_ID}', + json={ + "message": "Active organization changed"}) + + data = provision.create_api_token(request, ORG_ID, BODY) + assert data['id'] == TOKEN_ID diff --git a/test/test_grafana_request.py b/test/test_grafana_request.py new file mode 100644 index 0000000000000000000000000000000000000000..d90b191bf0245b129c10d3c3ceeb198f1c7d7863 --- /dev/null +++ b/test/test_grafana_request.py @@ -0,0 +1,106 @@ +import pytest +import responses +import requests +import json +from brian_dashboard_manager.grafana.utils.request import \ + AdminRequest, \ + TokenRequest + + +@responses.activate +def test_admin_request(data_config): + ENDPOINT = 'test/url/endpoint' + request = AdminRequest(**data_config) + url = '{admin_username}:{admin_password}@{hostname}/'. \ + format(**data_config) + assert request.BASE_URL == 'http://' + url + assert request.username == data_config['admin_username'] + + def get_callback(request): + assert request.path_url[1:] == ENDPOINT + return 200, {}, '' + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + ENDPOINT, + callback=get_callback) + + request.get(ENDPOINT) + + +@responses.activate +def test_token_request(data_config): + TOKEN = '123' + ENDPOINT = 'test/url/endpoint' + request = TokenRequest(**data_config, token=TOKEN) + assert request.BASE_URL == 'http://{hostname}/'.format( + **data_config) + assert request.token == TOKEN + + def get_callback(request): + assert request.path_url[1:] == ENDPOINT + assert TOKEN in request.headers['authorization'] + return 200, {}, '' + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + ENDPOINT, + callback=get_callback) + + request.get(ENDPOINT) + +# document unimplemented handling of server-side errors + + +@pytest.mark.xfail(raises=requests.exceptions.HTTPError) +@responses.activate +def test_POST_fails(data_config): + ORG_NAME = 'fakeorg123' + + def post_callback(request): + body = json.loads(request.body) + assert body['name'] == ORG_NAME + return 500, {}, '' + + request = AdminRequest(**data_config) + + responses.add_callback( + method=responses.POST, + url=request.BASE_URL + 'api/orgs', + callback=post_callback) + + request.post('api/orgs', json={'name': ORG_NAME}) + + +@pytest.mark.xfail(raises=requests.exceptions.HTTPError) +@responses.activate +def test_GET_fails(data_config): + ORG_NAME = 'fakeorg123' + + def get_callback(request): + return 500, {}, '' + + request = AdminRequest(**data_config) + + responses.add_callback( + method=responses.GET, + url=request.BASE_URL + 'api/orgs', + callback=get_callback) + + request.get('api/orgs', json={'name': ORG_NAME}) + + +@pytest.mark.xfail(raises=requests.exceptions.HTTPError) +@responses.activate +def test_DELETE_fails(data_config): + def delete_callback(request): + return 500, {}, '' + + request = AdminRequest(**data_config) + + responses.add_callback( + method=responses.DELETE, + url=request.BASE_URL + 'api/orgs/1', + callback=delete_callback) + + request.delete('api/orgs/1') diff --git a/test/test_update.py b/test/test_update.py new file mode 100644 index 0000000000000000000000000000000000000000..607d02d1b0ee512efef191ef04b4979679459b5b --- /dev/null +++ b/test/test_update.py @@ -0,0 +1,254 @@ +import responses +import json + +DEFAULT_REQUEST_HEADERS = { + "Content-type": "application/json", + "Accept": ["application/json"] +} + + +TEST_INTERFACES = [ + { + "router": "mx1.ath2.gr.geant.net", + "name": "xe-1/0/1", + "bundle": [], + "bundle-parents": [], + "snmp-index": 569, + "description": "PHY RESERVED | New OTEGLOBE ATH2-VIE 10Gb LS", + "circuits": [] + }, + { + "router": "mx1.ath2.gr.geant.net", + "name": "ge-1/3/7", + "bundle": [], + "bundle-parents": [], + "snmp-index": 543, + "description": "PHY SPARE", + "circuits": [] + }, + { + "router": "mx1.ham.de.geant.net", + "name": "xe-2/2/0.13", + "bundle": [], + "bundle-parents": [], + "snmp-index": 721, + "description": "SRV_L2CIRCUIT CUSTOMER WP6T3 WP6T3 #ham_lon2-WP6-GTS_20063 |", # noqa: E501 + "circuits": [ + { + "id": 52382, + "name": "ham_lon2-WP6-GTS_20063_L2c", + "type": "", + "status": "operational" + } + ] + }, + { + "router": "mx1.fra.de.geant.net", + "name": "ae27", + "bundle": [], + "bundle-parents": [ + "xe-10/0/2", + "xe-10/3/2", + "xe-10/3/3" + ], + "snmp-index": 760, + "description": "LAG CUSTOMER ULAKBIM SRF9940983 |", + "circuits": [ + { + "id": 40983, + "name": "ULAKBIM AP2 LAG", + "type": "", + "status": "operational" + } + ] + }, + { + "router": "mx2.zag.hr.geant.net", + "name": "xe-2/1/0", + "bundle": [], + "bundle-parents": [], + "snmp-index": 739, + "description": "PHY SPARE", + "circuits": [] + }, + { + "router": "rt1.rig.lv.geant.net", + "name": "xe-0/1/5", + "bundle": [], + "bundle-parents": [], + "snmp-index": 539, + "description": "PHY SPARE", + "circuits": [] + }, + { + "router": "srx1.ch.office.geant.net", + "name": "ge-0/0/0", + "bundle": [], + "bundle-parents": [], + "snmp-index": 513, + "description": "Reserved for GEANT OC to test Virgin Media link", + "circuits": [] + }, + { + "router": "mx1.par.fr.geant.net", + "name": "xe-4/1/4.1", + "bundle": [], + "bundle-parents": [], + "snmp-index": 1516, + "description": "SRV_L2CIRCUIT INFRASTRUCTURE JRA1 JRA1 | #SDX-L2_PILOT-Br52 OF-P3_par ", # noqa: E501 + "circuits": [] + }, + { + "router": "mx1.lon.uk.geant.net", + "name": "lt-1/3/0.61", + "bundle": [], + "bundle-parents": [], + "snmp-index": 1229, + "description": "SRV_IAS INFRASTRUCTURE ACCESS GLOBAL #LON-IAS-RE-Peering | BGP Peering - IAS Side", # noqa: E501 + "circuits": [] + }, + { + "router": "mx1.sof.bg.geant.net", + "name": "xe-2/0/5", + "bundle": [], + "bundle-parents": [], + "snmp-index": 694, + "description": "PHY RESERVED | Prime Telecom Sofia-Bucharest 3_4", + "circuits": [] + } +] + + +def generate_folder(data): + return { + "id": 555, + "uid": data['uid'], + "title": data['title'], + "url": f"/dashboards/f/{data['uid']}/{data['title'].lower()}", + "hasAcl": False, + "canSave": True, + "canEdit": True, + "canAdmin": True, + "createdBy": "Anonymous", + "created": "2021-02-23T15:33:46Z", + "updatedBy": "Anonymous", + "updated": "2021-02-23T15:33:46Z", + "version": 1 + } + + +@responses.activate +def test_provision(data_config, mocker, client): + + def get_callback(request): + return 200, {}, json.dumps(TEST_INTERFACES) + + responses.add_callback( + method=responses.GET, + url=f"http://{data_config['inventory_provider']}/poller/interfaces", + callback=get_callback) + + def folder_get(request): + return 200, {}, json.dumps([]) + + responses.add_callback( + method=responses.GET, + url=f"http://{data_config['hostname']}/api/folders", + callback=folder_get) + + def folder_post(request): + data = json.loads(request.body) + return 200, {}, json.dumps(generate_folder(data)) + + responses.add_callback( + method=responses.POST, + url=f"http://{data_config['hostname']}/api/folders", + callback=folder_post) + + def home_dashboard(request): + return 200, {}, json.dumps([]) + + responses.add_callback( + method=responses.GET, + url=f"http://{data_config['hostname']}/api/search?query=Home", + callback=home_dashboard) + + TEST_DATASOURCE = [{ + "name": "brian-influx-datasource", + "type": "influxdb", + "access": "proxy", + "url": "http://test-brian-datasource.geant.org:8086", + "database": "test-db", + "basicAuth": False, + "isDefault": True, + "readOnly": False + }] + + def datasources(request): + return 200, {}, json.dumps(TEST_DATASOURCE) + + responses.add_callback( + method=responses.GET, + url=f"http://{data_config['hostname']}/api/datasources", + callback=datasources) + + PROVISIONED_ORGANIZATION = { + 'name': data_config['organizations'][0], + 'id': 0 + } + + EXISTING_ORGS = [{**org, 'id': i + 1} + for i, org in enumerate(data_config['organizations'][1:])] + + _mocked_get_organizations = mocker.patch( + 'brian_dashboard_manager.grafana.provision.get_organizations') + # all organizations are provisioned except the first one. + _mocked_get_organizations.return_value = EXISTING_ORGS.copy() + + _mocked_create_organization = mocker.patch( + 'brian_dashboard_manager.grafana.provision.create_organization') + + # spoof creating first organization + _mocked_create_organization.return_value = PROVISIONED_ORGANIZATION + + _mocked_delete_expired_api_tokens = mocker.patch( + 'brian_dashboard_manager.grafana.provision.delete_expired_api_tokens') + # we dont care about this, , tested separately + _mocked_delete_expired_api_tokens.return_value = None + + _mocked_create_api_token = mocker.patch( + 'brian_dashboard_manager.grafana.provision.create_api_token') + _mocked_create_api_token.return_value = { + 'key': 'testtoken', 'id': 0} # api token + + _mocked_create_datasource = mocker.patch( + 'brian_dashboard_manager.grafana.provision.create_datasource') + # we dont care about this, just mark it created + _mocked_create_datasource.return_value = True + + _mocked_get_dashboard_definitions = mocker.patch( + 'brian_dashboard_manager.grafana.provision.get_dashboard_definitions') + + UID = 1 + ID = 1 + VERSION = 1 + TITLE = 'testdashboard' + dashboard = {'id': ID, 'uid': UID, 'title': TITLE, 'version': VERSION} + _mocked_get_dashboard_definitions.return_value = [ + dashboard # test dashboard + ] + + _mocked_create_dashboard = mocker.patch( + 'brian_dashboard_manager.grafana.provision.create_dashboard') + # we dont care about this, just mark it created + # we dont care about this, tested separately + _mocked_create_dashboard.return_value = None + + _mocked_delete_api_token = mocker.patch( + 'brian_dashboard_manager.grafana.provision.delete_api_token') + # we dont care about this, tested separately + _mocked_delete_api_token.return_value = None + response = client.get('/update/', headers=DEFAULT_REQUEST_HEADERS) + assert response.status_code == 200 + data = json.loads(response.data.decode('utf-8'))['data'] + assert data == EXISTING_ORGS + [PROVISIONED_ORGANIZATION] diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000000000000000000000000000000000..31480e823de253929907310ceab787c6e312ef51 --- /dev/null +++ b/tox.ini @@ -0,0 +1,19 @@ +[tox] +envlist = py36 + +[flake8] +exclude = venv,.tox + +[testenv] +deps = + coverage + flake8 + -r requirements.txt + +commands = + coverage erase + coverage run --source brian_dashboard_manager -m py.test {posargs} + coverage xml + coverage html + coverage report --fail-under 75 + flake8 \ No newline at end of file