diff --git a/Changelog.md b/Changelog.md
index 241783dd118098419ef63f16cac824ebae0f5272..5e1473f11bcd48d66948d2ff076f733a7490b762 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -2,6 +2,12 @@
 
 All notable changes to this project will be documented in this file.
 
+## [0.67] - 2021-06-24
+- DBOARD3-448: pulled additional customers from CircuitCustomerRelation
+- DBOARD3-449: add CORS headers to responses
+- POL1-452: added /poller/gws/indirect
+- POL1-453: config should contain 'CenturyLink' and not 'Century Link'
+
 ## [0.66] - 2021-06-09
 - POL1-445: added /poller/gws/direct endpoint
 - DBOARD3-445: bugfixes in /lg/routers/X endpoint
diff --git a/inventory_provider/__init__.py b/inventory_provider/__init__.py
index aa92cb410dffe41600160d1d94bd1b98e3f9a75d..877944d598d7f3731b2022174a03f7424eadcb1c 100644
--- a/inventory_provider/__init__.py
+++ b/inventory_provider/__init__.py
@@ -4,6 +4,7 @@ automatically invoked app factory
 import logging
 import os
 from flask import Flask
+from flask_cors import CORS
 
 from inventory_provider import environment
 
@@ -34,6 +35,8 @@ def create_app():
         inventory_provider_config = config.load(f)
 
     app = Flask(__name__)
