From 5ae5b5479580345b5a2b42b49bc2fb5544d88a48 Mon Sep 17 00:00:00 2001
From: armingol <pablo.armingolrobles@telefonica.com>
Date: Thu, 21 Mar 2024 10:58:38 +0100
Subject: [PATCH] Logical Inventory: First version

1) Modify device component to store ACL data
2) New WebUI tab with logical inventory
---
 .../drivers/openconfig/templates/Acl.py       |  64 +--
 src/webui/service/device/routes.py            |  10 +
 src/webui/service/templates/device/home.html  |   9 +
 .../service/templates/device/logical.html     | 397 ++++++++++++++++++
 4 files changed, 449 insertions(+), 31 deletions(-)
 create mode 100644 src/webui/service/templates/device/logical.html

diff --git a/src/device/service/drivers/openconfig/templates/Acl.py b/src/device/service/drivers/openconfig/templates/Acl.py
index c316772a5..e9a9119c5 100644
--- a/src/device/service/drivers/openconfig/templates/Acl.py
+++ b/src/device/service/drivers/openconfig/templates/Acl.py
@@ -20,7 +20,7 @@ from .Tools import add_value_from_tag
 LOGGER = logging.getLogger(__name__)
 
 XPATH_ACL_SET     = "//ocacl:acl/ocacl:acl-sets/ocacl:acl-set"
-XPATH_A_ACL_ENTRY = ".//ocacl:acl-entries/ocacl:ecl-entry"
+XPATH_A_ACL_ENTRY = ".//ocacl:acl-entries/ocacl:acl-entry"
 XPATH_A_IPv4      = ".//ocacl:ipv4/ocacl:config"
 XPATH_A_TRANSPORT = ".//ocacl:transport/ocacl:config"
 XPATH_A_ACTIONS   = ".//ocacl:actions/ocacl:config"
@@ -34,29 +34,31 @@ def parse(xml_data : ET.Element) -> List[Tuple[str, Dict[str, Any]]]:
 
     response = []
     acl = {}
+    name = {}
 
     for xml_acl in xml_data.xpath(XPATH_ACL_SET, namespaces=NAMESPACES):
         #LOGGER.info('xml_acl = {:s}'.format(str(ET.tostring(xml_acl))))
 
         acl_name = xml_acl.find('ocacl:name', namespaces=NAMESPACES)
         if acl_name is None or acl_name.text is None: continue
-        add_value_from_tag(acl, 'name', acl_name)
+        add_value_from_tag(name, 'name', acl_name)
 
         acl_type = xml_acl.find('ocacl:type', namespaces=NAMESPACES)
         add_value_from_tag(acl, 'type', acl_type)
 
         for xml_acl_entries in xml_acl.xpath(XPATH_A_ACL_ENTRY, namespaces=NAMESPACES):
 
-            acl_id = xml_acl_entries.find('ocacl:sequence_id', namespaces=NAMESPACES)
-            add_value_from_tag(acl, 'sequence_id', acl_id)
+            acl_id = xml_acl_entries.find('ocacl:sequence-id', namespaces=NAMESPACES)
+            add_value_from_tag(acl, 'sequence-id', acl_id)
+            LOGGER.info('xml_acl_id = {:s}'.format(str(ET.tostring(acl_id))))
 
             for xml_ipv4 in xml_acl_entries.xpath(XPATH_A_IPv4, namespaces=NAMESPACES):
 
-                ipv4_source = xml_ipv4.find('ocacl:source_address', namespaces=NAMESPACES)
-                add_value_from_tag(acl, 'source_address' , ipv4_source)
+                ipv4_source = xml_ipv4.find('ocacl:source-address', namespaces=NAMESPACES)
+                add_value_from_tag(acl, 'source-address' , ipv4_source)
 
