diff --git a/brian_dashboard_manager/config.py b/brian_dashboard_manager/config.py
index 294e16e2b03b539f4451359f362f0754818053fb..c94da1d81b20df68db32c7992752797a8a7cf332 100644
--- a/brian_dashboard_manager/config.py
+++ b/brian_dashboard_manager/config.py
@@ -77,6 +77,8 @@ DEFAULT_ORGANIZATIONS = [
             "IAS PUBLIC": True,
             "IAS UPSTREAM": True,
             "GWS PHY Upstream": True,
+            "GWS Direct": True,
+            "GWS Indirect": True,
         }
     },
     {
@@ -116,7 +118,9 @@ DEFAULT_ORGANIZATIONS = [
             "IAS PRIVATE": True,
             "IAS PUBLIC": True,
             "IAS UPSTREAM": True,
-            "GWS PHY Upstream": True
+            "GWS PHY Upstream": True,
+            "GWS Direct": True,
+            "GWS Indirect": True,
         }
     }
 ]
diff --git a/brian_dashboard_manager/dashboards/services_ias.json b/brian_dashboard_manager/dashboards/services_ias.json
index d0644528905b1caada0daad603c263c0cbf58f23..5719b432a0a4ad905e566ae1639efc88d7bafb6c 100755
--- a/brian_dashboard_manager/dashboards/services_ias.json
+++ b/brian_dashboard_manager/dashboards/services_ias.json
@@ -46,7 +46,7 @@
       "folderId": null,
       "gridPos": {
         "h": 25,
-        "w": 6,
+        "w": 4,
         "x": 0,
         "y": 0
       },
@@ -114,8 +114,8 @@
       "folderId": null,
       "gridPos": {
         "h": 25,
-        "w": 6,
-        "x": 6,
+        "w": 4,
+        "x": 4,
         "y": 0
       },
       "headings": false,
@@ -182,8 +182,8 @@
       "folderId": null,
       "gridPos": {
         "h": 25,
-        "w": 6,
-        "x": 12,
+        "w": 4,
+        "x": 8,
         "y": 0
       },
       "headings": false,
@@ -250,8 +250,8 @@
       "folderId": null,
       "gridPos": {
         "h": 25,
-        "w": 6,
-        "x": 18,
+        "w": 4,
+        "x": 12,
         "y": 0
       },
       "headings": false,
@@ -306,6 +306,142 @@
       "timeShift": null,
       "title": "IAS PRIVATE",
       "type": "dashlist"
+    },
+    {
+      "datasource": null,
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "folderId": null,
+      "gridPos": {
+        "h": 25,
+        "w": 4,
+        "x": 16,
+        "y": 0
+      },
+      "headings": false,
+      "id": 6,
+      "limit": 100,
+      "pluginVersion": "7.1.4",
+      "query": "",
+      "recent": false,
+      "search": true,
+      "starred": false,
+      "tags": [
+        "GWS_DIRECT"
+      ],
+      "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": "GWS Direct",
+      "type": "dashlist"
+    },
+    {
+      "datasource": null,
+      "fieldConfig": {
+        "defaults": {
+          "custom": {}
+        },
+        "overrides": []
+      },
+      "folderId": null,
+      "gridPos": {
+        "h": 25,
+        "w": 4,
+        "x": 20,
+        "y": 0
+      },
+      "headings": false,
+      "id": 7,
+      "limit": 100,
+      "pluginVersion": "7.1.4",
+      "query": "",
+      "recent": false,
+      "search": true,
+      "starred": false,
+      "tags": [
+        "GWS_INDIRECT"
+      ],
+      "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": "GWS Indirect",
+      "type": "dashlist"
     }
   ],
   "schemaVersion": 26,
@@ -338,3 +474,4 @@
   "title": "IAS",
   "version": 1
 }
+
diff --git a/brian_dashboard_manager/grafana/provision.py b/brian_dashboard_manager/grafana/provision.py
index 54b5bf90391a38e9cf48f87e5610f0bbba0ee793..f04d851b4dbb9016cf3fae1e4c9db1793725f385 100644
--- a/brian_dashboard_manager/grafana/provision.py
+++ b/brian_dashboard_manager/grafana/provision.py
@@ -8,6 +8,7 @@ import time
 import json
 import datetime
 from functools import reduce