+    CORS(app)
+
     app.secret_key = 'super secret session key'
 
     logging.info(
diff --git a/inventory_provider/config.py b/inventory_provider/config.py
index 4881d4c10921fae338a29cf71b8638e48add315d..733877cc12a078b0579ae783dc9585d490471e63 100644
--- a/inventory_provider/config.py
+++ b/inventory_provider/config.py
@@ -137,7 +137,7 @@ CONFIG_SCHEMA = {
                 'nren': {'type': 'string'},
                 'isp': {
                     'type': 'string',
-                    'enum': ['Cogent', 'Telia', 'Century Link']
+                    'enum': ['Cogent', 'Telia', 'CenturyLink']
                 },
                 'hosts': {
                     'type': 'array',
diff --git a/inventory_provider/db/ims_data.py b/inventory_provider/db/ims_data.py
index f6d856c845b328aa7a0cf65a7afa7af10adad29d..0283754bf0f3367fdb6a1bf9911dcf3ecb88970d 100644
--- a/inventory_provider/db/ims_data.py
+++ b/inventory_provider/db/ims_data.py
@@ -2,7 +2,8 @@ import logging
 import re
 from collections import OrderedDict, defaultdict
 from copy import copy
-from itertools import chain
+from itertools import chain, groupby
+from operator import itemgetter
 
 from inventory_provider import environment
 from inventory_provider.db import ims
@@ -75,6 +76,18 @@ def get_customer_service_emails(ds: IMS):
         yield k, sorted(list(v))
 
 
+def get_circuit_related_customer_ids(ds: IMS):
+    relations = sorted(
+        list(
+            ds.get_filtered_entities(
+                'CircuitCustomerRelation',
+                'circuit.inventoryStatusId== 3')
+        ), key=itemgetter('circuitid'))
+
+    return {k: [c['customerid'] for c in v] for k, v in
+            groupby(relations, key=itemgetter('circuitid'))}
+
+
 def get_port_id_services(ds: IMS):
     circuit_nav_props = [
         ims.CIRCUIT_PROPERTIES['Ports'],
diff --git a/inventory_provider/routes/classifier.py b/inventory_provider/routes/classifier.py
index 6d27c8a081c398aec98ade4aac42647ef0dd00fe..d2b46d342a1793562e52aa5cd04ca265c88a480c 100644
--- a/inventory_provider/routes/classifier.py
+++ b/inventory_provider/routes/classifier.py
@@ -185,12 +185,12 @@ def get_interface_services_and_loc(ims_source_equipment, ims_interface, redis):
         for s in json.loads(raw_services.decode('utf-8')):
             related_services.update(
                 {r['id']: r for r in s['related-services']})
+            contacts.update(set(s.pop('contacts', set())))
             if s['circuit_type'] == 'service':
-                contacts.update(set(s.pop('contacts', set())))
                 _format_service(s)
                 result['services'].append(s)
-        result['related-services'] = list(related_services.values())
         result['contacts'] = sorted(list(contacts))
+        result['related-services'] = list(related_services.values())
 
         if not result['services']:
             result.pop('services', None)
diff --git a/inventory_provider/routes/common.py b/inventory_provider/routes/common.py
index 56ed526edd5f5fa87a6b0c37947f73bf36fb8f73..8213ad6ba18a635f3f17c3324f4d29e3d955234c 100644
--- a/inventory_provider/routes/common.py
+++ b/inventory_provider/routes/common.py
@@ -4,6 +4,7 @@ import json
 import logging
 import queue
 import random
+import re
 import threading
 
 from distutils.util import strtobool
@@ -17,13 +18,18 @@ _DECODE_TYPE_XML = 'xml'
 _DECODE_TYPE_JSON = 'json'
 
 
-def _ignore_cache_or_retrieve(request_, cache_key, r):
-
-    ignore_cache = request_.args.get('ignore-cache', default='false', type=str)
+def get_bool_request_arg(name, default=False):
+    assert isinstance(default, bool)  # sanity, otherwise caller error
+    value = request.args.get(name, default=str(default), type=str)
     try:
-        ignore_cache = strtobool(ignore_cache)
+        value = strtobool(value)
     except ValueError:
-        ignore_cache = False
+        value = default
+    return value
+
+
+def _ignore_cache_or_retrieve(request_, cache_key, r):
+    ignore_cache = get_bool_request_arg('ignore-cache', default=False)
     if ignore_cache:
         result = False
         logger.debug('ignoring cache')
@@ -254,6 +260,7 @@ def load_xml_docs(config_params, key_pattern, num_threads=10):
         config_params, key_pattern, num_threads, doc_type=_DECODE_TYPE_XML)
 
 
+@functools.lru_cache(maxsize=None)
 def load_snmp_indexes(hostname=None):
     result = dict()
     key_pattern = f'snmp-interfaces:{hostname}*' \
@@ -266,3 +273,21 @@ def load_snmp_indexes(hostname=None):
         result[router] = {e['name']: e for e in doc['value']}
 
     return result
+
+
+def ims_equipment_to_hostname(equipment):
+    """
+    changes names like MX1.AMS.NL to mx1.ams.nl.geant.net
+
+    leaves CPE names alone (e.g. 'INTERXION Z-END')
+    :param equipment: the IMS equipment name string
+    :return: hostname, or the input string if it doesn't look like a host
+    """
+    if re.match(r'.*\s.*', equipment):
+        # doesn't look like a hostname
+        return equipment
+
+    hostname = equipment.lower()
+    if not re.match(r'.*\.geant\.(net|org)$', hostname):
+        hostname = f'{hostname}.geant.net'
+    return hostname
diff --git a/inventory_provider/routes/jobs.py b/inventory_provider/routes/jobs.py
index 078e8a56392bd5bb4bb4076d83b6ced585f89e97..b33476fb1d031b4133585eb204c2ca587b466b59 100644
--- a/inventory_provider/routes/jobs.py
+++ b/inventory_provider/routes/jobs.py
@@ -28,8 +28,7 @@ These endpoints are used for monitoring running jobs.
 import json
 import logging
 
-from distutils.util import strtobool
-from flask import Blueprint, current_app, jsonify, Response, request
+from flask import Blueprint, current_app, jsonify, Response
 
 from inventory_provider.tasks import monitor
 from inventory_provider.tasks import worker
@@ -125,15 +124,10 @@ def update():
 
     :return:
     """
-    force = request.args.get('force', default='false', type=str)
-    try:
-        force = strtobool(force)
-    except ValueError:
-        force = False
-
     config = current_app.config['INVENTORY_PROVIDER_CONFIG']
     r = get_current_redis(config)
 
+    force = common.get_bool_request_arg('force', default=False)
     if not force:
         latch = get_latch(r)
         if latch and latch['pending']:
diff --git a/inventory_provider/routes/lg.py b/inventory_provider/routes/lg.py
index 21360110371073f056a65a440facc094567cb444..e8231e86bdf04f558d662278692bc4790ddad59d 100644
--- a/inventory_provider/routes/lg.py
+++ b/inventory_provider/routes/lg.py
@@ -12,7 +12,6 @@ These endpoints are intended for use by LG.
 """
 import json
 import logging
-import re
 
 from flask import Blueprint, Response, request
 
@@ -119,14 +118,12 @@ def routers(access):
         for k in redis.scan_iter('ims:lg:*', count=1000):
             rtr = redis.get(k.decode('utf-8')).decode('utf-8')
             rtr = json.loads(rtr)
-            hostname = rtr['equipment name'].lower()
-            if ' ' in hostname:
+            if ' ' in rtr['equipment name']:
                 logger.warning(
                     'skipping LG router with ws in hostname: {hostname}')
                 continue
-            if not re.match(r'.*\.geant\.(net|org)$', hostname):
-                hostname = f'{hostname}.geant.net'
-            rtr['equipment name'] = hostname
+            rtr['equipment name'] = common.ims_equipment_to_hostname(
+                rtr['equipment name'])
             yield rtr
 
     cache_key = f'classifier-cache:ims-lg:{access}'
diff --git a/inventory_provider/routes/msr.py b/inventory_provider/routes/msr.py
index fa944be7e8bc7af567e411df0829c8e0abe98150..0bb6a65c68f23bf45ceac35c979f391c85ef563d 100644
--- a/inventory_provider/routes/msr.py
+++ b/inventory_provider/routes/msr.py
@@ -4,12 +4,13 @@ MSR Support Endpoints
 
 These endpoints are intended for use by MSR.
 
+
 .. contents:: :local:
 
-/msr/access-services
+/poller/access-services
 ---------------------------------
 
-.. autofunction:: inventory_provider.routes.msr.access_services
+.. autofunction::inventory_provider.routes.msr.get_access_services
 
 
 /msr/bgp/logical-systems
@@ -62,75 +63,46 @@ from flask import Blueprint, Response, request
 
 from inventory_provider.routes import common
 from inventory_provider.routes.common import _ignore_cache_or_retrieve
+from inventory_provider.routes.poller import get_services
 
-routes = Blueprint("msr-query-routes", __name__)
-
+routes = Blueprint('msr-query-routes', __name__)
 
-ACCESS_SERVICES_LIST_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-07/schema#",
-
-    "definitions": {
-        "service": {
-            "type": "object",
-            "properties": {
-                "id": {"type": "integer"},
-                "name": {"type": "string"},
-                "equipment": {"type": "string"},
-                "pop_name": {"type": "string"},
-                "other_end_equipment": {"type": "string"},
-                "other_end_pop_name": {"type": "string"},
-                "speed_value": {"type": "integer"},
-                "speed_unit": {"type": "string"}
-            },
-            "required": [
-                "id", "name",
-                "pop_name", "equipment",
-                "other_end_pop_name", "other_end_equipment",
-                "speed_value", "speed_unit"
-            ],
-            "additionalProperties": False
-        }
-    },
-
-    "type": "array",
-    "items": {"$ref": "#/definitions/service"}
-}
 
 PEERING_GROUP_LIST_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-07/schema#",
-    "type": "array",
-    "items": {"type": "string"}
+    '$schema': 'http://json-schema.org/draft-07/schema#',
+    'type': 'array',
+    'items': {'type': 'string'}
 
 }
 
 PEERING_LIST_SCHEMA = {
-    "$schema": "http://json-schema.org/draft-07/schema#",
-    "definitions": {
-        "peering-instance": {
-            "type": "object",
-            "properties": {
-                "address": {"type": "string"},
-                "description": {"type": "string"},
-                "logical-system": {"type": "string"},
-                "group": {"type": "string"},
-                "hostname": {"type": "string"},
-                "remote-asn": {"type": "integer"},
-                "local-asn": {"type": "integer"},
-                "instance": {"type": "string"}
+    '$schema': 'http://json-schema.org/draft-07/schema#',
+    'definitions': {
+        'peering-instance': {
+            'type': 'object',
+            'properties': {
+                'address': {'type': 'string'},
+                'description': {'type': 'string'},
+                'logical-system': {'type': 'string'},
+                'group': {'type': 'string'},
+                'hostname': {'type': 'string'},
+                'remote-asn': {'type': 'integer'},
+                'local-asn': {'type': 'integer'},
+                'instance': {'type': 'string'}
             },
             # only vrr peerings have remote-asn
             # only group peerings have local-asn or instance
             # not all group peerings have 'description'
             # and only vrr or vpn-proxy peerings are within a logical system
-            "required": [
-                "address",
-                "group",
-                "hostname"],
-            "additionalProperties": False
+            'required': [
+                'address',
+                'group',
+                'hostname'],
+            'additionalProperties': False
         }
     },
-    "type": "array",
-    "items": {"$ref": "#/definitions/peering-instance"}
+    'type': 'array',
+    'items': {'$ref': '#/definitions/peering-instance'}
 }
 
 
@@ -139,48 +111,6 @@ def after_request(resp):
     return common.after_request(resp)
 
 
-@routes.route("/access-services", methods=['GET', 'POST'])
-@common.require_accepts_json
-def access_services():
-    """
-    Handler for `/msr/access-services`.
-
-    This method is in development, not yet used.
-
-    The response will be formatted according to the following schema:
-
-    .. asjson::
-       inventory_provider.routes.msr.ACCESS_SERVICES_LIST_SCHEMA
-
-    :return:
-    """
-    redis = common.get_current_redis()
-
-    def _services():
-        for k in redis.scan_iter('ims:access_services:*'):
-            service = redis.get(k.decode('utf-8')).decode('utf-8')
-            yield json.loads(service)
-
-    cache_key = 'classifier-cache:msr:access-services'
-
-    result = _ignore_cache_or_retrieve(request, cache_key, redis)
-
-    if not result:
-        result = list(_services())
-
-        if not result:
-            return Response(
-                response='no access services found',
-                status=404,
-                mimetype="text/html")
-
-        # cache this data for the next call
-        result = json.dumps(result)
-        redis.set(cache_key, result.encode('utf-8'))
-
-    return Response(result, mimetype="application/json")
-
-
 def _handle_peering_group_request(name, cache_key, group_key_base):
     """
     Common method for used by
@@ -239,8 +169,8 @@ def _handle_peering_group_request(name, cache_key, group_key_base):
     return Response(items, mimetype="application/json")
 
 
-@routes.route("/bgp/logical-system-peerings", methods=['GET', 'POST'])
-@routes.route("/bgp/logical-system-peerings/<name>", methods=['GET', 'POST'])
+@routes.route('/bgp/logical-system-peerings', methods=['GET', 'POST'])
+@routes.route('/bgp/logical-system-peerings/<name>', methods=['GET', 'POST'])
 @common.require_accepts_json
 def logical_system_peerings(name=None):
     """
@@ -258,8 +188,8 @@ def logical_system_peerings(name=None):
         group_key_base='juniper-peerings:logical-system')
 
 
-@routes.route("/bgp/group-peerings", methods=['GET', 'POST'])
-@routes.route("/bgp/group-peerings/<name>", methods=['GET', 'POST'])
+@routes.route('/bgp/group-peerings', methods=['GET', 'POST'])
+@routes.route('/bgp/group-peerings/<name>', methods=['GET', 'POST'])
 @common.require_accepts_json
 def bgp_group_peerings(name=None):
     """
@@ -277,8 +207,8 @@ def bgp_group_peerings(name=None):
         group_key_base='juniper-peerings:group')
 
 
-@routes.route("/bgp/routing-instance-peerings", methods=['GET', 'POST'])
-@routes.route("/bgp/routing-instance-peerings/<name>", methods=['GET', 'POST'])
+@routes.route('/bgp/routing-instance-peerings', methods=['GET', 'POST'])
+@routes.route('/bgp/routing-instance-peerings/<name>', methods=['GET', 'POST'])
 @common.require_accepts_json
 def bgp_routing_instance_peerings(name=None):
     """
@@ -338,7 +268,7 @@ def _handle_peering_group_list_request(cache_key, group_key_base):
     return Response(names, mimetype="application/json")
 
 
-@routes.route("/bgp/logical-systems", methods=['GET', 'POST'])
+@routes.route('/bgp/logical-systems', methods=['GET', 'POST'])
 @common.require_accepts_json
 def get_logical_systems():
     """
@@ -354,7 +284,7 @@ def get_logical_systems():
         group_key_base='juniper-peerings:logical-system')
 
 
-@routes.route("/bgp/groups", methods=['GET', 'POST'])
+@routes.route('/bgp/groups', methods=['GET', 'POST'])
 @common.require_accepts_json
 def get_peering_groups():
     """
@@ -370,7 +300,7 @@ def get_peering_groups():
         group_key_base='juniper-peerings:group')
 
 
-@routes.route("/bgp/routing-instances", methods=['GET', 'POST'])
+@routes.route('/bgp/routing-instances', methods=['GET', 'POST'])
 @common.require_accepts_json
 def get_peering_routing_instances():
     """
@@ -384,3 +314,16 @@ def get_peering_routing_instances():
     return _handle_peering_group_list_request(
         cache_key='classifier-cache:msr:routing-instances',
         group_key_base='juniper-peerings:routing-instance')
+
+
+@routes.route('/access-services', methods=['GET', 'POST'])
+@common.require_accepts_json
+def get_access_services():
+    """
+    Handler for `/msr/access-services`
+
+    Same as `/poller/services/geant_ip`
+
+    cf. :meth:`inventory_provider.routes.poller.get_services`
+    """
+    return get_services(service_type='geant_ip')
diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py
index 4be62cbfc5e12d3161af63e2c38c2daa1b0c4e28..9561ce47a0c4a0de06a5f62fc97e00d6fb104065 100644
--- a/inventory_provider/routes/poller.py
+++ b/inventory_provider/routes/poller.py
@@ -29,6 +29,25 @@ These endpoints are intended for use by BRIAN.
 
 .. autofunction:: inventory_provider.routes.poller.gws_direct
 
+
+/poller/gws/indirect
+---------------------------------
+
+.. autofunction:: inventory_provider.routes.poller.gws_indirect
+
+
+/poller/services</service-type>
+---------------------------------
+
+.. autofunction:: inventory_provider.routes.poller.get_services
+
+
+/poller/service-types
+---------------------------------
+
+.. autofunction:: inventory_provider.routes.poller.get_service_types
+
+
 """
 import json
 import logging
@@ -39,6 +58,7 @@ from lxml import etree
 
 from inventory_provider import juniper
 from inventory_provider.routes import common
+from inventory_provider.tasks.common import ims_sorted_service_type_key
 from inventory_provider.routes.classifier import get_ims_equipment_name, \
     get_ims_interface
 from inventory_provider.routes.common import _ignore_cache_or_retrieve
@@ -182,7 +202,7 @@ GWS_DIRECT_DATA_SCHEMA = {
                 'nren': {'type': 'string'},
                 'isp': {
                     'type': 'string',
-                    'enum': ['Cogent', 'Telia', 'Century Link']
+                    'enum': ['Cogent', 'Telia', 'CenturyLink']
                 },
                 'hostname': {'type': 'string'},
                 'tag': {'type': 'string'},
@@ -203,6 +223,74 @@ GWS_DIRECT_DATA_SCHEMA = {
 }
 
 
+SERVICES_LIST_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-07/schema#',
+
+    'definitions': {
+        'oid': {
+            'type': 'string',
+            'pattern': r'^(\d+\.)*\d+$'
+        },
+        'counter-field': {
+            'type': 'object',
+            'properties': {
+                'field': {
+                    'type': 'string',
+                    'enum': ['egressOctets', 'ingressOctets']
+                },
+                'oid': {'$ref': '#/definitions/oid'}
+            },
+            'required': ['field', 'oid'],
+            'additionalProperties': False
+        },
+        'snmp-info': {
+            'type': 'object',
+            'properties': {
+                'ifIndex': {'type': 'integer'},
+                'community': {'type': 'string'},
+                'counters': {
+                    'type': 'array',
+                    'items': {'$ref': '#/definitions/counter-field'},
+                    'minItems': 1
+                }
+            },
+            'required': ['ifIndex', 'community'],
+            'additionalProperties': False
+        },
+        'service': {
+            'type': 'object',
+            'properties': {
+                'id': {'type': 'integer'},
+                'name': {'type': 'string'},
+                'customer': {'type': 'string'},
+                'speed': {'type': 'integer'},
+                'pop': {'type': 'string'},
+                'hostname': {'type': 'string'},
+                'interface': {'type': 'string'},
+                'type': {'type': 'string'},
+                'status': {'type': 'string'},
+                'snmp': {'$ref': '#/definitions/snmp-info'}
+            },
+            'required': [
+                'id', 'name', 'customer',
+                'speed', 'pop', 'hostname',
+                'interface', 'type', 'status'],
+            'additionalProperties': False
+        }
+    },
+
+    'type': 'array',
+    'items': {'$ref': '#/definitions/service'}
+}
+
+
+STRING_LIST_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-07/schema#',
+    'type': 'array',
+    'items': {'type': 'string'}
+}
+
+
 @routes.after_request
 def after_request(resp):
     return common.after_request(resp)
@@ -314,7 +402,9 @@ def _load_interfaces(hostname):
 
     base_key_pattern = f'netconf:{hostname}*' if hostname else 'netconf:*'
     yield from _load_docs(base_key_pattern)
-    yield from _load_docs(f'lab:{base_key_pattern}')
+    no_lab = common.get_bool_request_arg('no-lab', False)
+    if not no_lab:
+        yield from _load_docs(f'lab:{base_key_pattern}')
 
 
 def _add_bundle_parents(interfaces, hostname=None):
@@ -407,6 +497,9 @@ def interfaces(hostname=None):
     which returns information for either all interfaces
     or those on the requested hostname.
 
+    The optional `no-lab` parameter omits lab routers
+    if it's truthiness evaluates to True.
+
     The response is a list of information for all
     interfaces that should be polled, including service
     information and snmp information.
@@ -421,6 +514,10 @@ def interfaces(hostname=None):
     cache_key = f'classifier-cache:poller-interfaces:{hostname}' \
         if hostname else 'classifier-cache:poller-interfaces:all'
 
+    no_lab = common.get_bool_request_arg('no-lab', False)
+    if no_lab:
+        cache_key = f'{cache_key}:no_lab'
+
     r = common.get_current_redis()
 
     result = _ignore_cache_or_retrieve(request, cache_key, r)
@@ -623,7 +720,7 @@ def gws_direct():
        inventory_provider.routes.poller.GWS_DIRECT_DATA_SCHEMA
 
     WARNING: interface tags in the `gws-direct` section of the config data
-    should be unique for each nren/isp/hostname combination.  i.e. if there
+    should be unique for each nren/isp combination.  i.e. if there
     are multiple community strings in use for a particular host, then please
     keep the interface tags unique.
 
@@ -662,3 +759,278 @@ def gws_direct():
         r.set(cache_key, result.encode('utf-8'))
 
     return Response(result, mimetype="application/json")
+
+
+# cf. https://gitlab.geant.net/puppet-apps/cacti/-/blob/production/files/scripts/juniper-firewall-dws.pl  # noqa: E501
+
+JNX_DCU_STATS_BYTES_OID = '1.3.6.1.4.1.2636.3.6.2.1.5'
+JNX_FW_COUNTER_BYTES_OID = '1.3.6.1.4.1.2636.3.5.2.1.5'
+
+JNX_ADDRESS_FAMILY = {
+    'ipv4': 1,
+    'ipv6': 2
+}
+
+JNX_FW_COUNTER_TYPE = {
+    'other': 1,
+    'counter': 2,
+    'policer': 3
+}
+
+
+def _str2oid(s):
+    chars = '.'.join(str(ord(c)) for c in s)
+    return f'{len(s)}.{chars}'
+
+
+def _jnx_dcu_byte_count_oid(
+        ifIndex,
+        class_name='dws-in',
+        address_family=JNX_ADDRESS_FAMILY['ipv4']):
+    # sanity checks (in case of programming errors)
+    assert isinstance(ifIndex, int)
+    assert isinstance(class_name, str)
+    assert isinstance(address_family, int)
+    return '.'.join([
+        JNX_DCU_STATS_BYTES_OID,
+        str(ifIndex),
+        str(address_family),
+        _str2oid(class_name)
+    ])
+
+
+def _jnx_fw_counter_bytes_oid(
+        customer,
+        interface_name,
+        filter_name=None,
+        counter_name=None):
+    # sanity checks (in case of programming errors)
+    assert isinstance(customer, str)
+    assert isinstance(interface_name, str)
+    assert filter_name is None or isinstance(filter_name, str)
+    assert counter_name is None or isinstance(counter_name, str)
+
+    if filter_name is None:
+        filter_name = f'nren_IAS_{customer}_OUT-{interface_name}-o'
+    if counter_name is None:
+        counter_name = f'DWS-out-{interface_name}-o'
+
+    return '.'.join([
+        JNX_FW_COUNTER_BYTES_OID,
+        _str2oid(filter_name),
+        _str2oid(counter_name),
+        str(JNX_FW_COUNTER_TYPE['counter'])
+    ])
+
+
+def _get_services_internal(service_type=None):
+    """
+    Performs the lookup and caching done for calls to
+    `/poller/services</service-type>`
+
+    This is a separate private utility so that it can be called by
+    :meth:`inventory_provider.routes.poller.get_services`
+    and :meth:`inventory_provider.routes.poller.get_service_types`
+
+    The response will be formatted according to the following schema:
+
+    .. asjson::
+       inventory_provider.routes.poller.SERVICES_LIST_SCHEMA
+
+    :param service_type: a service type, or None to return all
+    :return: service list, json-serialized to a string
+    """
+
+    return_all = common.get_bool_request_arg('all', False)
+    include_snmp = common.get_bool_request_arg('snmp', False)
+
+    def _services():
+        key_pattern = f'ims:services:{service_type}:*' \
+            if service_type else 'ims:services:*'
+
+        for doc in common.load_json_docs(
+                config_params=current_app.config['INVENTORY_PROVIDER_CONFIG'],
+                key_pattern=key_pattern,
+                num_threads=20):
+            yield doc['value']
+
+    def _add_snmp(s):
+        all_snmp_info = common.load_snmp_indexes()
+        snmp_interfaces = all_snmp_info.get(s['hostname'], {})
+        interface_info = snmp_interfaces.get(s['interface'], None)
+        if interface_info:
+            s['snmp'] = {
+                'ifIndex': interface_info['index'],
+                'community': interface_info['community'],
+            }
+            if s['type'] == 'GWS - INDIRECT':
+                s['snmp']['counters'] = [
+                    {
+                        'field': 'ingressOctets',
+                        'oid': _jnx_dcu_byte_count_oid(
+                            interface_info['index']),
+                    },
+                    {
+                        'field': 'egressOctets',
+                        'oid': _jnx_fw_counter_bytes_oid(
+                            s['customer'], s['interface'])
+                    }
+                ]
+        return s
+
+    def _wanted_in_output(s):
+        return return_all or (s['status'].lower() == 'operational')
+
+    def _format_services(s):
+        return {
+            'id': s['id'],
+            'name': s['name'],
+            'customer': s['project'],
+            'speed': s['speed_value'],
+            'pop': s['here']['pop']['name'],
+            'hostname': common.ims_equipment_to_hostname(
+                s['here']['equipment']),
+            'interface': s['here']['port'].lower(),
+            'type': s['type'],
+            'status': s['status']
+        }
+
+    cache_key = f'classifier-cache:poller:services:{service_type}' \
+        if service_type else 'classifier-cache:poller:services:all-types'
+
+    if return_all:
+        cache_key = f'{cache_key}:all'
+    if include_snmp:
+        cache_key = f'{cache_key}:snmp'
+
+    redis = common.get_current_redis()
+    result = _ignore_cache_or_retrieve(request, cache_key, redis)
+
+    if not result:
+        result = _services()
+        result = filter(_wanted_in_output, result)
+        result = map(_format_services, result)
+        if include_snmp:
+            result = map(_add_snmp, result)
+        result = list(result)
+
+        if not result:
+            return None
+
+        # cache this data for the next call
+        result = json.dumps(result)
+        redis.set(cache_key, result)
+
+    return result
+
+
+@routes.route('/services', methods=['GET', 'POST'])
+@routes.route('/services/<service_type>', methods=['GET', 'POST'])
+@common.require_accepts_json
+def get_services(service_type=None):
+    """
+    Handler for `/poller/services</service-type>`
+
+    Use `/poller/service-types` for possible values of `service-type`.
+
+    If the endpoint is called with no `service-type`,
+    all services are returned.
+
+    Supported url parameters:
+
+    :all: Default is False to return only operational services.
+          If present and evaluates to True, then return all services.
+    :snmp: If present and evalutes to True, add snmp interface polling
+           params to the response.
+
+    (sphinx bug: the param names aren't capitalized)
+
+    The response will be formatted according to the following schema:
+
+    .. asjson::
+       inventory_provider.routes.poller.SERVICES_LIST_SCHEMA
+
+    A few possible values for `service_type` are (currently): gws_internal,
+    geant_ip, geant_lambda, l3_vpn, md_vpn_proxy (there are more)
+
+    If the endpoint is called with no `service_type`,
+    all services are returned
+
+    :return:
+    """
+
+    services_json_str = _get_services_internal(service_type)
+
+    if not services_json_str:
+        message = f'no {service_type} services found' \
+            if service_type else 'no services found'
+        return Response(
+            response=message,
+            status=404,
+            mimetype='text/html')
+
+    return Response(services_json_str, mimetype='application/json')
+
+
+@routes.route("/gws/indirect", methods=['GET', 'POST'])
+@common.require_accepts_json
+def gws_indirect():
+    """
+    Handler for `/poller/gws/indirect`
+
+    Same as `/poller/services/gws_indirect`
+
+    cf. :meth:`inventory_provider.routes.poller.get_services`
+    :return:
+    """
+    return get_services(service_type='gws_indirect')
+
+
+@routes.route('/service-types', methods=['GET', 'POST'])
+@common.require_accepts_json
+def get_service_types():
+    """
+    Handler for `/poller/service-types`
+
+    This method returns a list of all values of `service_type` that
+    can be used with `/poller/services/service-type` to return
+    a non-empty list of services.
+
+    Adding a truthy `all` request parameter (e.g. `?all=1`) will
+    return also return valid service types for which none of the
+    defined services are operational.
+
+    The response will be formatted according to the following schema:
+
+    .. asjson::
+       inventory_provider.routes.poller.STRING_LIST_SCHEMA
+
+    """
+    cache_key = 'classifier-cache:poller:service-types'
+
+    return_all = common.get_bool_request_arg('all', False)
+    if return_all:
+        cache_key = f'{cache_key}-all'
+
+    redis = common.get_current_redis()
+    service_types = _ignore_cache_or_retrieve(request, cache_key, redis)
+
+    if not service_types:
+        all_services = json.loads(_get_services_internal())
+        service_types = {
+            ims_sorted_service_type_key(s['type'])
+            for s in all_services
+        }
+
+        if not service_types:
+            return Response(
+                response='no service types found',
+                status=404,
+                mimetype='text/html')
+
+        # cache this data for the next call
+        service_types = sorted(list(service_types))
+        service_types = json.dumps(service_types)
+        redis.set(cache_key, service_types)
+
+    return Response(service_types, mimetype='application/json')
diff --git a/inventory_provider/tasks/common.py b/inventory_provider/tasks/common.py
index 427d51d8a18e4128f3dd3b174274e65fa17f5f8f..ad929ed4677cee9750f0b9b52c2846ecc26584d1 100644
--- a/inventory_provider/tasks/common.py
+++ b/inventory_provider/tasks/common.py
@@ -1,5 +1,6 @@
 import json
 import logging
+import re
 import time
 
 import jsonschema
@@ -218,3 +219,23 @@ def get_next_redis(config):
             'derived next id: {}'.format(next_id))
 
     return _get_redis(config, next_id)