-                ipv4_destination = xml_ipv4.find('ocacl:destination_address', namespaces=NAMESPACES)
-                add_value_from_tag(acl, 'destination_address' , ipv4_destination)
+                ipv4_destination = xml_ipv4.find('ocacl:destination-address', namespaces=NAMESPACES)
+                add_value_from_tag(acl, 'destination-address' , ipv4_destination)
 
                 ipv4_protocol = xml_ipv4.find('ocacl:protocol', namespaces=NAMESPACES)
                 add_value_from_tag(acl, 'protocol' , ipv4_protocol)
@@ -64,30 +66,30 @@ def parse(xml_data : ET.Element) -> List[Tuple[str, Dict[str, Any]]]:
                 ipv4_dscp = xml_ipv4.find('ocacl:dscp', namespaces=NAMESPACES)
                 add_value_from_tag(acl, 'dscp' , ipv4_dscp)
 
-                ipv4_hop_limit = xml_ipv4.find('ocacl:hop_limit', namespaces=NAMESPACES)
-                add_value_from_tag(acl, 'hop_limit' , ipv4_hop_limit)
+                ipv4_hop_limit = xml_ipv4.find('ocacl:hop-limit', namespaces=NAMESPACES)
+                add_value_from_tag(acl, 'hop-limit' , ipv4_hop_limit)
 
             for xml_transport in xml_acl_entries.xpath(XPATH_A_TRANSPORT, namespaces=NAMESPACES):
 
-                transport_source = xml_transport.find('ocacl:source_port', namespaces=NAMESPACES)
-                add_value_from_tag(acl, 'source_port' ,transport_source)
+                transport_source = xml_transport.find('ocacl:source-port', namespaces=NAMESPACES)
+                add_value_from_tag(acl, 'source-port' ,transport_source)
 
-                transport_destination = xml_transport.find('ocacl:destination_port', namespaces=NAMESPACES)
-                add_value_from_tag(acl, 'destination_port' ,transport_destination)
+                transport_destination = xml_transport.find('ocacl:destination-port', namespaces=NAMESPACES)
+                add_value_from_tag(acl, 'destination-port' ,transport_destination)
 
-                transport_tcp_flags = xml_transport.find('ocacl:tcp_flags', namespaces=NAMESPACES)
-                add_value_from_tag(acl, 'tcp_flags' ,transport_tcp_flags)
+                transport_tcp_flags = xml_transport.find('ocacl:tcp-flags', namespaces=NAMESPACES)
+                add_value_from_tag(acl, 'tcp-flags' ,transport_tcp_flags)
 
             for xml_action in xml_acl_entries.xpath(XPATH_A_ACTIONS, namespaces=NAMESPACES):
 
-                action = xml_action.find('ocacl:forwarding_action', namespaces=NAMESPACES)
-                add_value_from_tag(acl, 'forwarding_action' ,action)
+                action = xml_action.find('ocacl:forwarding-action', namespaces=NAMESPACES)
+                add_value_from_tag(acl, 'forwarding-action' ,action)
 
-                log_action = xml_action.find('ocacl:log_action', namespaces=NAMESPACES)
-                add_value_from_tag(acl, 'log_action' ,log_action)
+                log_action = xml_action.find('ocacl:log-action', namespaces=NAMESPACES)
+                add_value_from_tag(acl, 'log-action' ,log_action)
 
             resource_key =  '/acl/acl-set[{:s}][{:s}]/acl-entry[{:s}]'.format(
-                acl['name'], acl['type'], acl['sequence-id'])
+                name['name'], acl['type'], acl['sequence-id'])
             response.append((resource_key,acl))
 
     for xml_interface in xml_data.xpath(XPATH_INTERFACE, namespaces=NAMESPACES):
@@ -99,25 +101,25 @@ def parse(xml_data : ET.Element) -> List[Tuple[str, Dict[str, Any]]]:
 
         for xml_ingress in xml_interface.xpath(XPATH_I_INGRESS, namespaces=NAMESPACES):
 
-            i_name = xml_ingress.find('ocacl:set_name_ingress', namespaces=NAMESPACES)
-            add_value_from_tag(interface, 'ingress_set_name' , i_name)
+            i_name = xml_ingress.find('ocacl:set-name-ingress', namespaces=NAMESPACES)
+            add_value_from_tag(interface, 'ingress-set-name' , i_name)
 