+from concurrent.futures import Future
 from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
 from brian_dashboard_manager.config import DEFAULT_ORGANIZATIONS, STATE_PATH
 from brian_dashboard_manager.grafana.utils.request import \
@@ -23,7 +24,7 @@ from brian_dashboard_manager.grafana.datasource import \
 from brian_dashboard_manager.grafana.folder import find_folder, \
     delete_folder
 from brian_dashboard_manager.inventory_provider.interfaces import \
-    get_interfaces
+    get_gws_direct, get_gws_indirect, get_interfaces
 from brian_dashboard_manager.templating.nren_access import generate_nrens
 
 from brian_dashboard_manager.templating.helpers import is_re_customer, \
@@ -34,6 +35,9 @@ from brian_dashboard_manager.templating.helpers import is_re_customer, \
     get_interface_data, parse_backbone_name, parse_phy_upstream_name, \
     get_dashboard_data, get_aggregate_interface_data
 
+from brian_dashboard_manager.templating.gws import generate_gws, \
+    generate_indirect
+
 from brian_dashboard_manager.templating.render import render_dashboard
 
 logger = logging.getLogger(__name__)
@@ -117,6 +121,9 @@ def provision_maybe(config):
             now = datetime.datetime.now()
             write_timestamp(now.timestamp(), val)
             provision(config)
+        except Exception as e:
+            logger.exception('Uncaught Exception:')
+            raise e
         finally:
             now = datetime.datetime.now()
             write_timestamp(now.timestamp(), False)
@@ -180,7 +187,10 @@ def provision(config):
 
         def excluded(interface):
             desc = interface.get('description', '').lower()
-            return not any(nren.lower() in desc for nren in excluded_nrens)
+            lab = 'lab.office' in interface.get('router', '').lower()
+            excluded_desc = any(
+                nren.lower() in desc for nren in excluded_nrens)
+            return not (excluded_desc or lab)
 
         excluded_interfaces = list(filter(excluded, interfaces))
 
@@ -263,6 +273,14 @@ def provision(config):
         dash_list = find_dashboard(token_request) or []
         dash_list = reduce(get_uid, dash_list, {})
 
+        def update_dash_list(dashboards):
+            for dashboard in dashboards:
+                if isinstance(dashboard, Future):
+                    dashboard = dashboard.result()
+                if dashboard is None:
+                    continue
+                dash_list[dashboard.get('uid')] = True
+
         with ProcessPoolExecutor(max_workers=4) as executor:
             provisioned = []
             for folder_name, dash in dashboards.items():
@@ -282,14 +300,55 @@ def provision(config):
                                       excluded_interfaces, datasource_name,
                                       exclude)
                 provisioned.append(res)
+
             for result in provisioned:
                 folder = result.result()
                 if folder is None:
                     continue
