diff --git a/changelog b/changelog
index b2af90b8e90a1bca48685ffcee83ee4da2b6deb1..52a582c38c54e861fbbf23e7e8a3d3e50d457429 100644
--- a/changelog
+++ b/changelog
@@ -13,3 +13,4 @@
       logging levels configured from environment
 0.12: added addresses to interface response
       put actual module number in version response
+0.13: added external inventory caching
diff --git a/inventory_provider/alarmsdb.py b/inventory_provider/alarmsdb.py
index 164cbbdb92b4b4de22a79b79ba5d8c47c04591a6..b3256e10bb1b60c3511d6942dff4f061ecd75ed1 100644
--- a/inventory_provider/alarmsdb.py
+++ b/inventory_provider/alarmsdb.py
@@ -1,40 +1,13 @@
-import contextlib
-import mysql.connector
+from inventory_provider import db
 
 
-@contextlib.contextmanager
-def connection(alarmsdb):  # pragma: no cover
-    cx = None
-    try:
-        cx = mysql.connector.connect(
-            host=alarmsdb["hostname"],
-            user=alarmsdb["username"],
-            passwd=alarmsdb["password"],
-            db=alarmsdb["dbname"])
-        yield cx
-    finally:
-        if cx:
-            cx.close()
-
-
-@contextlib.contextmanager
-def cursor(cnx):  # pragma: no cover
-    csr = None
-    try:
-        csr = cnx.cursor()
-        yield csr
-    finally:
-        if csr:
-            csr.close()
-
-
-def get_last_known_infinera_interface_status(db, equipment, interface):
+def get_last_known_infinera_interface_status(connection, equipment, interface):
     query = "SELECT status FROM infinera_alarms" \
             " WHERE" \
             " CONCAT(ne_name, '-', REPLACE(object_name, 'T', '')) = %s" \
             " ORDER BY ne_init_time DESC, ne_clear_time DESC LIMIT 1"
     search_string = equipment + "-" + interface
-    with cursor(db) as crs:
+    with db.cursor(connection) as crs:
         crs.execute(query, (search_string,))
         result = crs.fetchone()
     if not result:
@@ -45,11 +18,11 @@ def get_last_known_infinera_interface_status(db, equipment, interface):
         return "up"
 
 
-def get_last_known_coriant_interface_status(db, equipment, interface):
+def get_last_known_coriant_interface_status(connection, equipment, interface):
     query = "SELECT status FROM coriant_alarms" \
             " WHERE ne_id_name = %s AND entity_string LIKE %s" \
             " ORDER BY last_event_time DESC LIMIT 1"
-    with cursor(db) as crs:
+    with db.cursor(connection) as crs:
         crs.execute(query, (equipment, interface + "-%"))
         result = crs.fetchone()
     if not result:
@@ -60,12 +33,13 @@ def get_last_known_coriant_interface_status(db, equipment, interface):
         return "up"
 
 
-def get_last_known_juniper_link_interface_status(db, equipment, interface):
+def get_last_known_juniper_link_interface_status(
+        connection, equipment, interface):
     query = "SELECT IF(link_admin_status = 'up'" \
             " AND link_oper_status = 'up', 1, 0) AS up FROM juniper_alarms" \
             " WHERE equipment_name = %s AND link_interface_name = %s" \
             " ORDER BY alarm_id DESC LIMIT 1"
-    with cursor(db) as crs:
+    with db.cursor(connection) as crs:
         crs.execute(query, ('lo0.' + equipment, interface))
         result = crs.fetchone()
     if not result:
@@ -76,13 +50,13 @@ def get_last_known_juniper_link_interface_status(db, equipment, interface):
         return "up"
 
 
-def get_last_known_interface_status(db, equipment, interface):
+def get_last_known_interface_status(connection, equipment, interface):
     result = get_last_known_infinera_interface_status(
-        db, equipment, interface)
+        connection, equipment, interface)
     if result == "unknown":
         result = get_last_known_coriant_interface_status(
-                db, equipment, interface)
+            connection, equipment, interface)
     if result == "unknown":
         result = get_last_known_juniper_link_interface_status(
-            db, equipment, interface)
+            connection, equipment, interface)
     return result
diff --git a/inventory_provider/db.py b/inventory_provider/db.py
new file mode 100644
index 0000000000000000000000000000000000000000..2de869b3bb39b7acaa46a624465b026934c228a4
--- /dev/null
+++ b/inventory_provider/db.py
@@ -0,0 +1,41 @@
+import contextlib
+import mysql.connector
+import redis
+
+from flask import current_app, g
+
+
+def get_redis():  # pragma: no cover
+    if 'redis_db' not in g:
+        config = current_app.config['INVENTORY_PROVIDER_CONFIG']
+        g.redis_db = redis.Redis(
+            host=config['redis']['hostname'],
+            port=config['redis']['port'])
+
+    return g.redis_db
+
+
+@contextlib.contextmanager
+def connection(db_params):
+    cx = None
+    try:
+        cx = mysql.connector.connect(
+            host=db_params["hostname"],
+            user=db_params["username"],
+            passwd=db_params["password"],
+            db=db_params["dbname"])
+        yield cx
+    finally:
+        if cx:
+            cx.close()
+
+
+@contextlib.contextmanager
+def cursor(cnx):  # pragma: no cover
+    csr = None
+    try:
+        csr = cnx.cursor()
+        yield csr
+    finally:
+        if csr:
+            csr.close()
diff --git a/inventory_provider/opsdb.py b/inventory_provider/opsdb.py
index 47fe94f0ab8e3f358e85c7e077d5d73990105231..c0a6417abbc10b803ba4ee39053d8e1498f64533 100644
--- a/inventory_provider/opsdb.py
+++ b/inventory_provider/opsdb.py
@@ -1,43 +1,231 @@
-import contextlib
-import logging
+from inventory_provider import db
 
