From 96ca88345e13ad68785c2b06d896f50aedd568cd Mon Sep 17 00:00:00 2001
From: Erik Reid <erik.reid@geant.org>
Date: Fri, 5 Mar 2021 08:57:03 +0100
Subject: [PATCH] refactored to share some code between both routes

---
 inventory_provider/routes/poller.py | 150 ++++++++++++++++++++++------
 1 file changed, 117 insertions(+), 33 deletions(-)

diff --git a/inventory_provider/routes/poller.py b/inventory_provider/routes/poller.py
index f7ef02d5..a9282bca 100644
--- a/inventory_provider/routes/poller.py
+++ b/inventory_provider/routes/poller.py
@@ -176,36 +176,78 @@ def _load_interfaces(hostname):
             }
 
 
-def _load_poller_interfaces(hostname=None):
+def _add_bundle_parents(interfaces, hostname=None):
+    """
+    generator that adds bundle-parents info to each interface.
 
-    snmp_indexes = _load_snmp_indexes(hostname)
+    :param interfaces: result of _load_interfaces
+    :param hostname: hostname or None for all
+    :return: generator with bundle-parents populated in each element
+    """
     bundles = _load_interface_bundles(hostname)
-    services = _load_services(hostname)
-
-    for ifc in _load_interfaces(hostname):
-
-        router_snmp = snmp_indexes.get(ifc['router'], None)
-        if not router_snmp or ifc['name'] not in router_snmp:
-            # there's no way to poll this interface
-            continue
-        ifc['snmp-index'] = router_snmp[ifc['name']]['index']
-        # TODO: uncomment this when it won't break poller-admin-service
-        # not urgent ... it looks empirically like all logical-system
-        #                interfaces are repeated for both communities
-        # ifc['snmp-community'] = router_snmp[ifc['name']]['community']
-
+    for ifc in interfaces:
         router_bundle = bundles.get(ifc['router'], None)
         if router_bundle:
             base_ifc = ifc['name'].split('.')[0]
             ifc['bundle-parents'] = router_bundle.get(base_ifc, [])
+        yield ifc
+
 
+def _add_circuits(interfaces, hostname=None):
+    """
+    generator that adds service info to each interface.
+
+    :param interfaces: result of _load_interfaces
+    :param hostname: hostname or None for all
+    :return: generator with 'circuits' populated in each element, if present
+    """
+    services = _load_services(hostname)
+    for ifc in interfaces:
         router_services = services.get(ifc['router'], None)
         if router_services:
             ifc['circuits'] = router_services.get(ifc['name'], [])
+        yield ifc
 
+
+def _add_snmp_indexes(interfaces, hostname=None):
+    """
+    generator that adds snmp info to each interface, if available
+
+    :param interfaces: result of _load_interfaces
+    :param hostname: hostname or None for all
+    :return: generator with 'snmp-index' optionally added to each element
+    """
+    snmp_indexes = _load_snmp_indexes(hostname)
+    for ifc in interfaces:
+        router_snmp = snmp_indexes.get(ifc['router'], None)
+        if router_snmp and ifc['name'] in router_snmp:
+            ifc['snmp-index'] = router_snmp[ifc['name']]['index']
+            # TODO: uncomment this when it won't break poller-admin-service
+            # not urgent ... it looks empirically like all logical-system
+            #                interfaces are repeated for both communities
+            # ifc['snmp-community'] = router_snmp[ifc['name']]['community']
         yield ifc
 
 
+def _load_interfaces_to_poll(hostname=None):
+    """
+    prepares the result of a call to /interfaces
+
+    :param hostname: hostname or None for all
+    :return: generator yielding interface elements
+    """
+    basic_interfaces = _load_interfaces(hostname)
+    with_bundles = _add_bundle_parents(basic_interfaces, hostname)
+    with_circuits = _add_circuits(with_bundles, hostname)
+    with_snmp = _add_snmp_indexes(with_circuits, hostname)
+
+    def _has_snmp_index(ifc):
+        return 'snmp-index' in ifc
+
+    # only return interfaces that can be polled
+    return filter(_has_snmp_index, with_snmp)
+
+
 @routes.route("/interfaces", methods=['GET', 'POST'])
 @routes.route('/interfaces/<hostname>', methods=['GET', 'POST'])
 @common.require_accepts_json
@@ -236,7 +278,7 @@ def interfaces(hostname=None):
     if result:
         result = result.decode('utf-8')
     else:
-        result = list(_load_poller_interfaces(hostname))
+        result = list(_load_interfaces_to_poll(hostname))
         if not result:
             return Response(
                 response='no interfaces found',
@@ -250,6 +292,60 @@ def interfaces(hostname=None):
     return Response(result, mimetype="application/json")
 
 
+def interface_speed(ifc):
+    """
+    Return the maximum bits per second expected for the given interface.
+
+    cf. https://www.juniper.net/documentation/us/en/software/
+               vmx/vmx-getting-started/topics/task/
+               vmx-chassis-interface-type-configuring.html
+
+    :param ifc:
+    :return: an integer bits per second
+    """
+    GB = 1000000000
+    # GB = 1 << 30
+
+    def _name_to_speed(ifc_name):
+        if ifc_name.startswith('ge'):
+            return GB
+        if ifc_name.startswith('xe'):
+            return 10 * GB
+        if ifc_name.startswith('et'):
+            return 100 * GB
+        logger.warning(f'unrecognized interface name: {ifc_name}')
+        return -1
+
+    if ifc['bundle-parents']:
+        if not ifc['name'].startswith('ae'):
+            logger.warning(
+                f'ifc has bundle-parents, but name is {ifc["name"]}')
+        return sum(_name_to_speed(name) for name in ifc['bundle-parents'])
+
+    return _name_to_speed(ifc['name'])
+
+
+def _load_interfaces_and_speeds(hostname=None):
+    """
+    prepares the result of a call to /speeds
+
+    :param hostname: hostname or None for all
+    :return: generator yielding interface elements
+    """
+    basic_interfaces = _load_interfaces(hostname)
+    with_bundles = _add_bundle_parents(basic_interfaces, hostname)
+    with_bundles = list(with_bundles)
+
+    def _result_ifc(ifc):
+        return {
+            'router': ifc['router'],
+            'name': ifc['name'],
+            'speed': interface_speed(ifc)
+        }
+
+    return map(_result_ifc, with_bundles)
+
+
 @routes.route("/speeds", methods=['GET', 'POST'])
 @routes.route('/speeds/<hostname>', methods=['GET', 'POST'])
 @common.require_accepts_json
@@ -260,8 +356,9 @@ def interface_speeds(hostname=None):
     which returns information for either all interfaces
     or those on the requested hostname.
 
-    The response is a list of speed information for all
-    known interfaces.
+    The response is a list of maximum speed information (in bits
+    per second) for all known interfaces.
+    [speed <= 0 means the max interface speed can't be determined]
 
     .. asjson::
        inventory_provider.routes.poller.INTERFACE_SPEED_LIST_SCHEMA
@@ -279,20 +376,7 @@ def interface_speeds(hostname=None):
     if result:
         result = result.decode('utf-8')
     else:
-
-        def _speed(ifc):
-            # TODO
-            return -1
-
-        def _result_ifc(ifc):
-            return {
-                'router': ifc['router'],
-                'name': ifc['name'],
-                'speed': _speed(ifc)
-            }
-
-        result = list(map(_result_ifc, _load_interfaces(hostname)))
-
+        result = list(_load_interfaces_and_speeds(hostname))
         if not result:
             return Response(
                 response='no interfaces found',
-- 
GitLab