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