-import mysql.connector
 
-from inventory_provider.constants import DATABASE_LOGGER_NAME
+equipment_location_query = """SELECT
+                          e.absid,
+                          e.name AS equipment_name,
+                          p.name AS pop_name,
+                          p.abbreviation AS pop_abbreviation,
+                          p.site_id AS pop_site_id,
+                          p.country,
+                          g.longitude,
+                          g.latitude
+                        FROM
+                          equipment e
+                        INNER JOIN pop p
+                          ON p.absid = e.PTR_pop
+                        INNER JOIN geocoding g
+                          ON g.absid = p.PTR_geocoding
+                        WHERE
+                          e.status != 'terminated'
+                          AND e.status != 'disposed'"""
 
 
-@contextlib.contextmanager
-def connection(opsdb):  # pragma: no cover
-    cx = None
+circuit_hierarchy_query = """SELECT
+                       pc.name AS parent_circuit,
+                       pc.absid AS parent_circuit_id,
+                       LOWER(pc.status) AS parent_circuit_status,
+                       cc.name AS child_circuit,
+                       cc.absid AS child_circuit_id,
+                       LOWER(cc.status) AS child_circuit_status,
+                       cg.segment_group AS segment_group
+                     FROM circuit_glue cg
+                     INNER JOIN circuit pc ON pc.absid = cg.PTR_circuit
+                     INNER JOIN circuit cc ON cc.absid = cg.PTR_component"""
+
+
+retrieve_services_query = """SELECT *
+                    FROM (SELECT
+                      c.absid AS id,
+                      c.name,
+                      LOWER(c.status) AS status,
+                      LOWER(c.circuit_type) AS circuit_type,
+                      LOWER(c.service_type) AS service_type,
+                      events.short_descr AS project,
+                      e.name AS equipment,
+                      cc.port_a AS port,
+                      cc.int_LU_a AS logical_unit,
+                      LOWER(o.name) AS manufacturer,
+                      LOWER(ec.card_id) AS card_id,
+                      LOWER(
+                        IF(pp.interface_name IS NULL,
+                        '', pp.interface_name)) AS interface_name
+                    FROM circuit c
+                      INNER JOIN circuit_connections cc
+                        ON cc.circ_absid = c.absid
+                      INNER JOIN equipment e
+                        ON e.absid = cc.PTR_equip_a
+                      LEFT JOIN events
+                        ON events.absid = cc.PTR_project
+                      INNER JOIN equipment_card ec
+                        ON ec.absid = cc.PTR_card_a
+                      LEFT JOIN organisation o
+                        ON o.absid = ec.manufacturer
+                      LEFT JOIN port_plugin pp
+                        ON pp.PTR_card = cc.PTR_card_a AND pp.port = cc.port_a
+                    WHERE c.status != 'terminated' AND is_circuit = 1
+                    UNION
+                    SELECT
+                      c.absid AS id,
+                      c.name,
+                      LOWER(c.status) AS status,
+                      LOWER(c.circuit_type) AS circuit_type,
+                      LOWER(c.service_type) AS service_type,
+                      events.short_descr AS project,
+                      e.name AS equipment,
+                      cc.port_b AS port,
+                      cc.int_LU_b AS logical_unit,
+                      LOWER(o.name) AS manufacturer,
+                      LOWER(ec.card_id) AS card_id,
+                      LOWER(
+                        IF(pp.interface_name IS NULL,
+                        '', pp.interface_name)) AS interface_name
+                    FROM circuit c
+                      INNER JOIN circuit_connections cc
+                        ON cc.circ_absid = c.absid
+                      INNER JOIN equipment e
+                        ON e.absid = cc.PTR_equip_b
+                      LEFT JOIN events
+                        ON events.absid = cc.PTR_project
+                      INNER JOIN equipment_card ec
+                        ON ec.absid = cc.PTR_card_b
+                      LEFT JOIN organisation o
+                        ON o.absid = ec.manufacturer
+                      LEFT JOIN port_plugin pp
+                        ON pp.PTR_card = cc.PTR_card_b AND pp.port = cc.port_b
+                    WHERE c.status != 'terminated' AND is_circuit = 1
+                    UNION
+                    SELECT
+                      c.absid AS id,
+                      c.name,
+                      LOWER(c.status) AS status,
+                      LOWER(c.circuit_type) AS circuit_type,
+                      LOWER(c.service_type) AS service_type,
+                      events.short_descr AS project,
+                      e.name AS equipment,
+                      cc.port_a_OUT AS port,
+                      cc.int_LU_a AS logical_unit,
+                      LOWER(o.name) AS manufacturer,
+                      LOWER(ec.card_id) AS card_id,
+                      LOWER(
+                        IF(pp.interface_name IS NULL,
+                        '', pp.interface_name)) AS interface_name
+                    FROM circuit c
+                      INNER JOIN circuit_connections cc
+                        ON cc.circ_absid = c.absid
+                      INNER JOIN equipment e
+                        ON e.absid = cc.PTR_equip_a
+                      LEFT JOIN events
+                        ON events.absid = cc.PTR_project
+                      INNER JOIN equipment_card ec
+                        ON ec.absid = cc.PTR_card_a_OUT
+                      LEFT JOIN organisation o
+                        ON o.absid = ec.manufacturer
+                      LEFT JOIN port_plugin pp
+                        ON pp.PTR_card = cc.PTR_card_a_OUT
+                         AND pp.port = cc.port_a_OUT
+                    WHERE c.status != 'terminated' AND is_circuit = 1
+                    UNION
+                    SELECT
+                      c.absid AS id,
+                      c.name,
+                      LOWER(c.status) AS status,
+                      LOWER(c.circuit_type) AS circuit_type,
+                      LOWER(c.service_type) AS service_type,
+                      events.short_descr AS project,
+                      e.name AS equipment,
+                      cc.port_b_OUT AS port,
+                      cc.int_LU_b AS logical_unit,
+                      LOWER(o.name) AS manufacturer,
+                      LOWER(ec.card_id) AS card_id,
+                      LOWER(
+                        IF(pp.interface_name IS NULL,
+                        '', pp.interface_name)) AS interface_name
+                    FROM circuit c
+                      INNER JOIN circuit_connections cc
+                        ON cc.circ_absid = c.absid
+                      INNER JOIN equipment e
+                        ON e.absid = cc.PTR_equip_b
+                      LEFT JOIN events
+                        ON events.absid = cc.PTR_project
+                      INNER JOIN equipment_card ec
+                        ON ec.absid = cc.PTR_card_b_OUT
+                      LEFT JOIN organisation o
+                        ON o.absid = ec.manufacturer
+                      LEFT JOIN port_plugin pp
+                        ON pp.PTR_card = cc.PTR_card_b_OUT
+                         AND pp.port = cc.port_b_OUT
+                    WHERE
+                     c.status != 'terminated'
+                      AND is_circuit = 1)
+                    AS inner_query
+                    ORDER BY
+                     FIELD(status,
+                      'spare',
+                      'planned',
+                      'ordered',
+                      'installed',
+                      'operational')"""
+
+
+def _convert_to_dict(crs):
+    return [dict((crs.description[i][0], "" if value is None else value)
+                 for i, value in enumerate(row)) for row in crs.fetchall()]
+
+
+def _infinera_field_update(record):
+    equipment_parts = record["equipment"].rsplit("-", 1)
+    card_parts = record["card_id"].split("-", 1)
+    record["interface_name"] = ""
+    record["equipment"] = equipment_parts[0]
     try:
-        cx = mysql.connector.connect(
-            host=opsdb["hostname"],
-            user=opsdb["username"],
-            passwd=opsdb["password"],
-            db=opsdb["dbname"])
-        yield cx
-    finally:
-        if cx:
-            cx.close()
-
-
-@contextlib.contextmanager
-def cursor(cnx):  # pragma: no cover
-    csr = None
+        record["interface_name"] = equipment_parts[1] + "-"
+    except IndexError:
+        pass  # Nothing to see here
     try:
-        csr = cnx.cursor()
-        yield csr
-    finally:
-        if csr:
-            csr.close()
-
-
-def _db_test(db, router):
-    database_logger = logging.getLogger(DATABASE_LOGGER_NAME)
-    with cursor(db) as crs:
-        query = "select model, manufacturer from equipment where name = %s"
-        crs.execute(query, (router['hostname'],))
-        for (model, manufacturer) in crs:
-            database_logger.debug("%s: %s %s" % (
-                router['hostname'], model, manufacturer))
-            yield {"model": model, "manufacturer": manufacturer}
+        record["interface_name"] += card_parts[1]
+    except IndexError:
+        record["interface_name"] += card_parts[0]
+    if record["port"] is not None and record["port"] != "":
+        record["interface_name"] += "-" + record["port"]
+    record["interface_name"] = record["interface_name"] \
+        .replace("--", "-").upper()
+    return record
+
+
+def _juniper_field_update(record):
+    if not record["interface_name"]:
+        record["interface_name"] = record["card_id"]
+        if record["port"] is not None and record["port"] != "":
+            separator = "/" if "-" in record["interface_name"] else ""
+            record["interface_name"] += separator + str(record["port"])
+    if record["logical_unit"] is not None and record["logical_unit"] != "":
+        record["interface_name"] += "." + str(record["logical_unit"])
+    return record
+
+
+def _update_fields(r):
+    func = globals().get("_" + r["manufacturer"] + "_field_update")
+    return func(r) if func else r
+
+
+def get_circuits(connection):
+    with db.cursor(connection) as crs:
+        crs.execute(retrieve_services_query)
+        r = _convert_to_dict(crs)
+    r = list(map(_update_fields, r))
+    return r
+
+
+def get_circuit_hierarchy(connection):
+    with db.cursor(connection) as crs:
+        crs.execute(circuit_hierarchy_query)
+        r = _convert_to_dict(crs)
+    return r
+
+
+def get_equipment_location_data(connection):
+    with db.cursor(connection) as crs:
+        crs.execute(equipment_location_query)
+        r = _convert_to_dict(crs)
+    return r
diff --git a/inventory_provider/routes/alarmsdb.py b/inventory_provider/routes/alarmsdb.py
index 8dddaa0b581be5799511270ef9c6b4e0a8187f76..ee1fda69bdeed171f78e2bdea561903562c7dfc0 100644
--- a/inventory_provider/routes/alarmsdb.py
+++ b/inventory_provider/routes/alarmsdb.py
@@ -2,7 +2,7 @@ import functools
 import json
 
 from flask import Blueprint, request, Response, current_app
-from inventory_provider import alarmsdb
+from inventory_provider import alarmsdb, db
 
 routes = Blueprint("inventory-alarmsdb-query-routes", __name__)
 
@@ -34,9 +34,9 @@ def get_interface_status():
     equipment = request.args.get("equipment")
     interface = request.args.get("interface")
 
-    with alarmsdb.connection(config['alarms-db']) as db:
+    with db.connection(config['alarms-db']) as connection:
         result = {"status": alarmsdb.get_last_known_interface_status(
-            db, equipment, interface)}
+            connection, equipment, interface)}
 
     return Response(
         json.dumps(result),
diff --git a/inventory_provider/routes/jobs.py b/inventory_provider/routes/jobs.py
index a81f9855e60b35178f60e2677227b24b908cf05e..b1a78c86065bc0a9446266c88aecd2a4e747f1f2 100644
--- a/inventory_provider/routes/jobs.py
+++ b/inventory_provider/routes/jobs.py
@@ -1,6 +1,8 @@
 import logging
 from flask import Blueprint, Response, current_app
 
+import inventory_provider.storage.external_inventory as external_inventory
+from inventory_provider import db, opsdb
 from inventory_provider.tasks.app import app
 from inventory_provider.constants import TASK_LOGGER_NAME
 
@@ -34,3 +36,58 @@ def update():
             args=[r["hostname"], r["community"]])
 
     return Response("OK")