+
+
+def ims_sorted_service_type_key(service_type):
+    """
+    special-purpose function used for mapping IMS service type strings
+    to the key used in redis ('ims:services:*')
+
+    this method is only used by
+    :meth:`inventory_provider:tasks:worker.update_circuit_hierarchy_and_port_id_services
+    and :meth:`inventory_provider:routes.poller.get_service_types
+
+    :param service_type: IMS-formatted service_type string
+    :return: ims sorted service type redis key
+    """  # noqa: E501
+
+    service_type = re.sub(
+        r'[^a-zA-Z0-9]+', '_', service_type.lower())
+    # prettification ... e.g. no trailing _ for 'MD-VPN (NATIVE)'
+    service_type = re.sub('_+$', '', service_type)
+    return re.sub('^_+', '', service_type)
diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py
index a36a5faf2214f572c9f0eca9170fea3782f51e84..544e10c098db1343ae249718c6011d50102b629b 100644
--- a/inventory_provider/tasks/worker.py
+++ b/inventory_provider/tasks/worker.py
@@ -29,7 +29,8 @@ from inventory_provider.db.ims import IMS
 from inventory_provider.tasks.app import app
 from inventory_provider.tasks.common \
     import get_next_redis, get_current_redis, \
-    latch_db, get_latch, set_latch, update_latch_status
+    latch_db, get_latch, set_latch, update_latch_status, \
+    ims_sorted_service_type_key
 from inventory_provider.tasks import monitor
 from inventory_provider import config
 from inventory_provider import environment