-            i_type = xml_ingress.find('ocacl:type_ingress', namespaces=NAMESPACES)
-            add_value_from_tag(interface, 'ingress_type' , i_type)
+            i_type = xml_ingress.find('ocacl:type-ingress', namespaces=NAMESPACES)
+            add_value_from_tag(interface, 'ingress-type' , i_type)
 
             resource_key =  '/acl/interfaces/ingress[{:s}][{:s}]'.format(
-                acl['name'], acl['type'])
+                name['name'], acl['type'])
             response.append((resource_key,interface))
 
         for xml_egress in xml_interface.xpath(XPATH_I_EGRESS, namespaces=NAMESPACES):
 
-            e_name = xml_egress.find('ocacl:set_name_egress', namespaces=NAMESPACES)
-            add_value_from_tag(interface, 'egress_set_name' , e_name)
+            e_name = xml_egress.find('ocacl:set-name-egress', namespaces=NAMESPACES)
+            add_value_from_tag(interface, 'egress-set-name' , e_name)
 
-            e_type = xml_egress.find('ocacl:type_egress', namespaces=NAMESPACES)
-            add_value_from_tag(interface, 'egress_type' , e_type)
+            e_type = xml_egress.find('ocacl:type-egress', namespaces=NAMESPACES)
+            add_value_from_tag(interface, 'egress-type' , e_type)
 
             resource_key =  '/acl/interfaces/egress[{:s}][{:s}]'.format(
-                acl['name'], acl['type'])
+                name['name'], acl['type'])
             response.append((resource_key,interface))
     return response
diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py
index 8b8bc236a..b579094e3 100644
--- a/src/webui/service/device/routes.py
+++ b/src/webui/service/device/routes.py
@@ -165,6 +165,16 @@ def inventory(device_uuid: str):
     context_client.close()
     return render_template('device/inventory.html', device=device_obj)
 
+@device.route('logical/<path:device_uuid>', methods=['GET', 'POST'])
+def logical(device_uuid: str):
+    context_client.connect()
+    device_obj = get_device(context_client, device_uuid, rw_copy=False)
+    if device_obj is None:
+        flash('Device({:s}) not found'.format(str(device_uuid)), 'danger')
+        device_obj = Device()
+    context_client.close()
+    return render_template('device/logical.html', device=device_obj)
+
 @device.get('<path:device_uuid>/delete')
 def delete(device_uuid):
     try:
diff --git a/src/webui/service/templates/device/home.html b/src/webui/service/templates/device/home.html
index e356fd4fb..b6c50c8dd 100644
--- a/src/webui/service/templates/device/home.html
+++ b/src/webui/service/templates/device/home.html
@@ -51,6 +51,7 @@
             <th scope="col">Config Rules</th>
             <th scope="col"></th>
             <th scope="col"></th>
+            <th scope="col"></th>
           </tr>
         </thead>
         <tbody>
@@ -83,6 +84,14 @@
                               </svg>
                         </a>
                     </td>
+                    <td>
+                        <a href="{{ url_for('device.logical', device_uuid=device.device_id.device_uuid.uuid) }}">
+                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
+                                <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/>
+                                <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0"/>
+                              </svg>
+                        </a>
+                    </td>
                 </tr>
                 {% endfor %}
             {% else %}