+
+
+@routes.route("update-services", methods=['GET'])
+def update_service():
+    config = current_app.config['INVENTORY_PROVIDER_CONFIG']
+
+    with db.connection(config['ops-db']) as connection:
+        result = opsdb.get_circuits(connection)
+    external_inventory.update_services_to_monitor(result)
+    return Response("OK")
+
+
+@routes.route("update-interfaces", methods=['GET'])
+def update_interfaces():
+    config = current_app.config['INVENTORY_PROVIDER_CONFIG']
+
+    with db.connection(config['ops-db']) as connection:
+        result = opsdb.get_circuits(connection)
+    external_inventory.update_interfaces_to_services(result)
+    return Response("OK")
+
+
+@routes.route("update-service-hierarchy", methods=['GET'])
+def update_service_hierarchy():
+    config = current_app.config['INVENTORY_PROVIDER_CONFIG']
+
+    with db.connection(config['ops-db']) as connection:
+        result = opsdb.get_circuit_hierarchy(connection)
+    external_inventory.update_service_hierarchy(result)
+    return Response("OK")
+
+
+@routes.route("update-equipment-locations", methods=['GET'])
+def update_equipment_locations():
+    config = current_app.config['INVENTORY_PROVIDER_CONFIG']
+
+    with db.connection(config['ops-db']) as connection:
+        result = opsdb.get_equipment_location_data(connection)
+    external_inventory.update_equipment_locations(result)
+    return Response("OK")
+
+
+@routes.route("update-from-inventory-system", methods=['GET'])
+def update_from_inventory_system():
+    config = current_app.config['INVENTORY_PROVIDER_CONFIG']
+
+    with db.connection(config['ops-db']) as connection:
+        circuits = opsdb.get_circuits(connection)
+        hierarchy = opsdb.get_circuit_hierarchy(connection)
+        equipment_locations = opsdb.get_equipment_location_data(connection)
+    external_inventory.update_services_to_monitor(circuits)
+    external_inventory.update_interfaces_to_services(circuits)
+    external_inventory.update_service_hierarchy(hierarchy)
+    external_inventory.update_equipment_locations(equipment_locations)
+    return Response("OK")
diff --git a/inventory_provider/routes/opsdb.py b/inventory_provider/routes/opsdb.py
index 1b3195ecec04c04d87598918bc03d4862e07739a..9d176e6825704b1ddfe7d645a23bab021606f79e 100644
--- a/inventory_provider/routes/opsdb.py
+++ b/inventory_provider/routes/opsdb.py
@@ -1,11 +1,16 @@
 import functools
 import json
 
-from flask import Blueprint, request, Response, current_app
-from inventory_provider import opsdb
+from flask import Blueprint, request, Response
+from inventory_provider import db
+from inventory_provider.storage import external_inventory
 
 routes = Blueprint("inventory-opsdb-query-routes", __name__)
 