-                for dashboard in folder:
-                    if dashboard is None:
-                        continue
-                    dash_list[dashboard.get('uid')] = True
+                update_dash_list(folder)
+
+        # fetch GWS direct data and provision related dashboards
+        logger.info('Provisioning GWS Indirect dashboards')
+        folder_name = 'GWS Indirect'
+        exclude_indirect = excluded_folders.get(folder_name, False)
+        if isinstance(exclude_indirect, bool) and exclude_indirect:
+            # don't provision GWS Direct folder
+            delete_folder(token_request, folder_name)
+        else:
+            folder = find_folder(token_request, folder_name)
+            with ProcessPoolExecutor(max_workers=4) as executor:
+                gws_indirect_data = get_gws_indirect(
+                    config['inventory_provider'])
+                provisioned = []
+                dashes = generate_indirect(gws_indirect_data, datasource_name)
+                for dashboard in dashes:
+                    rendered = render_dashboard(dashboard)
+                    provisioned.append(executor.submit(create_dashboard,
+                                                       token_request,
+                                                       rendered, folder['id']))
+
+                update_dash_list(provisioned)
+
+        # fetch GWS direct data and provision related dashboards
+        logger.info('Provisioning GWS Direct dashboards')
+        folder_name = 'GWS Direct'
+        exclude_gws = excluded_folders.get(folder_name, False)
+        if isinstance(exclude_gws, bool) and exclude_gws:
+            # don't provision GWS Direct folder
+            delete_folder(token_request, folder_name)
+        else:
+            folder = find_folder(token_request, folder_name)
+            with ProcessPoolExecutor(max_workers=4) as executor:
+                gws_data = get_gws_direct(config['inventory_provider'])
+                provisioned = []
+
+                for dashboard in generate_gws(gws_data, datasource_name):
+                    rendered = render_dashboard(dashboard)
+                    provisioned.append(executor.submit(create_dashboard,
+                                                       token_request,
+                                                       rendered, folder['id']))
+
+                update_dash_list(provisioned)
 
         aggregate_dashboards = {
             'CLS PEERS': {
@@ -319,7 +378,6 @@ def provision(config):
         if isinstance(exclude_agg, bool) and exclude_agg:
             # don't provision aggregate folder
             delete_folder(token_request, 'Aggregates')
-            pass
         else:
             with ProcessPoolExecutor(max_workers=4) as executor:
                 provisioned = []
@@ -338,11 +396,7 @@ def provision(config):
                                           excluded_interfaces, datasource_name)
                     provisioned.append(res)
 
-                for result in provisioned:
-                    dashboard = result.result()
-                    if dashboard is None:
-                        continue
-                    dash_list[dashboard.get('uid')] = True
+                update_dash_list(provisioned)
 
         # NREN Access dashboards
         # uses a different template than the above.
diff --git a/brian_dashboard_manager/grafana/utils/request.py b/brian_dashboard_manager/grafana/utils/request.py
index d55dc878a3dc4057e4e612417790a4698e620477..d1c5452340bc94f4b2ad93fd72444996d76e4314 100644
--- a/brian_dashboard_manager/grafana/utils/request.py
+++ b/brian_dashboard_manager/grafana/utils/request.py
@@ -1,4 +1,8 @@
 import requests
+import logging
+from requests.models import HTTPError
+
+logger = logging.getLogger(__name__)
 
 
 class Request(object):
@@ -11,47 +15,34 @@ class Request(object):
 
         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()
+    def do_request(self, method, endpoint, *args, **kwargs):
+        r = requests.request(method, self.BASE_URL + endpoint,
+                             *args,
+                             **kwargs,
+                             headers={
+                                 **kwargs.get('headers', {}),
+                                 **self.headers
+                             })
+
+        try:
+            r.raise_for_status()
+        except HTTPError as e:
+            if e.response.status_code < 500:
+                logger.error(e.response.content.decode('utf-8'))
+            raise e
         return r.json()
 
-    def post(self, endpoint: str, headers=None, **kwargs):
+    def get(self, endpoint: str, *args, **kwargs):
+        return self.do_request('get', endpoint, *args, **kwargs)
 
-        r = requests.post(
-            self.BASE_URL + endpoint,
-            headers={**headers, **self.headers} if headers else self.headers,
-            **kwargs
-        )
-        r.raise_for_status()
+    def post(self, endpoint: str, data=None, **kwargs):
+        return self.do_request('post', endpoint, data=data, **kwargs)
 
-        return r.json()
+    def put(self, endpoint: str, **kwargs):
+        return self.do_request('put', endpoint, **kwargs)
 
-    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()
-        return r.json()
-
-    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()
-
-        return r.json()
+    def delete(self, endpoint: str, **kwargs):
+        return self.do_request('delete', endpoint, **kwargs)
 
 
 class AdminRequest(Request):
diff --git a/brian_dashboard_manager/inventory_provider/interfaces.py b/brian_dashboard_manager/inventory_provider/interfaces.py
index a27ceecfaee82f49d18c984e012665ac9ff6050e..400db47e3f484f4d4b1a712b83f29b46a5bcb524 100644
--- a/brian_dashboard_manager/inventory_provider/interfaces.py
+++ b/brian_dashboard_manager/inventory_provider/interfaces.py
@@ -40,6 +40,8 @@ def get_interfaces(host):  # pragma: no cover
             return interface
 
         ip = router.get(interface['name'])
+        if not ip:
+            return interface
         ipv4 = ip['ipv4']
         ipv6 = ip['ipv6']
         interface['ipv4'] = ipv4
@@ -47,3 +49,17 @@ def get_interfaces(host):  # pragma: no cover
         return interface
     enriched = list(map(enrich, interfaces))
     return enriched
+
+
+def get_gws_direct(host):
+    r = requests.get(f'{host}/poller/gws/direct')
+    r.raise_for_status()
+    interfaces = r.json()
+    return interfaces
+
+
+def get_gws_indirect(host):
+    r = requests.get(f'{host}/poller/gws/indirect')
+    r.raise_for_status()
+    interfaces = r.json()
+    return interfaces
diff --git a/brian_dashboard_manager/routes/update.py b/brian_dashboard_manager/routes/update.py
index 780d40d6affb7aa7784273df23d6760e2830902f..9b6fa58732e33ec6e2bfa602bb076c29d13b5fd9 100644
--- a/brian_dashboard_manager/routes/update.py
+++ b/brian_dashboard_manager/routes/update.py
@@ -40,6 +40,13 @@ def should_provision():
             timestamp = datetime.datetime.fromtimestamp(
                 state.get('timestamp', 1))
 
+            now = datetime.datetime.now()
+            if provisioning and (now - timestamp).total_seconds() > 86400:
+                # if we stay in provisioning state
+                # for over a day, we probably restarted
+                # and the state file is out of sync.
+                provisioning = False
+
             can_provision = not provisioning
             return can_provision, timestamp
     except FileNotFoundError:
diff --git a/brian_dashboard_manager/templating/gws.py b/brian_dashboard_manager/templating/gws.py
new file mode 100644
index 0000000000000000000000000000000000000000..b781fd63d482d18b2c8653d90c8a4eb6e1c760e6
--- /dev/null
+++ b/brian_dashboard_manager/templating/gws.py
@@ -0,0 +1,88 @@
+from typing import DefaultDict
+from brian_dashboard_manager.templating.helpers import get_dashboard_data
+
+
+def get_panel_data(interfaces):
+    result = DefaultDict(list)
+
+    count = {}
+
+    def add_num(iface):
+        name = iface['nren'] + iface['isp'] + iface['tag']
+        count[name] = count.get(name, 0) + 1
+        iface['num'] = count[name]
+        return iface
+
+    # Add a number to multiple interfaces for same nren/isp combination,
+    # as they share the same tag if the hostnames are different.
+    interfaces = list(map(add_num, interfaces))
+
+    for interface in interfaces:
+
+        isp = interface.get('isp')
+        nren = interface.get('nren')
+        hostname = interface.get('hostname')
+
+        # identifier for interface, as we don't have their names
+        interface_tag = interface.get('tag')
+        if_num = interface.get('num')
+
+        counters = interface.get('counters')
+
+        skip = True
+
+        for counter in counters:
+            if counter.get('field') in ['traffic_in', 'traffic_out']:
+                skip = False
+
+        if skip:
+            # only process interfaces where we are polling traffic data
+            continue
+
+        gws_measurement = 'gwsd_rates'
+        title = f'{nren} GWS Direct {isp} Interface {if_num} ({hostname})'
+        result[f'GWS Direct - {isp}'].append({
+            'isp': isp,
+            'nren': nren,
+            'measurement': gws_measurement,
+            'title': title,
+            'interface_tag': interface_tag,
+            'hostname': hostname,
+            'has_v6': False
+        })
+    return result
+
+
+def get_gws_indirect_panel_data(interfaces):
+    result = DefaultDict(list)
+
+    for interface in interfaces:
+
+        hostname = interface.get('hostname').replace('.geant.net', '')
+        if_name = interface.get('interface')
+        service_name = interface.get('name')
+        customer = interface.get('customer')
+
+        measurement = 'dscp32_rates'
+        panel_title = f'{hostname} - {{}} - {if_name} - #{service_name} IASGWS'
+        result[f'GWS Indirect - {customer}'].append({
+            'measurement': measurement,
+            'title': panel_title,
+            'interface': if_name,
+            'hostname': interface.get('hostname'),
+            'has_v6': False
+        })
+    return result
+
+
+def generate_gws(gws_data, datasource):
+
+    panel_data = get_panel_data(gws_data)
+    for dash in get_dashboard_data(panel_data, datasource, 'GWS_DIRECT'):
+        yield dash
+
+
+def generate_indirect(gws_data, datasource):
+    panel_data = get_gws_indirect_panel_data(gws_data)
+    for dash in get_dashboard_data(panel_data, datasource, 'GWS_INDIRECT'):
+        yield dash
diff --git a/brian_dashboard_manager/templating/helpers.py b/brian_dashboard_manager/templating/helpers.py
index d2001f1156f5299a477bfb21c88746ce98ed1e85..5ce022c39c01206662da9017ef85c27f4cb02a77 100644
--- a/brian_dashboard_manager/templating/helpers.py
+++ b/brian_dashboard_manager/templating/helpers.py
@@ -294,7 +294,9 @@ def get_panel_fields(panel, panel_type, datasource):
 
     def get_target_data(alias, field):
         return {
-            **panel,  # panel has target hostname and interface
+            # panel includes identifying information
+            # such as hostname, interface, etc.
+            **panel,
             'alias': alias,
             'refId': next(letters),
             'select_field': field,
@@ -344,9 +346,9 @@ def get_dashboard_data(data, datasource, tag, errors=False):
                     {**panel, **next(gridPos)}, 'errors', datasource))
         return result
 