diff --git a/src/webui/service/templates/device/logical.html b/src/webui/service/templates/device/logical.html
new file mode 100644
index 000000000..1287c20cf
--- /dev/null
+++ b/src/webui/service/templates/device/logical.html
@@ -0,0 +1,397 @@
+<!--
+    Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/)
+   
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+   
+         http://www.apache.org/licenses/LICENSE-2.0
+   
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+   -->
+
+{% extends 'base.html' %}
+
+{% block content %}
+<style>
+    ul,
+    #myUL {
+        list-style-type: none;
+    }
+
+    #myUL {
+        margin: 0;
+        padding: 0;
+    }
+
+    .caret {
+        cursor: pointer;
+        -webkit-user-select: none;
+        /* Safari 3.1+ */
+        -moz-user-select: none;
+        /* Firefox 2+ */
+        -ms-user-select: none;
+        /* IE 10+ */
+        user-select: none;
+    }
+
+    .caret::before {
+        content: "\25B6";
+        color: black;
+        display: inline-block;
+        margin-right: 6px;
+    }
+
+    .caret-down::before {
+        -ms-transform: rotate(90deg);
+        /* IE 9 */
+        -webkit-transform: rotate(90deg);
+        /* Safari */
+        transform: rotate(90deg);
+    }
+
+    .nested {
+        display: none;
+    }
+
+    .active {
+        display: block;
+    }
+</style>
+
+<h1>Device {{ device.name }} ({{ device.device_id.device_uuid.uuid }})</h1>
+
+<div class="row mb-3">
+    <div class="col-sm-3">
+        <button type="button" class="btn btn-success" onclick="window.location.href='{{ url_for('device.home') }}'">
+            <i class="bi bi-box-arrow-in-left"></i>
+            Back to device list
+        </button>
+    </div>
+</div>
+<br>
+
+<div class="row mb-3">
+    <div>
+        <ul id="myUL">
+            <li><span class="caret">ACL</span>
+                <ul class="nested">
+                    {% set acl_names = [] %}
+                    {% for config in device.device_config.config_rules %}
+                        {% if config.WhichOneof('config_rule') == 'custom' %}
+                            {% if '/acl/' in config.custom.resource_key %}
+                                {% if 'acl-set' in config.custom.resource_key %}
+                                    {% set acl_name = config.custom.resource_key.split('acl-set[')[1].split('][')[0] %}
+                                {% else %}
+                                    {% set acl_name = config.custom.resource_key.split('ress[')[1].split('][')[0] %}
+                                {% endif %}
+                                {% if acl_name|length == 0 %} 
+                                    {% set acl_name = 'Undefined' %}
+                                {% endif %}
+                                {% if acl_name not in acl_names %}
+                                    {% set _ = acl_names.append(acl_name) %}
+                                {% endif %}
+                            {% endif %}
+                        {% endif %}
+                    {% endfor %}
+                    {% for acl_name in acl_names %}
+                        <li><span class="caret">{{ acl_name }}</span>
+                            <ul class="nested">
+                                {% for config in device.device_config.config_rules %}
+                                    {% if config.WhichOneof('config_rule') == 'custom' %}
+                                        {% if '/acl/' in config.custom.resource_key and acl_name in config.custom.resource_key.split('][')[0] %}
+                                            {% if 'acl-entry' in config.custom.resource_key %}
+                                                {% set rule_number = config.custom.resource_key.split('acl-entry[')[1].split(']')[0] %}
+                                                <li><span><b>Rule {{ rule_number }}:</b> {{ config.custom.resource_value }}</span></li>
+                                            {% else %}
+                                                <li><span><b>Interface:</b> {{ config.custom.resource_value }}</span></li>
+                                            {% endif %}
+                                        {% endif %}
+                                    {% endif %}
+                                {% endfor %}
+                            </ul>
+                        </li>
+                    {% endfor %}
+                </ul>
+            </li>
+        </ul>
+
+        <ul id="myUL">
+            <li><span class="caret">Routing Policy</span>
+                <ul class="nested">
+                    {% set pol_names = [] %}
+                    {% for config in device.device_config.config_rules %}
+                        {% if config.WhichOneof('config_rule') == 'custom' %}
+                            {% if '/routing_policy/' in config.custom.resource_key %}
+                                {% if 'policy_definition' in config.custom.resource_key %}
+                                    {% set pol_name = config.custom.resource_key.split('policy_definition[')[1].split(']')[0] %}
+                                {% endif %}
+                                {% if pol_name|length == 0 %} 
+                                    {% set pol_name = 'Undefined' %}
+                                {% endif %}
+                                {% if pol_name not in pol_names %}
+                                    {% set _ = pol_names.append(pol_name) %}
+                                {% endif %}
+                            {% endif %}
+                        {% endif %}
+                    {% endfor %}
+                    {% for pol_name in pol_names %}
+                        <li><span class="caret">{{ pol_name }}</span>
+                            <ul class="nested">
+                            {% for config in device.device_config.config_rules %}
+                                {% if config.WhichOneof('config_rule') == 'custom' %}
+                                    {% if '/routing_policy/' in config.custom.resource_key and pol_name in config.custom.resource_key.split('[')[1].split(']')[0] %}
+                                        {% if 'policy_definition' not  in config.custom.resource_key %}
+                                            <li><span>{{ config.custom.resource_value }}</span></li>
+                                        {% endif %}
+                                    {% endif %}
+                                {% endif %}
+                            {% endfor %}
+                            </ul>
+                        </li>
+                    {% endfor %}
+                </ul>
+            </li>
+        </ul>
+
+        <ul id="myUL">
+            <li><span class="caret">VRFs</span>
+                <ul class="nested">
+                    <li><span class="caret">VRF default</span>
+                        <ul class="nested">
+                            {% for config in device.device_config.config_rules %}
+                                {% if config.WhichOneof('config_rule') == 'custom' %}
+                                    {% if '/network_instance' in config.custom.resource_key and  config.custom.resource_key.split('[')[1].split(']')[0] in 'default' %}
+                                        {% if ']/' in config.custom.resource_key%}
+                                            {% set aux = config.custom.resource_key.split(']/')[1].split('[')[0] %}
+                                            <li><span><b> {{ aux.replace('_', ' ').title() }}:</b> {{ config.custom.resource_value }}</span></li>
+                                        {% else %}
+                                            <li><span><b> Network Instance:</b> {{ config.custom.resource_value }}</span></li>
+                                        {% endif %}
+                                    {% endif %}
+                                {% endif %}
+                            {% endfor %}
+                        </ul>
+                    </li>
+
+                    <li><span class="caret">L3VPN</span>
+                        <ul class="nested">
+                            {% set vpn_names = [] %}
+                            {% for config in device.device_config.config_rules %}
+                                {% if config.WhichOneof('config_rule') == 'custom' %}
+                                    {% if '/network_instance' in config.custom.resource_key %}
+
+                                        {% if 'L3VRF' in config.custom.resource_value %}
+                                            {% set vpn_name = config.custom.resource_key.split('network_instance[')[1].split(']')[0] %}
+                                            {% if vpn_name not in vpn_names %}
+                                                {% set _ = vpn_names.append(vpn_name) %}
+                                            {% endif %}
+                                        {% endif %}
+                                    {% endif %}
+                                {% endif %}
+                            {% endfor %}
+                            {% for vpn_name in vpn_names %}
+                                <li><span class="caret">{{ vpn_name }}</span>
+                                    <ul class="nested">
+                                        {% for config in device.device_config.config_rules %}
+                                            {% if config.WhichOneof('config_rule') == 'custom' %}
+                                                {% if '/network_instance' in config.custom.resource_key and  config.custom.resource_key.split('[')[1].split(']')[0] in vpn_name %}
+                                                    {% if ']/' in config.custom.resource_key%}
+                                                        {% set aux = config.custom.resource_key.split(']/')[1].split('[')[0] %}
+                                                        <li><span><b> {{ aux.replace('_', ' ').title() }}:</b> {{ config.custom.resource_value }}</span></li>
+                                                    {% else %}
+                                                        <li><span><b> Network Instance:</b> {{ config.custom.resource_value }}</span></li>
+                                                    {% endif %}
+                                                {% endif %}
+                                            {% endif %}
+                                        {% endfor %}
+                                    </ul>
+                                </li>
+                            {% endfor %}
+                        </ul>
+                    </li>
+
+                    <li><span class="caret">L2VPN</span>
+                        <ul class="nested">
+                            {% set vpn_names = [] %}
+                            {% for config in device.device_config.config_rules %}
+                                {% if config.WhichOneof('config_rule') == 'custom' %}
+                                    {% if '/network_instance' in config.custom.resource_key %}
+        
+                                        {% if 'L2VSI' in config.custom.resource_value %}
+                                            {% set vpn_name = config.custom.resource_key.split('network_instance[')[1].split(']')[0] %}
+                                            {% if vpn_name not in vpn_names %}
+                                                {% set _ = vpn_names.append(vpn_name) %}
+                                            {% endif %}
+                                        {% endif %}
+                                    {% endif %}
+                                {% endif %}
+                            {% endfor %}
+                            {% for vpn_name in vpn_names %}
+                                <li><span class="caret">{{ vpn_name }}</span>
+                                    <ul class="nested">
+                                        {% for config in device.device_config.config_rules %}
+                                            {% if config.WhichOneof('config_rule') == 'custom' %}
+                                                {% if '/network_instance' in config.custom.resource_key and  config.custom.resource_key.split('[')[1].split(']')[0] in vpn_name %}
+                                                    {% if ']/' in config.custom.resource_key%}
+                                                        {% set aux = config.custom.resource_key.split(']/')[1].split('[')[0] %}
+                                                        <li><span><b> {{ aux.replace('_', ' ').title() }}:</b> {{ config.custom.resource_value }}</span></li>
+                                                    {% else %}
+                                                        <li><span><b> Network Instance:</b> {{ config.custom.resource_value }}</span></li>
+                                                    {% endif %}
+                                                {% endif %}
+                                            {% endif %}
+                                        {% endfor %}
+                                    </ul>
+                                </li>
+                            {% endfor %}
+                        </ul>
+                    </li>
+                </ul>
+            </li>
+        </ul>
+
+        <ul id="myUL">
+            <li><span class="caret">Interfaces</span>
+                <ul class="nested">
+                    <li><span class="caret">Logical Interfaces</span>
+                        <ul class="nested">
+                            {% set interface_names = [] %}
+                            {% for config in device.device_config.config_rules %}
+                                {% if config.WhichOneof('config_rule') == 'custom' %}
+                                    {% if '/interface[' in config.custom.resource_key %}
+                                        {% if 'ethernetCsmacd' in config.custom.resource_value %}
+                                            {% set interface_name = config.custom.resource_key.split('interface[')[1].split(']')[0] %}
+                                            <li><span>{{ interface_name}}:</span> {{config.custom.resource_value}}</li>     
+                                        {% endif %}
+                                    {% endif %}
+                                {% endif %}
+                            {% endfor %}
+                        </ul>
+                    </li>
+
+                    <li><span class="caret">Loopback</span>
+                        <ul class="nested">
+                            {% set interface_names = [] %}
+                            {% for config in device.device_config.config_rules %}
+                                {% if config.WhichOneof('config_rule') == 'custom' %}
+                                    {% if '/interface[' in config.custom.resource_key %}
+                                        {% if 'softwareLoopback' in config.custom.resource_value %}
+                                            {% set interface_name = config.custom.resource_key.split('interface[')[1].split(']')[0] %}
+                                            {% if interface_name not in interface_names %}
+                                                {% set _ = interface_names.append(interface_name) %}
+                                            {% endif %}
+                                        {% endif %}
+                                    {% endif %}
+                                {% endif %}
+                            {% endfor %}
+                            {% for interface_name in interface_names %}
+                                <li><span class="caret">{{ interface_name }}</span>
+                                    <ul class="nested">
+                                        {% for config in device.device_config.config_rules %}
+                                            {% if config.WhichOneof('config_rule') == 'custom' %}
+                                                {% if '/interface' in config.custom.resource_key and config.custom.resource_key.split('[')[1].split(']')[0] in interface_name %}
+                                                    {% if 'subinterface' in config.custom.resource_key %}
+                                                        {% set subinterface_name = config.custom.resource_key.split('subinterface[')[1].split(']')[0] %}
+                                                        <li><span><b>Subinterface {{subinterface_name}}: </b>{{ config.custom.resource_value }}</span></li>
+                                                    {% else %}
+                                                        <li><span>{{ config.custom.resource_value }}</span></li>
+                                                    {% endif %}
+                                                {% endif %}
+                                            {% endif %}
+                                        {% endfor %}
+                                    </ul>
+                                </li>
+                            {% endfor %}
+                        </ul>
+                    </li>
+
+                    <li><span class="caret">Interfaces L3</span>
+                        <ul class="nested">
+                            {% set interface_names = [] %}
+                            {% for config in device.device_config.config_rules %}
+                                {% if config.WhichOneof('config_rule') == 'custom' %}
+                                    {% if '/interface[' in config.custom.resource_key %}
+                                        {% if 'l3ipvlan' in config.custom.resource_value %}
+                                            {% set interface_name = config.custom.resource_key.split('interface[')[1].split(']')[0] %}
+                                            {% if interface_name not in interface_names %}
+                                                {% set _ = interface_names.append(interface_name) %}
+                                            {% endif %}
+                                        {% endif %}
+                                    {% endif %}
+                                {% endif %}
+                            {% endfor %}
+                            {% for interface_name in interface_names %}
+                                <li><span class="caret">{{ interface_name }}</span>
+                                    <ul class="nested">
+                                        {% for config in device.device_config.config_rules %}
+                                            {% if config.WhichOneof('config_rule') == 'custom' %}
+                                                {% if '/interface' in config.custom.resource_key and  '/subinterface' in config.custom.resource_key and config.custom.resource_key.split('[')[1].split(']')[0] in interface_name %}
+                                                    <li><span>{{ config.custom.resource_value }}</span></li>
+                                                {% endif %}
+                                            {% endif %}
+                                        {% endfor %}
+                                    </ul>
+                                </li>
+                            {% endfor %}
+                        </ul>
+                    </li>
+
+                    <li><span class="caret">Interfaces L2</span>
+                        <ul class="nested">
+                            {% set interface_names = [] %}
+                            {% for config in device.device_config.config_rules %}
+                                {% if config.WhichOneof('config_rule') == 'custom' %}
+                                    {% if '/interface[' in config.custom.resource_key %}
+                                        {% if 'l2vlan' in config.custom.resource_value or 'mplsTunnel' in config.custom.resource_value %}
+                                            {% set interface_name = config.custom.resource_key.split('interface[')[1].split(']')[0] %}
+                                            {% if interface_name not in interface_names %}
+                                                {% set _ = interface_names.append(interface_name) %}
+                                            {% endif %}
+                                        {% endif %}
+                                    {% endif %}
+                                {% endif %}
+                            {% endfor %}
+                            {% for interface_name in interface_names %}
+                                <li><span class="caret">{{ interface_name }}</span>
+                                    <ul class="nested">
+                                        {% for config in device.device_config.config_rules %}
+                                            {% if config.WhichOneof('config_rule') == 'custom' %}
+                                                {% if 'subinterface' in config.custom.resource_key %}
+                                                    {% if '/interface' in config.custom.resource_key and  '/subinterface' in config.custom.resource_key and config.custom.resource_key.split('[')[1].split(']')[0] in interface_name %}
+                                                        <li><span>{{ config.custom.resource_value }}</span></li>
+                                                    {% endif %}
+                                                {% else %}
+                                                    {% if '/interface' in config.custom.resource_key and config.custom.resource_key.split('[')[1].split(']')[0] in interface_name %}
+                                                        <li><span>{{ config.custom.resource_value }}</span></li>
+                                                    {% endif %}
+                                                {% endif %}
+                                            {% endif %}
+                                        {% endfor %}
+                                    </ul>
+                                </li>
+                            {% endfor %}
+                        </ul>
+                    </li>
+                </ul>
+            </li>
+        </ul>
+
+        <script>
+            var toggler = document.getElementsByClassName("caret");
+            var i;
+            for (i = 0; i < toggler.length; i++) {
+                toggler[i].addEventListener("click", function() {
+                    this.parentElement.querySelector(".nested").classList.toggle("active");
+                    this.classList.toggle("caret-down");
+                });
+            }
+        </script>
+    </div>
+</div>
+
+{% endblock %}
-- 
GitLab