+services_key = "inv_services"
+interfaces_key = "inv_interfaces"
+equipment_locations_key = "inv_eq_locations"
+
 
 def require_accepts_json(f):
     """
@@ -26,16 +31,78 @@ def require_accepts_json(f):
     return decorated_function
 
 
-@routes.route("/test", methods=['GET', 'POST'])
-@require_accepts_json
-def opsdb_test():
-    config = current_app.config['INVENTORY_PROVIDER_CONFIG']
+def _decode_utf8_dict(d):
+    return {k.decode('utf8'): json.loads(v) for k, v in d.items()}
+
+
+@routes.route("/interfaces")
+def get_all_interface_details():
+    r = db.get_redis()
+    result = _decode_utf8_dict(
+            r.hgetall(interfaces_key))
+
+    return Response(
+        json.dumps(result),
+        mimetype="application/json")
+
 
+@routes.route("/interfaces/<equipment_name>")
+def get_interface_details_for_equipment(equipment_name):
+    r = db.get_redis()
     result = {}
-    with opsdb.connection(config['ops-db']) as db:
-        for r in config['routers']:
-            result[r['hostname']] = list(opsdb._db_test(db, r))
+    for t in r.hscan_iter(interfaces_key, "{}::*".format(equipment_name)):
+        result[t[0].decode("utf8")] = json.loads(t[1])
+
+    return Response(
+        json.dumps(result),
+        mimetype="application/json")
+
+
+@routes.route("/interfaces/<equipment_name>/<path:interface>")
+def get_interface_details(equipment_name, interface):
+    r = db.get_redis()
+    return Response(
+        r.hget(
+            interfaces_key,
+            "{}::{}".format(equipment_name, interface)),
+        mimetype="application/json")
+
+
+@routes.route("/equipment-location")
+def get_all_equipment_locations():
+    r = db.get_redis()
+    result = list(
+        _decode_utf8_dict(
+            r.hgetall(equipment_locations_key)).values())
 
     return Response(
         json.dumps(result),
         mimetype="application/json")
+
+
+@routes.route("/equipment-location/<path:equipment_name>")
+def get_equipment_location(equipment_name):
+    r = db.get_redis()
+    return Response(
+        r.hget(equipment_locations_key, equipment_name),
+        mimetype="application/json")
+
+
+@routes.route("/circuit-hierarchy/children/<parent_id>")
+def get_children(parent_id):
+    r = db.get_redis()
+    return Response(
+        r.hget(
+            external_inventory.service_parent_to_children_key,
+            parent_id),
+        mimetype="application/json")
+
+
+@routes.route("/circuit-hierarchy/parents/<child_id>")
+def get_parents(child_id):
+    r = db.get_redis()
+    return Response(
+        r.hget(
+            external_inventory.service_child_to_parents_key,
+            child_id),
+        mimetype="application/json")
diff --git a/inventory_provider/static/juniper.html b/inventory_provider/static/juniper.html
index 5ccb25324d28e99a0aea763fe3536aa65201fcf4..80e70c607a0fc98730a0df46b52b397798e56c59 100644
--- a/inventory_provider/static/juniper.html
+++ b/inventory_provider/static/juniper.html
@@ -20,7 +20,11 @@
         <p><b>interfaces</b></p>
         <ul>
           <li ng-repeat="i in interfaces">{{i.name}}
-            <ul><li>{{i.description}}</li></ul>
+            <ul>
+              <li>{{i.description}}</li>
+              <li ng-repeat="v4 in i.ipv4">v4: {{v4}}</li>
+              <li ng-repeat="v6 in i.ipv6">v6: {{v6}}</li>
+            </ul>
           </li>
         </ul>
         <div class="raw">{{interfaces}}</div>
diff --git a/inventory_provider/storage/__init__.py b/inventory_provider/storage/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/inventory_provider/storage/external_inventory.py b/inventory_provider/storage/external_inventory.py
new file mode 100644
index 0000000000000000000000000000000000000000..cc7bccf69f48c717202e22656f88f9f55f5c33aa
--- /dev/null
+++ b/inventory_provider/storage/external_inventory.py
@@ -0,0 +1,60 @@
+import json
+from collections import defaultdict
+from inventory_provider import db
+
+
+services_key = "inv_services"
+interfaces_key = "inv_interfaces"
+equipment_locations_key = "inv_eq_locations"
+service_child_to_parents_key = "inv_service_child_to_parents"
+service_parent_to_children_key = "inv_service_parent_to_children"
+
+
+def update_services_to_monitor(services):
+    r = db.get_redis()
+    relevant_types = ('path', 'service', 'l2circuit')
+    r.delete(services_key)
+    for service in services:
+        if service['circuit_type'].lower() in relevant_types:
+            r.hset(services_key, service['id'], json.dumps(service))
+
+
+def update_interfaces_to_services(services):
+    r = db.get_redis()
+    mapped_interfaces = defaultdict(list)
+    r.delete(interfaces_key)
+    for service in services:
+        key = "{}::{}".format(
+            service['equipment'],
+            service['interface_name']
+        )
+        mapped_interfaces[key].append(service)
+
+    for key, value in mapped_interfaces.items():
+        r.hset(interfaces_key, key, json.dumps(value))
+
+
+def update_service_hierarchy(records):
+    r = db.get_redis()
+    children_to_parents = defaultdict(list)
+    parents_to_children = defaultdict(list)
+    for relation in records:
+        parent_id = relation["parent_circuit_id"]
+        child_id = relation["child_circuit_id"]
+        parents_to_children[parent_id].append(relation)
+        children_to_parents[child_id].append(relation)
+
+    r.delete(service_child_to_parents_key)
+    for child, parents in children_to_parents.items():
+        r.hset(service_child_to_parents_key, child, json.dumps(parents))
+
+    r.delete(service_parent_to_children_key)
+    for parent, children in parents_to_children.items():
+        r.hset(service_parent_to_children_key, parent, json.dumps(children))
+
+
+def update_equipment_locations(equipment_location_data):
+    r = db.get_redis()
+    r.delete(equipment_locations_key)
+    for ld in equipment_location_data:
+        r.hset(equipment_locations_key, ld['equipment_name'], json.dumps(ld))
diff --git a/setup.py b/setup.py
index d8aae6b09389b832b27d55c2f26d3a121c4e253b..b041145b677577119497968f883a6ed70f59cb09 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='inventory-provider',
-    version="0.12",
+    version="0.13",
     author='GEANT',
     author_email='swd@geant.org',
     description='Dashboard inventory provider',
diff --git a/test/storage/test_external_inventory.py b/test/storage/test_external_inventory.py
new file mode 100644
index 0000000000000000000000000000000000000000..30c13e0bc7287fa2b118b875b05b707482028857
--- /dev/null
+++ b/test/storage/test_external_inventory.py
@@ -0,0 +1,150 @@
+from inventory_provider.storage import external_inventory
+
+
+def test_update_services_to_monitor(mocker):
+    mocked_redis = mocker.patch(
+        "inventory_provider.storage.external_inventory.db.get_redis")
+    mocked_hset = mocked_redis.return_value.hset
+
+    services = [
+        {"circuit_type": "path", "id": "test_id_0", "other": "stuff"},
+        {"circuit_type": "service", "id": "test_id_1", "other": "stuff"},
+        {"circuit_type": "l2circuit", "id": "test_id_2", "other": "stuff"},
+        {"circuit_type": "spam", "id": "test_id_4", "other": "stuff"}
+    ]
+    external_inventory.update_services_to_monitor(services)
+    mocked_hset.assert_any_call(
+        external_inventory.services_key, "test_id_0",
+        "{\"circuit_type\": \"path\","
+        " \"id\": \"test_id_0\","
+        " \"other\": \"stuff\"}")
+    mocked_hset.assert_any_call(
+        external_inventory.services_key, "test_id_1",
+        "{\"circuit_type\": \"service\","
+        " \"id\": \"test_id_1\","
+        " \"other\": \"stuff\"}")
+    mocked_hset.assert_any_call(
+        external_inventory.services_key, "test_id_2",
+        "{\"circuit_type\": \"l2circuit\","
+        " \"id\": \"test_id_2\","
+        " \"other\": \"stuff\"}")
+    assert mocked_hset.call_count == 3
+
+
+def test_update_interfaces_to_services(mocker):
+    mocked_redis = mocker.patch(
+        "inventory_provider.storage.external_inventory.db.get_redis")
+    mocked_hset = mocked_redis.return_value.hset
+    services = [
+        {"equipment": "eq_0", "interface_name": "if_0"},
+        {"equipment": "eq_1", "interface_name": "if_1"},
+        {"equipment": "eq_2", "interface_name": "if_2"},
+        {"equipment": "eq_3", "interface_name": "if_3"},
+        {"equipment": "eq_3", "interface_name": "if_3", "extra": "stuff"},
+        {"equipment": "eq_4", "interface_name": "if_4"}
+    ]
+    unique_keys = set(s["equipment"] + "::" + s["interface_name"]
+                      for s in services)
+
+    external_inventory.update_interfaces_to_services(services)
+    assert mocked_hset.call_count == len(unique_keys)
+    mocked_hset.assert_any_call(external_inventory.interfaces_key,
+                                "eq_2::if_2",
+                                "[{\"equipment\": \"eq_2\","
+                                " \"interface_name\": \"if_2\"}]")
+    mocked_hset.assert_any_call(external_inventory.interfaces_key,
+                                "eq_3::if_3",
+                                "[{\"equipment\": \"eq_3\","
+                                " \"interface_name\": \"if_3\"},"
+                                " {\"equipment\": \"eq_3\","
+                                " \"interface_name\": \"if_3\","
+                                " \"extra\": \"stuff\"}]")
+
+
+def test_update_service_hierarchy(mocker):
+    mocked_redis = mocker.patch(
+        "inventory_provider.storage.external_inventory.db.get_redis")
+    mocked_hset = mocked_redis.return_value.hset
+    data = [
+        {"parent_circuit": "PC_1",
+         "parent_circuit_id": "1001",
+         "parent_circuit_status": "operational",
+         "child_circuit": "CC_1",
+         "child_circuit_id": "2001",
+         "segment_group": 1},
+        {"parent_circuit": "PC_2",
+         "parent_circuit_id": "1002",
+         "parent_circuit_status": "operational",
+         "child_circuit": "CC_1",
+         "child_circuit_id": "2001",
+         "segment_group": 1},
+        {"parent_circuit": "PC_3",
+         "parent_circuit_id": "1003",
+         "parent_circuit_status": "operational",
+         "child_circuit": "CC_2",
+         "child_circuit_id": "2002",
+         "segment_group": 1},
+        {"parent_circuit": "PC_3",
+         "parent_circuit_id": "1003",
+         "parent_circuit_status": "operational",
+         "child_circuit": "CC_3",
+         "child_circuit_id": "2003",
+         "segment_group": 1}
+    ]
+    external_inventory.update_service_hierarchy(data)
+    u_parent_keys = set([d["parent_circuit_id"] for d in data])
+    u_child_keys = set([d["child_circuit_id"] for d in data])
+    assert mocked_hset.call_count == len(u_parent_keys) + len(u_child_keys)
+    mocked_hset.assert_any_call(
+        external_inventory.service_child_to_parents_key,
+        "2001",
+        "[{\"parent_circuit\": \"PC_1\","
+        " \"parent_circuit_id\": \"1001\","
+        " \"parent_circuit_status\": \"operational\","
+        " \"child_circuit\": \"CC_1\","
+        " \"child_circuit_id\": \"2001\","
+        " \"segment_group\": 1},"
+        " {\"parent_circuit\": \"PC_2\","
+        " \"parent_circuit_id\": \"1002\","
+        " \"parent_circuit_status\": \"operational\","
+        " \"child_circuit\": \"CC_1\","
+        " \"child_circuit_id\": \"2001\","
+        " \"segment_group\": 1}]"
+    )
+    mocked_hset.assert_any_call(
+        external_inventory.service_child_to_parents_key,
+        "2002",
+        "[{\"parent_circuit\": \"PC_3\","
+        " \"parent_circuit_id\": \"1003\","
+        " \"parent_circuit_status\": \"operational\","
+        " \"child_circuit\": \"CC_2\","
+        " \"child_circuit_id\": \"2002\","
+        " \"segment_group\": 1}]"
+    )
+
+    mocked_hset.assert_any_call(
+        external_inventory.service_parent_to_children_key,
+        "1003",
+        "[{\"parent_circuit\": \"PC_3\","
+        " \"parent_circuit_id\": \"1003\","
+        " \"parent_circuit_status\": \"operational\","
+        " \"child_circuit\": \"CC_2\","
+        " \"child_circuit_id\": \"2002\","
+        " \"segment_group\": 1},"
+        " {\"parent_circuit\": \"PC_3\","
+        " \"parent_circuit_id\": \"1003\","
+        " \"parent_circuit_status\": \"operational\","
+        " \"child_circuit\": \"CC_3\","
+        " \"child_circuit_id\": \"2003\","
+        " \"segment_group\": 1}]"
+    )
+    mocked_hset.assert_any_call(
+        external_inventory.service_parent_to_children_key,
+        "1002",
+        "[{\"parent_circuit\": \"PC_2\","
+        " \"parent_circuit_id\": \"1002\","
+        " \"parent_circuit_status\": \"operational\","
+        " \"child_circuit\": \"CC_1\","
+        " \"child_circuit_id\": \"2001\","
+        " \"segment_group\": 1}]"
+    )
diff --git a/test/test_alarmdb_routes.py b/test/test_alarmdb_routes.py
index 2e16e72d8fb59c170b43299b9a0809ea8af97baf..82522455868b48bdda22a82131c606dcdd8f0d72 100644
--- a/test/test_alarmdb_routes.py
+++ b/test/test_alarmdb_routes.py
@@ -9,7 +9,7 @@ DEFAULT_REQUEST_HEADERS = {
 
 def test_get_interface_status(mocker, client):
     mocked_conn = mocker.patch('inventory_provider.routes.alarmsdb'
-                               '.alarmsdb.connection')
+                               '.db.connection')
     mocked_conn.return_value.__enter__.return_value = None
 
     mocked_inteface_status = mocker.patch(
diff --git a/test/test_alarmsdb.py b/test/test_alarmsdb.py
index c993a762106481a77983df66efdc1276b1a78d2e..7eb707c1210bca0a21cd36ebdc3b658290cca426 100644
--- a/test/test_alarmsdb.py
+++ b/test/test_alarmsdb.py
@@ -2,7 +2,7 @@ import inventory_provider.alarmsdb as alarmsdb
 
 
 def test_infinera_interface_status(mocker):
-    mocked_get_cursor = mocker.patch('inventory_provider.alarmsdb.cursor')
+    mocked_get_cursor = mocker.patch('inventory_provider.alarmsdb.db.cursor')
     mocked_execute = mocked_get_cursor. \
         return_value.__enter__.return_value.execute
     mocked_fetchone = mocked_get_cursor.return_value.__enter__. \
@@ -30,7 +30,7 @@ def test_infinera_interface_status(mocker):
 
 
 def test_coriant_interface_status(mocker):
-    mocked_get_cursor = mocker.patch('inventory_provider.alarmsdb.cursor')
+    mocked_get_cursor = mocker.patch('inventory_provider.alarmsdb.db.cursor')
     mocked_execute = mocked_get_cursor. \
         return_value.__enter__.return_value.execute
     mocked_fetchone = mocked_get_cursor.return_value.__enter__. \
@@ -58,7 +58,7 @@ def test_coriant_interface_status(mocker):
 
 
 def test_juniper_interface_status(mocker):
-    mocked_get_cursor = mocker.patch('inventory_provider.alarmsdb.cursor')
+    mocked_get_cursor = mocker.patch('inventory_provider.alarmsdb.db.cursor')
     mocked_execute = mocked_get_cursor. \
         return_value.__enter__.return_value.execute
     mocked_fetchone = mocked_get_cursor.return_value.__enter__. \
diff --git a/test/test_external_inventory_routes.py b/test/test_external_inventory_routes.py
new file mode 100644
index 0000000000000000000000000000000000000000..4adc7d5890795740626e7a696fe51f6c1a89a6fc
--- /dev/null
+++ b/test/test_external_inventory_routes.py
@@ -0,0 +1,134 @@
+import json
+from inventory_provider.storage import external_inventory
+
+DEFAULT_REQUEST_HEADERS = {
+    "Content-type": "application/json",
+    "Accept": ["application/json"]
+}
+
+
+def test_get_one_equipment_location(mocker, client):
+    mocked_redis = mocker.patch(
+        "inventory_provider.routes.opsdb.db.get_redis")
+    mocked_hget = mocked_redis.return_value.hget
+    dummy_data = {
+        "absid": 1404,
+        "equipment_name": "pp-cz-e13b",
+        "pop_name": "Prague",
+        "pop_abbreviation": "pra",
+        "pop_site_id": "",
+        "country": "Czech Republic",
+        "longitude": 14.391738888889,
+        "latitude": 50.101847222222
+    }
+    mocked_hget.return_value = json.dumps(dummy_data)
+
+    rv = client.get(
+        '/opsdb/equipment-location/dummy-equipment',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+    assert dummy_data == json.loads(rv.data.decode("utf-8"))
+
+    mocked_hget.assert_called_with(
+        external_inventory.equipment_locations_key,
+        "dummy-equipment"
+    )
+
+
+def test_get_equipment_location(mocker, client):
+    mocked_redis = mocker.patch(
+        "inventory_provider.routes.opsdb.db.get_redis")
+    mocked_hgetall = mocked_redis.return_value.hgetall
+
+    rv = client.get(
+        '/opsdb/equipment-location',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+
+    mocked_hgetall.assert_called_with(
+        external_inventory.equipment_locations_key
+    )
+
+
+def test_get_interface_info(mocker, client):
+    mocked_redis = mocker.patch(
+        "inventory_provider.routes.opsdb.db.get_redis")
+    mocked_hgetall = mocked_redis.return_value.hgetall
+
+    rv = client.get(
+        '/opsdb/interfaces',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+
+    mocked_hgetall.assert_called_with(
+        external_inventory.interfaces_key
+    )
+
+
+def test_get_interface_info_for_equipment(mocker, client):
+    mocked_redis = mocker.patch(
+        "inventory_provider.routes.opsdb.db.get_redis")
+    mocked_hscan_iter = mocked_redis.return_value.hscan_iter
+
+    rv = client.get(
+        '/opsdb/interfaces/dummy-equipment',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+
+    mocked_hscan_iter.assert_called_with(
+        external_inventory.interfaces_key, "dummy-equipment::*"
+    )
+
+
+def test_get_interface_info_for_equipment_and_interface(mocker, client):
+    mocked_redis = mocker.patch(
+        "inventory_provider.routes.opsdb.db.get_redis")
+    mocked_hget = mocked_redis.return_value.hget
+
+    rv = client.get(
+        '/opsdb/interfaces/dummy-equipment/xe-2/3/1',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+
+    mocked_hget.assert_called_with(
+        external_inventory.interfaces_key, "dummy-equipment::xe-2/3/1"
+    )
+
+
+def test_get_children(mocker, client):
+    mocked_redis = mocker.patch(
+        "inventory_provider.routes.opsdb.db.get_redis")
+    mocked_hget = mocked_redis.return_value.hget
+
+    rv = client.get(
+        '/opsdb/circuit-hierarchy/children/22987',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+
+    mocked_hget.assert_called_with(
+        external_inventory.service_parent_to_children_key,
+        "22987"
+    )
+
+
+def test_get_parents(mocker, client):
+    mocked_redis = mocker.patch(
+        "inventory_provider.routes.opsdb.db.get_redis")
+    mocked_hget = mocked_redis.return_value.hget
+
+    rv = client.get(
+        '/opsdb/circuit-hierarchy/children/22987',
+        headers=DEFAULT_REQUEST_HEADERS)
+    assert rv.status_code == 200
+    assert rv.is_json
+
+    mocked_hget.assert_called_with(
+        external_inventory.service_parent_to_children_key,
+        "22987"
+    )
diff --git a/test/test_opsdb.py b/test/test_opsdb.py
new file mode 100644
index 0000000000000000000000000000000000000000..3507bdc0bcb80e341586cd2d8d966a5591da94fc
--- /dev/null
+++ b/test/test_opsdb.py
@@ -0,0 +1,116 @@
+import inventory_provider.opsdb
+
+
+def test_update_fields(mocker):
+    mocker.patch("inventory_provider.opsdb._juniper_field_update")
+    t = {"manufacturer": "juniper"}
+    inventory_provider.opsdb._update_fields(t)
+    inventory_provider.opsdb._juniper_field_update.assert_called_once_with(t)
+
+    mocker.patch("inventory_provider.opsdb._infinera_field_update")
+    t = {"manufacturer": "infinera"}
+    inventory_provider.opsdb._update_fields(t)
+    inventory_provider.opsdb._infinera_field_update.assert_called_once_with(t)
+
+    f = {"manufacturer": "non-existent"}
+    r = inventory_provider.opsdb._update_fields(f)
+    assert f == r
+
+
+def test_infinera_field_update():
+    i = {
+        "equipment": "AMS01-DTNX10-1-1",
+        "card_id": "tim-b-5-7",
+        "port": "1"
+         }
+    r = inventory_provider.opsdb._infinera_field_update(i)
+    assert r["equipment"] == "AMS01-DTNX10-1"
+    assert r["interface_name"] == "1-B-5-7-1"
+
+    i = {
+        "equipment": "BUD01_CX_01",
+        "card_id": "tim-1/2",
+        "port": "1"
+         }
+    r = inventory_provider.opsdb._infinera_field_update(i)
+    assert r["equipment"] == "BUD01_CX_01"
+    assert r["interface_name"] == "1/2-1"
+
+    i = {
+        "equipment": "irrelevant",
+        "card_id": "tim_1/2",
+        "port": "1"
+         }
+    r = inventory_provider.opsdb._infinera_field_update(i)
+    assert r["interface_name"] == "TIM_1/2-1"
+
+
+def test_juniper_field_update():
+    i = {
+        "interface_name": "xe-1/2",
+        "logical_unit": None
+         }
+    r = inventory_provider.opsdb._juniper_field_update(i)
+    assert r["interface_name"] == "xe-1/2"
+
+    i["interface_name"] = "xe-1/2"
+    i["logical_unit"] = 101
+    r = inventory_provider.opsdb._juniper_field_update(i)
+    assert r["interface_name"] == "xe-1/2.101"
+
+    i["interface_name"] = "xe-1/2"
+    i["logical_unit"] = 0
+    r = inventory_provider.opsdb._juniper_field_update(i)
+    assert r["interface_name"] == "xe-1/2.0"
+
+    i["interface_name"] = "xe-1/2"
+    i["logical_unit"] = None
+    i["port"] = 0
+    r = inventory_provider.opsdb._juniper_field_update(i)
+    assert r["interface_name"] == "xe-1/2"
+
+    i["interface_name"] = None
+    i["card_id"] = "xe-2/0"
+    i["logical_unit"] = None
+    i["port"] = None
+    r = inventory_provider.opsdb._juniper_field_update(i)
+    assert r["interface_name"] == "xe-2/0"
+
+    i["interface_name"] = None
+    i["port"] = "0"
+    r = inventory_provider.opsdb._juniper_field_update(i)
+    assert r["interface_name"] == "xe-2/0/0"
+
+    i["interface_name"] = None
+    i["port"] = 0
+    r = inventory_provider.opsdb._juniper_field_update(i)
+    assert r["interface_name"] == "xe-2/0/0"
+
+    i["interface_name"] = None
+    i["logical_unit"] = "123"
+    r = inventory_provider.opsdb._juniper_field_update(i)
+    assert r["interface_name"] == "xe-2/0/0.123"
+
+    i["interface_name"] = None
+    i["logical_unit"] = 123
+    r = inventory_provider.opsdb._juniper_field_update(i)
+    assert r["interface_name"] == "xe-2/0/0.123"
+
+
+def test_get_circuits(mocker):
+    mocker.patch("inventory_provider.opsdb.db.cursor")
+    mocked_convert_to_dict = mocker.patch(
+        "inventory_provider.opsdb._convert_to_dict")
+    i = {"manufacturer": "infinera"}
+    j = {"manufacturer": "juniper"}
+    mocked_convert_to_dict.return_value = [i, j]
+
+    mocked_infinera_update = mocker.patch(
+        "inventory_provider.opsdb._infinera_field_update")
+    mocked_juniper_update = mocker.patch(
+        "inventory_provider.opsdb._juniper_field_update")
+
+    inventory_provider.opsdb.get_circuits(None)
+
+    mocked_infinera_update.assert_called_once_with(i)
+    mocked_juniper_update.assert_called_once_with(j)
diff --git a/tox.ini b/tox.ini
index 1f203d8810d2613924649bd99f8c67c77007d627..08605507f9962a1b519d7779d9bbefcc8a82519f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,7 +10,7 @@ deps =
 
 commands =
     coverage erase
-    coverage run --source inventory_provider -m py.test
+    coverage run --source inventory_provider -m py.test {posargs}
     coverage xml
     coverage html
     coverage report --fail-under 75