-    for peer, panels in data.items():
+    for dashboard_name, panels in data.items():
         result = {
-            'title': peer,
+            'title': dashboard_name,
             'datasource': datasource,
             'panels': get_panel_definitions(panels, datasource),
         }
diff --git a/brian_dashboard_manager/templating/templates/shared/panel_target.json.j2 b/brian_dashboard_manager/templating/templates/shared/panel_target.json.j2
index b98bd3c3b5e335c6c7f798ee1ac2ad8eead80143..0bcd9f694d3153a187f63012bb032a8bbc796cb4 100644
--- a/brian_dashboard_manager/templating/templates/shared/panel_target.json.j2
+++ b/brian_dashboard_manager/templating/templates/shared/panel_target.json.j2
@@ -12,7 +12,11 @@
         }
         {% endif %}
     ],
+    {% if measurement %}
+    "measurement": "{{ measurement }}",
+    {% else %}
     "measurement": "interface_rates",
+    {% endif %}
     "orderByTime": null,
     "policy": null,
     "refId": "{{ refId }}",
@@ -41,6 +45,7 @@
         ]
     ],
     "tags": [
+        {% if not isp %}
         {
             "condition": null,
             "key": "hostname",
@@ -53,5 +58,25 @@
             "operator": "=",
             "value": "{{ interface }}"
         }
+        {% else %}
+        {
+            "condition": null,
+            "key": "tag",
+            "operator": "=",
+            "value": "{{ interface_tag }}"
+        },
+        {
+            "condition": "AND",
+            "key": "isp",
+            "operator": "=",
+            "value": "{{ isp }}"
+        },
+        {
+            "condition": "AND",
+            "key": "nren",
+            "operator": "=",
+            "value": "{{ nren }}"
+        }
+        {% endif %}
     ]
 }
