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