diff --git a/inventory_provider/db/opsdb.py b/inventory_provider/db/opsdb.py
index e41748fd1c7472fdc2ac59914ad3c73d3108a55f..07a24e275ca2aa107ddcfc1199fee64292ac4745 100644
--- a/inventory_provider/db/opsdb.py
+++ b/inventory_provider/db/opsdb.py
@@ -1,5 +1,11 @@
+import logging
+import re
+from collections import defaultdict
+
 from inventory_provider.db import db
 
+logger = logging.getLogger(__name__)
+
 
 def _convert_to_dict(crs):
     return [dict((crs.description[i][0], "" if value is None else value)
@@ -109,6 +115,65 @@ WHERE
     return r
 
 
+def get_fibre_spans(connection):
+
+    _sql = """
+SELECT c.absid, c.name,
+parent.absid parent_absid, parent.name parent_name,
+parent.status parent_status, LOWER(parent.circuit_type) parent_type,
+pa.name pop_a, pa.abbreviation pop_abbr_a,
+ea.name equipment_a, LOWER(ea.type) eq_type_a,
+pb.name pop_b, pb.abbreviation pop_abbr_b,
+eb.name equipment_b, LOWER(eb.type) eq_type_b
+FROM vcircuitconns c
+INNER JOIN pop pa ON pa.absid = c.PTR_pop_a
+INNER JOIN pop pb ON pb.absid = c.PTR_pop_b
+INNER JOIN equipment ea ON ea.absid = c.PTR_equip_a
+INNER JOIN equipment eb ON eb.absid = c.PTR_equip_b
+INNER JOIN circuit_glue cg ON c.absid = cg.PTR_component
+INNER JOIN circuit parent ON parent.absid = cg.PTR_circuit
+WHERE
+c.is_circuit = 1 AND c.status != 'terminated' AND parent.status != 'terminated'
+AND c.circuit_type = 'fibre span'
+"""
+
+    ne_details = {}
+    with db.cursor(connection) as crs:
+        crs.execute(_sql)
+        rows = _convert_to_dict(crs)
+    for row in rows:
+        if row['parent_type'] != 'fibre route':
+            logger.debug(f'Wrong Parent Type c: {row["absid"]} '
+                         f'p: {row["parent_absid"]} {row["parent_type"]}')
+            continue
+        ne_pattern = r'.+-(OLA|DTNX)\d+-\d.*'
+        ne_a_match = re.match(ne_pattern, row['equipment_a'])
+        ne_b_match = re.match(ne_pattern, row['equipment_b'])
+        if ne_a_match:
+            ne_details[f'{row["equipment_a"]}_{row["parent_absid"]}'] = {
+                'ne': row['equipment_a'],
+                'df_route': row['parent_name'],
+                'df_route_id': row['parent_absid'],
+                'df_status': row['parent_status'],
+                'pop': row['pop_a'],
+                'pop_abbreviation': row['pop_abbr_a'],
+            }
+        if ne_b_match:
+            ne_details[f'{row["equipment_b"]}_{row["parent_absid"]}'] = {
+                'ne': row['equipment_b'],
+                'df_route': row['parent_name'],
+                'df_route_id': row['parent_absid'],
+                'df_status': row['parent_status'],
+                'pop': row['pop_b'],
+                'pop_abbreviation': row['pop_abbr_b']
+            }
+    by_ne = defaultdict(lambda: [])
+    for d in ne_details.values():
+        by_ne[d['ne']].append(d)
+
+    yield from by_ne.items()
+
+
 def get_circuits(connection):
     _sql = """
 SELECT *
diff --git a/inventory_provider/routes/classifier.py b/inventory_provider/routes/classifier.py
index f11e22692d08fa313a2fdf0b1a1a74e5507c042e..ba9f31a365bb86cea64771353215471e8726a906 100644
--- a/inventory_provider/routes/classifier.py
+++ b/inventory_provider/routes/classifier.py
@@ -1,4 +1,5 @@
 import ipaddress
+import itertools
 import json
 import logging
 import re
@@ -412,6 +413,72 @@ def get_trap_metadata(source_equipment, interface, circuit_id):
     return Response(result, mimetype="application/json")
 
 
+@routes.route("/infinera-fiberlink-info/<ne_name_str>/<object_name_str>",
+              methods=['GET', 'POST'])
+@common.require_accepts_json
+def get_fiberlink_trap_metadata(ne_name_str, object_name_str):
+    objects = object_name_str.split('_')
+    shelves = [x.split('-')[0] for x in objects]
+    p = r'([a-zA-Z\d]+?-(OLA|DTNX)\d+(-\d)?)'
+    matches = re.findall(p, ne_name_str)
+    if len(matches) != 2 or len(shelves) != 2:
+        raise ClassifierProcessingError(
+            f'unable to parse {ne_name_str} {object_name_str } '
+            'into two elements')
+
+    r = common.get_current_redis()
+
+    # double check that we only need to check the two nodes and not the objects
+    cache_key = f'classifier-cache:fiberlink:{ne_name_str}:{object_name_str}'
+    result = r.get(cache_key)
+
+    if result:
+        result = result.decode('utf-8')
+    else:
+        nes_a = f'{matches[0][0]}-{shelves[0]}'
+        nes_b = f'{matches[1][0]}-{shelves[1]}'
+        result = []
+        df_a = r.get(f'opsdb:ne_fibre_spans:{nes_a}')
+        df_b = r.get(f'opsdb:ne_fibre_spans:{nes_b}')
+        if df_a and df_b:
+            a = json.loads(df_a.decode('utf-8'))
+            b = json.loads(df_b.decode('utf-8'))
+
+            matches = [x for x in itertools.product(a, b) if
+                       x[0]['df_route_id'] == x[1]['df_route_id']]
+            if matches:
+                match = matches[0]
+                result = {
+                    'ends': {
+                        'a': {
+                            'pop': match[0]['pop'],
+                            'pop_abbreviation': match[0]['pop_abbreviation'],
+                        },
+                        'b': {
+                            'pop': match[1]['pop'],
+                            'pop_abbreviation': match[1]['pop_abbreviation'],
+                        },
+                    },
+                    'df_route': {
+                        'id': match[0]['df_route_id'],
+                        'name': match[0]['df_route'],
+                        'status': match[0]['df_status'],
+                    },
+                    'related-services':
+                        get_top_level_services(match[0]['df_route_id'], r)
+                }
+                result = json.dumps(result)
+        r.set(cache_key, result)
+        if not result:
+            return Response(
+                response="no available info for "
+                         f"{ne_name_str} {object_name_str}",
+                status=404,
+                mimetype="text/html")
+
+    return Response(result, mimetype="application/json")
+
+
 @routes.route('/coriant-info/<equipment_name>/<path:entity_string>',
               methods=['GET', 'POST'])
 @common.require_accepts_json
diff --git a/inventory_provider/routes/testing.py b/inventory_provider/routes/testing.py
index 60073eae1eb16aa4ebc146202002a31a74979101..6f9ce50e67bdb58aec172e2276b64fcd883e77f1 100644
--- a/inventory_provider/routes/testing.py
+++ b/inventory_provider/routes/testing.py
@@ -41,6 +41,12 @@ def update_geant_lambdas():
     return Response('OK')
 
 
+@routes.route("update-fibre-spans", methods=['GET', 'POST'])
+def update_fibre_spans():
+    worker.update_fibre_spans.delay()
+    return Response('OK')
+
+
 @routes.route("update-service-hierarchy")
 def update_service_hierarchy():
     worker.update_circuit_hierarchy.delay()
diff --git a/inventory_provider/tasks/worker.py b/inventory_provider/tasks/worker.py
index 9189a9cd0b8552d9604f03030256d44bca9f4e5f..944fc2bb8912a9746078aee2215fcc70954add87 100644
--- a/inventory_provider/tasks/worker.py
+++ b/inventory_provider/tasks/worker.py
@@ -311,6 +311,26 @@ def update_geant_lambdas(self):
         rp.execute()
 
 
+@app.task(base=InventoryTask, bind=True, name='update_fibre_spans')
+@log_task_entry_and_exit
+def update_fibre_spans(self):
+    r = get_next_redis(InventoryTask.config)
+    rp = r.pipeline()
+    # scan with bigger batches, to mitigate network latency effects
+    for key in r.scan_iter('opsdb:ne_fibre_spans:*', count=1000):
+        rp.delete(key)
+    rp.execute()
+
+    with db.connection(InventoryTask.config["ops-db"]) as cx:
+        rp = r.pipeline()
+        for ne, fs in opsdb.get_fibre_spans(cx):
+
+            rp.set(
+                f'opsdb:ne_fibre_spans:{ne}',
+                json.dumps(fs))
+        rp.execute()
+
+
 @app.task(base=InventoryTask, bind=True,
           name='update_neteng_managed_device_list')
 @log_task_entry_and_exit
diff --git a/test/test_classifier_routes.py b/test/test_classifier_routes.py
index caee5904d1e17a9f09d3b0f681c05a19c36d31d2..aaef9369e7914ae3032073bdc4389e5ceff4cb5f 100644
--- a/test/test_classifier_routes.py
+++ b/test/test_classifier_routes.py
@@ -461,3 +461,69 @@ def test_coriant_info_not_found(client, mocker):
         headers=DEFAULT_REQUEST_HEADERS)
 
     assert rv.status_code == 404
+
+
+def test_infinera_unparseable_fiberlink(client):
+    rv = client.get(
+        '/classifier/infinera-fiberlink-info/unparseableentitystring/aaa',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 422
+
+    rv = client.get(
+        '/classifier/infinera-fiberlink-info/XXX-OLA1-XXX02-DTNX10-1/aaa',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 422
+
+
+def test_infinera_fiberlink_not_found(client):
+    rv = client.get(
+        '/classifier/infinera-fiberlink-info/'
+        'XXX-OLA1-XXX02-DTNX10-1/1-A-2-L1_3-A-2-L1',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 404
+
+
+def test_infinera_fiberlink(client, mocker):
+    # mocker.patch('inventory_provider.routes.classifier.json.dumps')
+    mocked_redis = mocker.patch(
+        'inventory_provider.routes.classifier.common.get_current_redis')
+    mg = mocked_redis.return_value.get
+    mg.side_effect = [
+        False,
+        b"[{\"ne\": \"XXX-OLA1-1\", \"df_route\": \"end1-end2-dfroute\", \"df_route_id\": 1234, \"df_status\": \"Operational\", \"pop\": \"POP 1\", \"pop_abbreviation\": \"p1\"}]",  # noqa
+        b"[{\"ne\": \"XXX02-DTNX10-1-3\", \"df_route\": \"end1-end2-dfroute\", \"df_route_id\": 1234, \"df_status\": \"Operational\", \"pop\": \"POP 2\", \"pop_abbreviation\": \"p2\"}]",  # noqa
+        ]
+    mocked_tls = mocker.patch(
+        'inventory_provider.routes.classifier.get_top_level_services')
+    mocked_tls.return_value = {'a': 'A'}
+    rv = client.get(
+        '/classifier/infinera-fiberlink-info/'
+        'XXX-OLA1-XXX02-DTNX10-1/1-A-2-L1_3-A-2-L1',
+        headers=DEFAULT_REQUEST_HEADERS)
+    mg.assert_any_call(
+        'classifier-cache:fiberlink:XXX-OLA1-XXX02-DTNX10-1:1-A-2-L1_3-A-2-L1')
+    mg.assert_any_call('opsdb:ne_fibre_spans:XXX-OLA1-1')
+    mg.assert_any_call('opsdb:ne_fibre_spans:XXX02-DTNX10-1-3')
+    mocked_tls.assert_called_with(1234, mocker.ANY)
+
+    expected = {
+        'ends': {
+            'a': {
+                'pop': 'POP 1',
+                'pop_abbreviation': 'p1',
+            },
+            'b': {
+                'pop': 'POP 2',
+                'pop_abbreviation': 'p2',
+            },
+        },
+        'df_route': {
+            'id': 1234,
+            'name': 'end1-end2-dfroute',
+            'status': 'Operational',
+        },
+        'related-services': {'a': 'A'}
+    }
+
+    assert rv.status_code == 200
+    assert rv.get_data(as_text=True) == json.dumps(expected)