@@ -573,12 +574,14 @@ def update_circuit_hierarchy_and_port_id_services(self, use_current=False):
         {k: v for k, v in ims_data.get_customer_service_emails(ds1)}
     circuit_ids_to_monitor = \
         list(ims_data.get_monitored_circuit_ids(ds1))
+    additional_circuit_customer_ids = \
+        ims_data.get_circuit_related_customer_ids(ds1)
 
     hierarchy = None
     port_id_details = defaultdict(list)
     port_id_services = defaultdict(list)
     interface_services = defaultdict(list)
-    access_services = {}
+    services_by_type = {}
 
     def _convert_to_bits(value, unit):
         unit = unit.lower()
@@ -612,12 +615,18 @@ def update_circuit_hierarchy_and_port_id_services(self, use_current=False):
             else:
                 return 0
 
+    def _get_circuit_contacts(c):
+        customer_ids = {c['customerid']}
+        customer_ids.update(additional_circuit_customer_ids.get(c['id'], []))
+        return set().union(
+            *[customer_contacts.get(cid, []) for cid in customer_ids])
+
     def _populate_hierarchy():
         nonlocal hierarchy
         hierarchy = {}
         for d in ims_data.get_circuit_hierarchy(ds1):
-            d['contacts'] = customer_contacts.get(d['customerid'], [])
             hierarchy[d['id']] = d