\ No newline at end of file
diff --git a/changelog.md b/changelog.md
index 23becc2556f5a7e4661670890b4f8156b1d46a7b..f9bafb446be11885a85a10a7b74e0802e689d004 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,10 @@
 
 All notable changes to this project will be documented in this file.
 
+## [0.16] - 2021-07-13
+- Add GWS Direct/Indirect graphs + related changes
+- Improve request logging for easier debugging
+
 ## [0.15] - 2021-05-17
 - [POL1-433] Only provision aggregate panels for NRENs if LAG interfaces are present.
 
diff --git a/setup.py b/setup.py
index 47b079424ac889549eb2f03be9e24b71f98d8df5..f4245e35f135384ed9269beaf3a3238f129f0e0e 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='brian-dashboard-manager',
-    version="0.15",
+    version="0.16",
     author='GEANT',
     author_email='swd@geant.org',
     description='',
diff --git a/test/test_gws_direct.py b/test/test_gws_direct.py
new file mode 100644
index 0000000000000000000000000000000000000000..688db385a1a0d1371876168d4f46b4066ed5cb5b
--- /dev/null
+++ b/test/test_gws_direct.py
@@ -0,0 +1,133 @@
+import responses
+import json
+from brian_dashboard_manager.templating.gws import generate_gws
+from brian_dashboard_manager.inventory_provider.interfaces import \
+    get_gws_direct
+
+
+TEST_DATA = [
+    {
+        "nren": "ARNES",
+        "isp": "Cogent",
+        "hostname": "88.200.0.63",
+        "tag": "a",
+        "counters": [
+            {
+                "field": "discards_in",
+                "oid": "1.3.6.1.2.1.2.2.1.13.533",
+                "community": "gn2nocT3st"
+            },
+            {
+                "field": "discards_out",
+                "oid": "1.3.6.1.2.1.2.2.1.19.533",
+                "community": "gn2nocT3st"
+            },
+            {
+                "field": "errors_in",
+                "oid": "1.3.6.1.2.1.2.2.1.14.533",
+                "community": "gn2nocT3st"
+            },
+            {
+                "field": "errors_out",
+                "oid": "1.3.6.1.2.1.2.2.1.20.533",
+                "community": "gn2nocT3st"
+            }
+        ]
+    },
+    {
+        "nren": "ARNES",
+        "isp": "Cogent",
+        "hostname": "88.200.0.63",
+        "tag": "b",
+        "counters": [
+            {
+                "field": "traffic_in",
+                "oid": "1.3.6.1.2.1.31.1.1.1.6.531",
+                "community": "gn2nocT3st"
+            },
+            {
+                "field": "traffic_out",
+                "oid": "1.3.6.1.2.1.31.1.1.1.10.531",
+                "community": "gn2nocT3st"
+            }
+        ]
+    },
+    {
+        "nren": "ARNES",
+        "isp": "Cogent",
+        "hostname": "88.200.0.63",
+        "tag": "c",
+        "counters": [
+            {
+                "field": "traffic_in",
+                "oid": "1.3.6.1.2.1.31.1.1.1.6.525",
+                "community": "gn2nocT3st"
+            },
+            {
+                "field": "traffic_out",
+                "oid": "1.3.6.1.2.1.31.1.1.1.10.525",
+                "community": "gn2nocT3st"
+            }
+        ]
+    },
+    {
+        "nren": "ARNES",
+        "isp": "Cogent",
+        "hostname": "88.200.0.63",
+        "tag": "d",
+        "counters": [
+            {
+                "field": "traffic_in",
+                "oid": "1.3.6.1.2.1.31.1.1.1.6.553",
+                "community": "gn2nocT3st"
+            },
+            {
+                "field": "traffic_out",
+                "oid": "1.3.6.1.2.1.31.1.1.1.10.553",
+                "community": "gn2nocT3st"
+            }
+        ]
+    },
+    {
+        "nren": "ARNES",
+        "isp": "Telia",
+        "hostname": "62.40.124.6",
+        "tag": "a",
+        "counters": [
+            {
+                "field": "traffic_in",
+                "oid": "1.3.6.1.2.1.31.1.1.1.6.611",
+                "community": "gn2nocT3st"
+            },
+            {
+                "field": "traffic_out",
+                "oid": "1.3.6.1.2.1.31.1.1.1.10.611",
+                "community": "gn2nocT3st"
+            }
+        ]
+    }
+]
+
+
+@responses.activate
+def test_gws(data_config, mocker, client):
+
+    def get_callback(request):
+        return 200, {}, json.dumps(TEST_DATA)
+
+    responses.add_callback(
+        method=responses.GET,
+        url=f"{data_config['inventory_provider']}/poller/gws/direct",
+        callback=get_callback)
+
+    gws_data = get_gws_direct(data_config['inventory_provider'])
+
+    dashboards = list(generate_gws(gws_data, 'testdatasource'))
+
+    assert len(dashboards) == 2
+
+    assert dashboards[0]['title'] == 'GWS Direct - Cogent'
+    assert len(dashboards[0]['panels']) == 3
+
+    assert dashboards[1]['title'] == 'GWS Direct - Telia'
+    assert len(dashboards[1]['panels']) == 1
diff --git a/test/test_gws_indirect.py b/test/test_gws_indirect.py
new file mode 100644
index 0000000000000000000000000000000000000000..9edd611152bbd469f5ba3deec6a38e4069ed86e4
--- /dev/null
+++ b/test/test_gws_indirect.py
@@ -0,0 +1,102 @@
+import responses
+import json
+from brian_dashboard_manager.templating.gws import generate_indirect
+from brian_dashboard_manager.inventory_provider.interfaces import \
+    get_gws_indirect
+
+
+TEST_DATA = [
+    {
+        "id": 712361,
+        "name": "FCCN-AP3-IAS",
+        "customer": "FCCN",
+        "speed": 107374182400,
+        "pop": "PORTO",
+        "hostname": "rt1.por.pt.geant.net",
+        "interface": "ae10.333",
+        "type": "GWS - INDIRECT",
+        "status": "operational"
+    },
+    {
+        "id": 661222,
+        "name": "FCCN-AP2-IAS",
+        "customer": "FCCN",
+        "speed": 42949672960,
+        "pop": "LISBON 2",
+        "hostname": "mx1.lis.pt.geant.net",
+        "interface": "ae10.333",
+        "type": "GWS - INDIRECT",
+        "status": "operational"
+    },
+    {
+        "id": 661500,
+        "name": "IUCC-AP1-IAS",
+        "customer": "IUCC",
+        "speed": 32212254720,
+        "pop": "LONDON",
+        "hostname": "mx1.lon.uk.geant.net",
+        "interface": "ae21.333",
+        "type": "GWS - INDIRECT",
+        "status": "operational"
+    },
+    {
+        "id": 663112,
+        "name": "ROEDUNET_AP1_IAS",
+        "customer": "ROEDUNET",
+        "speed": 42949672960,
+        "pop": "BUCHAREST",
+        "hostname": "mx1.buc.ro.geant.net",
+        "interface": "ae11.333",
+        "type": "GWS - INDIRECT",
+        "status": "operational"
+    },
+    {
+        "id": 663228,
+        "name": "IUCC-AP2-IAS",
+        "customer": "IUCC",
+        "speed": 32212254720,
+        "pop": "FRANKFURT",
+        "hostname": "mx1.fra.de.geant.net",
+        "interface": "ae21.333",
+        "type": "GWS - INDIRECT",
+        "status": "operational"
+    },
+    {
+        "id": 661641,
+        "name": "FCCN-AP1-IAS",
+        "customer": "FCCN",
+        "speed": 42949672960,
+        "pop": "LISBON",
+        "hostname": "mx2.lis.pt.geant.net",
+        "interface": "ae10.333",
+        "type": "GWS - INDIRECT",
+        "status": "operational"
+    }
+]
+
+
+@responses.activate
+def test_gws(data_config, mocker, client):
+
+    def get_callback(request):
+        return 200, {}, json.dumps(TEST_DATA)
+
+    responses.add_callback(
+        method=responses.GET,
+        url=f"{data_config['inventory_provider']}/poller/gws/indirect",
+        callback=get_callback)
+
+    gws_data = get_gws_indirect(data_config['inventory_provider'])
+
+    dashboards = list(generate_indirect(gws_data, 'testdatasource'))
+
+    assert len(dashboards) == 3
+
+    assert dashboards[0]['title'] == 'GWS Indirect - FCCN'
+    assert len(dashboards[0]['panels']) == 3
+
+    assert dashboards[1]['title'] == 'GWS Indirect - IUCC'
+    assert len(dashboards[1]['panels']) == 2
+
+    assert dashboards[2]['title'] == 'GWS Indirect - ROEDUNET'
+    assert len(dashboards[2]['panels']) == 1
diff --git a/test/test_update.py b/test/test_update.py
index 0512ebcebc934133fb3f53fadcc2ad46f0b990e0..55050de6be23493d7b201cdecb1dfaa0f36ad556 100644
--- a/test/test_update.py
+++ b/test/test_update.py
@@ -460,6 +460,14 @@ def test_provision(data_config, mocker, client):
     _mocked_get_dashboard_definitions = mocker.patch(
         'brian_dashboard_manager.grafana.provision.get_dashboard_definitions')
 
+    _mocked_gws = mocker.patch(
+        'brian_dashboard_manager.grafana.provision.get_gws_direct')
+    _mocked_gws.return_value = []
+
+    _mocked_gws_indirect = mocker.patch(
+        'brian_dashboard_manager.grafana.provision.get_gws_indirect')
+    _mocked_gws_indirect.return_value = []
+
     UID = 1
     ID = 1
     VERSION = 1