+            d['contacts'] = sorted(list(_get_circuit_contacts(d)))
         logger.debug("hierarchy complete")
 
     def _populate_port_id_details():
@@ -671,7 +680,7 @@ def update_circuit_hierarchy_and_port_id_services(self, use_current=False):
                     'circuit_type': c['circuit-type'],
                     'service_type': c['product'],
                     'project': c['project'],
-                    'contacts': sorted(list(c['contacts']))
+                    'contacts': c['contacts']
                 }
                 if c['id'] in circuit_ids_to_monitor:
                     rs[c['id']]['status'] = c['status']
@@ -736,13 +745,7 @@ def update_circuit_hierarchy_and_port_id_services(self, use_current=False):
             circuits = port_id_services.get(details['port_id'], [])
 
             for circ in circuits:
-                contacts = set()
-                contacts.update(
-                    customer_contacts.get(
-                        circ['customerid'],
-                        []
-                    )
-                )
+                contacts = _get_circuit_contacts(circ)
                 circ['fibre-routes'] = []
                 for x in set(_get_fibre_routes(circ['id'])):
                     c = {
@@ -762,8 +765,9 @@ def update_circuit_hierarchy_and_port_id_services(self, use_current=False):
                 circ['calculated-speed'] = _get_speed(circ['id'])
                 _format_service(circ)
 
-                if circ['service_type'].lower() == 'geant ip':
-                    access_services[circ['id']] = circ
+                type_services = services_by_type.setdefault(
+                    ims_sorted_service_type_key(circ['service_type']), dict())
+                type_services[circ['id']] = circ
 
             interface_services[k].extend(circuits)
 
@@ -782,6 +786,8 @@ def update_circuit_hierarchy_and_port_id_services(self, use_current=False):
     rp = r.pipeline()
     for k in r.scan_iter('ims:access_services:*', count=1000):
         rp.delete(k)
+    for k in r.scan_iter('ims:gws_indirect:*', count=1000):
+        rp.delete(k)
     rp.execute()
     rp = r.pipeline()
     for circ in hierarchy.values():
@@ -797,19 +803,36 @@ def update_circuit_hierarchy_and_port_id_services(self, use_current=False):
 
     populate_poller_cache(interface_services, r)
 
-    for v in access_services.values():
-        rp.set(
-            f'ims:access_services:{v["name"]}',
-            json.dumps({
-                'id': v['id'],
-                'name': v['name'],
-                'pop_name': v['pop_name'],
-                'other_end_pop_name': v['other_end_pop_name'],
-                'equipment': v['equipment'],
-                'other_end_equipment': v['other_end_equipment'],
-                'speed_value': v['calculated-speed'],
-                'speed_unit': 'n/a'
-            }))
+    for service_type, services in services_by_type.items():
+        for v in services.values():
+            rp.set(
+                f'ims:services:{service_type}:{v["name"]}',
+                json.dumps({
+                    'id': v['id'],
+                    'name': v['name'],
+                    'project': v['project'],
+                    'here': {
+                        'pop': {
+                            'name': v['pop_name'],
+                            'abbreviation': v['pop_abbreviation']
+                        },
+                        'equipment': v['equipment'],
+                        'port': v['port'],
+                    },
+                    'there': {
+                        'pop': {
+                            'name': v['other_end_pop_name'],
+                            'abbreviation': v['other_end_pop_abbreviation']
+                        },
+                        'equipment': v['other_end_equipment'],
+                        'port': v['other_end_port'],
+                    },
+                    'speed_value': v['calculated-speed'],
+                    'speed_unit': 'n/a',
+                    'status': v['status'],
+                    'type': v['service_type']
+                }))
+
     rp.execute()
 
 
diff --git a/requirements.txt b/requirements.txt
index dac8c3c31d676c037b165cb45c2f13b04f8815e7..949b83d8a8781bf7ceb7685c45d48667aa95a358 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@ pysnmp
 jsonschema==3.2.0
 paramiko
 flask
+flask-cors
 redis==3.2.1
 kombu==4.5.0
 vine==1.3.0
diff --git a/setup.py b/setup.py
index cf2c00e3b7ffd27eaeefce6a6508f7fce1c4c8a1..e7c146e3351fff602b48eb5317b9a2d4eb2306ae 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='inventory-provider',
-    version="0.66",
+    version="0.67",
     author='GEANT',
     author_email='swd@geant.org',
     description='Dashboard inventory provider',
@@ -16,6 +16,7 @@ setup(
         'jsonschema==3.2.0',
         'paramiko',
         'flask',
+        'flask-cors',
         'redis==3.2.1',
         'kombu==4.5.0',
         'vine==1.3.0',
diff --git a/test/data/gws-direct.json b/test/data/gws-direct.json
index 8fcb50564dd4687b74ff105e62423d75501e999c..db422736b59a697fda797b4a1f9390fc8f430ecd 100644
--- a/test/data/gws-direct.json
+++ b/test/data/gws-direct.json
@@ -218,7 +218,7 @@
   },
   {
     "nren": "RoEduNet",
-    "isp": "Century Link",
+    "isp": "CenturyLink",
     "hosts": [
       {
         "hostname": "212.162.45.194",
@@ -256,7 +256,7 @@
   },
   {
     "nren": "PSNC",
-    "isp": "Century Link",
+    "isp": "CenturyLink",
     "hosts": [
       {
         "hostname": "212.191.126.6",
diff --git a/test/data/router-info.json b/test/data/router-info.json
index 14e59ab1c12192c95f1949146c5e44857b6fedb3..fdb6beb945efaf539dabc43b823580b731f7ae05 100644
Binary files a/test/data/router-info.json and b/test/data/router-info.json differ
diff --git a/test/data/update-test-db.py b/test/data/update-test-db.py
index 1567466fe81c46534aa5f293e66f9e9a92c6ccb6..b615e898b76227081fdc86b160cdd92451bb247d 100644
--- a/test/data/update-test-db.py
+++ b/test/data/update-test-db.py
@@ -107,11 +107,10 @@ if __name__ == '__main__':
             #     ]
             # },
             {
-                'hostname': 'localhost',
+                'hostname': 'test-dashboard-storage03.geant.org',
                 'db-index': 0,
                 'key-patterns': [
-                    'ims:interface_services*',
-                    'ims:circuit_hierarchy*',
+                    '*'
                 ]
             },
         ]
diff --git a/test/test_general_poller_routes.py b/test/test_general_poller_routes.py
index 7a7807c6053c9522a6fdaba7c2b176a0f5c51f2c..e17d8f3f6c1cbd2438020a7b21740bf7390bd05b 100644
--- a/test/test_general_poller_routes.py
+++ b/test/test_general_poller_routes.py
@@ -1,5 +1,6 @@
 import json
 import jsonschema
+import pytest
 from inventory_provider.routes import poller
 
 DEFAULT_REQUEST_HEADERS = {
@@ -19,6 +20,21 @@ def test_get_all_interfaces(client):
     response_routers = {ifc['router'] for ifc in response_data}
     assert len(response_routers) > 1, \
         'there should data from be lots of routers'
+    assert any('.lab.' in name for name in response_routers)
+
+
+def test_get_all_interfaces_no_lab(client):
+    rv = client.get(
+        '/poller/interfaces?no-lab=1',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    response_data = json.loads(rv.data.decode('utf-8'))
+    jsonschema.validate(response_data, poller.INTERFACE_LIST_SCHEMA)
+    response_routers = {ifc['router'] for ifc in response_data}
+    assert len(response_routers) > 1, \
+        'there should data from be lots of routers'
+    assert all('.lab.' not in name for name in response_routers)
 
 
 def test_all_router_interface_speeds(client):
@@ -62,3 +78,140 @@ def test_gws_direct(client):
     response_data = json.loads(rv.data.decode('utf-8'))
     jsonschema.validate(response_data, poller.GWS_DIRECT_DATA_SCHEMA)
     assert response_data, "the subscription list shouldn't be empty"
+
+
+def test_gws_indirect(client):
+    rv = client.get(
+        '/poller/gws/indirect',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    response_data = json.loads(rv.data.decode('utf-8'))
+    jsonschema.validate(response_data, poller.SERVICES_LIST_SCHEMA)
+
+    assert response_data  # test data is non-empty
+    assert all(s['type'] == 'GWS - INDIRECT' for s in response_data)
+
+
+def test_gws_indirect_snmp(client):
+    # same as test_services_snmp, but also verify that
+    # the snmp data in the response contains counters
+    rv = client.get(
+        '/poller/services/gws_indirect?snmp=1',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    response_data = json.loads(rv.data.decode('utf-8'))
+    jsonschema.validate(response_data, poller.SERVICES_LIST_SCHEMA)
+
+    assert response_data  # test data is non-empty
+    # all access services should have snmp info
+    assert all('snmp' in s for s in response_data)
+    assert all('counters' in s['snmp'] for s in response_data)
+
+
+def test_all_services_default(client):
+    rv = client.get(
+        '/poller/services',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    response_data = json.loads(rv.data.decode('utf-8'))
+    jsonschema.validate(response_data, poller.SERVICES_LIST_SCHEMA)
+
+    assert response_data  # test data is non-empty
+    assert all(s['status'] == 'operational' for s in response_data)
+
+
+def test_all_services_all(client):
+    rv = client.get(
+        '/poller/services?all=1',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    response_data = json.loads(rv.data.decode('utf-8'))
+    jsonschema.validate(response_data, poller.SERVICES_LIST_SCHEMA)
+
+    assert response_data  # test data is non-empty
+    assert any(s['status'] != 'operational' for s in response_data)
+    assert all('snmp' not in s for s in response_data)
+
+
+@pytest.mark.parametrize('service_type', [
+    # services with these types should all have snmp configs
+    'gws_indirect', 'geant_ip', 'l3_vpn'])
+def test_services_snmp(client, service_type):
+
+    rv = client.get(
+        f'/poller/services/{service_type}?snmp=1',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    response_data = json.loads(rv.data.decode('utf-8'))
+    jsonschema.validate(response_data, poller.SERVICES_LIST_SCHEMA)
+
+    assert response_data  # test data is non-empty
+    # all access services should have snmp info
+    assert all('snmp' in s for s in response_data)
+
+
+@pytest.mark.parametrize('uri_type,expected_type', [
+    ('gws_indirect', 'GWS - INDIRECT'),
+    ('gws_upstream', 'GWS - UPSTREAM'),
+    ('geant_ip', 'GEANT IP'),
+    ('geant_lambda', 'GEANT LAMBDA'),
+    # ('gts', 'GTS'),  # these are non-monitored (TODO: make a flag?)
+    ('md_vpn_native', 'MD-VPN (NATIVE)'),
+    ('md_vpn_proxy', 'MD-VPN (PROXY)'),
+    ('cbl1', 'CBL1'),
+    ('l3_vpn', 'L3-VPN')
+])
+def test_all_services_by_type(client, uri_type, expected_type):
+    rv = client.get(
+        f'/poller/services/{uri_type}',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    response_data = json.loads(rv.data.decode('utf-8'))
+    jsonschema.validate(response_data, poller.SERVICES_LIST_SCHEMA)
+
+    assert response_data  # test data is non-empty
+    assert all(s['type'] == expected_type for s in response_data)
+
+
+def test_get_service_types(client):
+    rv = client.get(
+        '/poller/service-types',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    response_data = json.loads(rv.data.decode('utf-8'))
+    jsonschema.validate(response_data, poller.STRING_LIST_SCHEMA)
+
+    assert response_data  # test data is non-empty
+    # some hard-coded values we expect to see from the test data ...
+    assert 'gws_indirect' in response_data
+    assert 'geant_ip' in response_data
+
+
+@pytest.mark.parametrize('ifIndex,expected_oid', [
+    (595, '1.3.6.1.4.1.2636.3.6.2.1.5.595.1.6.100.119.115.45.105.110'),
+    (607, '1.3.6.1.4.1.2636.3.6.2.1.5.607.1.6.100.119.115.45.105.110'),
+    (1135, '1.3.6.1.4.1.2636.3.6.2.1.5.1135.1.6.100.119.115.45.105.110')
+])
+def test_dcu_oid_values(ifIndex, expected_oid):
+    assert poller._jnx_dcu_byte_count_oid(ifIndex) == expected_oid
+
+
+@pytest.mark.parametrize('customer, interface_name, expected_oid', [
+    ('ASREN', 'ae17.333', '1.3.6.1.4.1.2636.3.5.2.1.5.29.110.114.101.110.95.73.65.83.95.65.83.82.69.78.95.79.85.84.45.97.101.49.55.46.51.51.51.45.111.18.68.87.83.45.111.117.116.45.97.101.49.55.46.51.51.51.45.111.2'),  # noqa: E501
+    ('FCCN', 'ae10.333', '1.3.6.1.4.1.2636.3.5.2.1.5.28.110.114.101.110.95.73.65.83.95.70.67.67.78.95.79.85.84.45.97.101.49.48.46.51.51.51.45.111.18.68.87.83.45.111.117.116.45.97.101.49.48.46.51.51.51.45.111.2'),  # noqa: E501
+    ('GRNET', 'ae11.333', '1.3.6.1.4.1.2636.3.5.2.1.5.29.110.114.101.110.95.73.65.83.95.71.82.78.69.84.95.79.85.84.45.97.101.49.49.46.51.51.51.45.111.18.68.87.83.45.111.117.116.45.97.101.49.49.46.51.51.51.45.111.2'),  # noqa: #E501
+    ('GRNET', 'ae10.333', '1.3.6.1.4.1.2636.3.5.2.1.5.29.110.114.101.110.95.73.65.83.95.71.82.78.69.84.95.79.85.84.45.97.101.49.48.46.51.51.51.45.111.18.68.87.83.45.111.117.116.45.97.101.49.48.46.51.51.51.45.111.2'),  # noqa: #E501
+    ('ULAKBIM', 'ae11.333', '1.3.6.1.4.1.2636.3.5.2.1.5.31.110.114.101.110.95.73.65.83.95.85.76.65.75.66.73.77.95.79.85.84.45.97.101.49.49.46.51.51.51.45.111.18.68.87.83.45.111.117.116.45.97.101.49.49.46.51.51.51.45.111.2'),  # noqa: E501
+    ('UOM', 'xe-11/0/0.333', '1.3.6.1.4.1.2636.3.5.2.1.5.32.110.114.101.110.95.73.65.83.95.85.79.77.95.79.85.84.45.120.101.45.49.49.47.48.47.48.46.51.51.51.45.111.23.68.87.83.45.111.117.116.45.120.101.45.49.49.47.48.47.48.46.51.51.51.45.111.2'),  # noqa: E501
+    ('LITNET', 'xe-0/1/1.333', '1.3.6.1.4.1.2636.3.5.2.1.5.34.110.114.101.110.95.73.65.83.95.76.73.84.78.69.84.95.79.85.84.45.120.101.45.48.47.49.47.49.46.51.51.51.45.111.22.68.87.83.45.111.117.116.45.120.101.45.48.47.49.47.49.46.51.51.51.45.111.2'),  # noqa: E501
+])
+def test_fw_counter_bytes_oid_values(customer, interface_name, expected_oid):
+    assert poller._jnx_fw_counter_bytes_oid(
+        customer, interface_name) == expected_oid
diff --git a/test/test_msr_routes.py b/test/test_msr_routes.py
index 403321060059c7e5895a388ba8317081f86362db..3788bdeb08a03adcb7e1dba504363bffcaab9b99 100644
--- a/test/test_msr_routes.py
+++ b/test/test_msr_routes.py
@@ -4,7 +4,8 @@ import jsonschema
 import pytest
 
 from inventory_provider.routes.msr import PEERING_LIST_SCHEMA, \
-    PEERING_GROUP_LIST_SCHEMA, ACCESS_SERVICES_LIST_SCHEMA
+    PEERING_GROUP_LIST_SCHEMA
+from inventory_provider.routes.poller import SERVICES_LIST_SCHEMA
 
 DEFAULT_REQUEST_HEADERS = {
     "Content-type": "application/json",
@@ -13,18 +14,16 @@ DEFAULT_REQUEST_HEADERS = {
 
 
 def test_access_services(client):
-
-    # todo - fix once IMS msr code is done
-
     rv = client.get(
         '/msr/access-services',
         headers=DEFAULT_REQUEST_HEADERS)
     assert rv.status_code == 200
     assert rv.is_json
     response_data = json.loads(rv.data.decode('utf-8'))
-    jsonschema.validate(response_data, ACCESS_SERVICES_LIST_SCHEMA)
+    jsonschema.validate(response_data, SERVICES_LIST_SCHEMA)
 
     assert response_data  # test data is non-empty
+    assert all(s['type'] == 'GEANT IP' for s in response_data)
 
 
 def test_logical_system_peerings_all(client):