diff --git a/scripts/run_tests_locally-device-gnmi-openconfig.sh b/scripts/run_tests_locally-device-gnmi-openconfig.sh new file mode 100755 index 0000000000000000000000000000000000000000..d81684da10fd259fbd4f8c052a5b0218f71b2021 --- /dev/null +++ b/scripts/run_tests_locally-device-gnmi-openconfig.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# 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. + + +PROJECTDIR=`pwd` + +cd $PROJECTDIR/src +RCFILE=$PROJECTDIR/coverage/.coveragerc + +# Run unitary tests and analyze coverage of code at same time +# helpful pytest flags: --log-level=INFO -o log_cli=true --verbose --maxfail=1 --durations=0 +coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \ + device/tests/test_unitary_gnmi_openconfig.py diff --git a/src/device/service/drivers/gnmi_openconfig/GnmiSessionHandler.py b/src/device/service/drivers/gnmi_openconfig/GnmiSessionHandler.py index 6f80ee82faf3cb7d094a92951d79cae0cd44669e..d9f73c9583ac79db1187e76719a7c272b0d1af9a 100644 --- a/src/device/service/drivers/gnmi_openconfig/GnmiSessionHandler.py +++ b/src/device/service/drivers/gnmi_openconfig/GnmiSessionHandler.py @@ -19,12 +19,13 @@ from common.type_checkers.Checkers import chk_float, chk_length, chk_string, chk from .gnmi.gnmi_pb2_grpc import gNMIStub from .gnmi.gnmi_pb2 import Encoding, GetRequest, SetRequest, UpdateResult # pylint: disable=no-name-in-module from .handlers import ALL_RESOURCE_KEYS, compose, get_path, parse -from .tools.Capabilities import get_supported_encodings +from .handlers.YangHandler import YangHandler +from .tools.Capabilities import check_capabilities from .tools.Channel import get_grpc_channel from .tools.Path import path_from_string, path_to_string #, compose_path from .tools.Subscriptions import Subscriptions from .tools.Value import decode_value #, value_exists -from .MonitoringThread import MonitoringThread +#from .MonitoringThread import MonitoringThread class GnmiSessionHandler: def __init__(self, address : str, port : int, settings : Dict, logger : logging.Logger) -> None: @@ -39,12 +40,20 @@ class GnmiSessionHandler: self._use_tls = settings.get('use_tls', False) self._channel : Optional[grpc.Channel] = None self._stub : Optional[gNMIStub] = None - self._monit_thread = None - self._supported_encodings = None + self._yang_handler = YangHandler() + #self._monit_thread = None self._subscriptions = Subscriptions() self._in_subscriptions = queue.Queue() self._out_samples = queue.Queue() + def __del__(self) -> None: + self._logger.warning('Destroying YangValidator...') + self._logger.warning('yang_validator.data:') + for path, dnode in self._yang_handler.get_data_paths().items(): + self._logger.warning(' {:s}: {:s}'.format(str(path), json.dumps(dnode.print_dict()))) + self._yang_handler.destroy() + self._logger.warning('DONE') + @property def subscriptions(self): return self._subscriptions @@ -58,18 +67,17 @@ class GnmiSessionHandler: with self._lock: self._channel = get_grpc_channel(self._address, self._port, self._use_tls, self._logger) self._stub = gNMIStub(self._channel) - self._supported_encodings = get_supported_encodings( - self._stub, self._username, self._password, timeout=120) - self._monit_thread = MonitoringThread( - self._stub, self._logger, self._settings, self._in_subscriptions, self._out_samples) - self._monit_thread.start() + check_capabilities(self._stub, self._username, self._password, timeout=120) + #self._monit_thread = MonitoringThread( + # self._stub, self._logger, self._settings, self._in_subscriptions, self._out_samples) + #self._monit_thread.start() self._connected.set() def disconnect(self): if not self._connected.is_set(): return with self._lock: - self._monit_thread.stop() - self._monit_thread.join() + #self._monit_thread.stop() + #self._monit_thread.join() self._channel.close() self._connected.clear() @@ -87,9 +95,9 @@ class GnmiSessionHandler: str_resource_name = 'resource_key[#{:d}]'.format(i) try: chk_string(str_resource_name, resource_key, allow_empty=False) - self._logger.debug('[GnmiSessionHandler:get] resource_key = {:s}'.format(str(resource_key))) + #self._logger.debug('[GnmiSessionHandler:get] resource_key = {:s}'.format(str(resource_key))) str_path = get_path(resource_key) - self._logger.debug('[GnmiSessionHandler:get] str_path = {:s}'.format(str(str_path))) + #self._logger.debug('[GnmiSessionHandler:get] str_path = {:s}'.format(str(str_path))) get_request.path.append(path_from_string(str_path)) except Exception as e: # pylint: disable=broad-except MSG = 'Exception parsing {:s}: {:s}' @@ -130,7 +138,7 @@ class GnmiSessionHandler: value = decode_value(update.val) #resource_key_tuple[1] = value #resource_key_tuple[2] = True - results.extend(parse(str_path, value)) + results.extend(parse(str_path, value, self._yang_handler)) except Exception as e: # pylint: disable=broad-except MSG = 'Exception processing update {:s}' self._logger.exception(MSG.format(grpc_message_to_json_string(update))) @@ -159,17 +167,17 @@ class GnmiSessionHandler: set_request = SetRequest() #for resource_key in resource_keys: for resource_key, resource_value in resources: - self._logger.info('---1') - self._logger.info(str(resource_key)) - self._logger.info(str(resource_value)) + #self._logger.info('---1') + #self._logger.info(str(resource_key)) + #self._logger.info(str(resource_value)) #resource_tuple = resource_tuples.get(resource_key) #if resource_tuple is None: continue #_, value, exists, operation_done = resource_tuple if isinstance(resource_value, str): resource_value = json.loads(resource_value) - str_path, str_data = compose(resource_key, resource_value, delete=False) - self._logger.info('---3') - self._logger.info(str(str_path)) - self._logger.info(str(str_data)) + str_path, str_data = compose(resource_key, resource_value, self._yang_handler, delete=False) + #self._logger.info('---3') + #self._logger.info(str(str_path)) + #self._logger.info(str(str_data)) set_request_list = set_request.update #if exists else set_request.replace set_request_entry = set_request_list.add() set_request_entry.path.CopyFrom(path_from_string(str_path)) @@ -228,18 +236,19 @@ class GnmiSessionHandler: set_request = SetRequest() #for resource_key in resource_keys: for resource_key, resource_value in resources: - self._logger.info('---1') - self._logger.info(str(resource_key)) - self._logger.info(str(resource_value)) + #self._logger.info('---1') + #self._logger.info(str(resource_key)) + #self._logger.info(str(resource_value)) #resource_tuple = resource_tuples.get(resource_key) #if resource_tuple is None: continue #_, value, exists, operation_done = resource_tuple #if not exists: continue if isinstance(resource_value, str): resource_value = json.loads(resource_value) - str_path, str_data = compose(resource_key, resource_value, delete=True) - self._logger.info('---3') - self._logger.info(str(str_path)) - self._logger.info(str(str_data)) + # pylint: disable=unused-variable + str_path, str_data = compose(resource_key, resource_value, self._yang_handler, delete=True) + #self._logger.info('---3') + #self._logger.info(str(str_path)) + #self._logger.info(str(str_data)) set_request_entry = set_request.delete.add() set_request_entry.CopyFrom(path_from_string(str_path)) diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/Component.py b/src/device/service/drivers/gnmi_openconfig/handlers/Component.py index cddf40d56454582942485ab49b00744b70c2d69f..73728192f3089bedffb092d0bea35f9ed6713cf7 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/Component.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/Component.py @@ -12,37 +12,44 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging #, json -import pyangbind.lib.pybindJSON as pybindJSON +import json, logging # libyang from typing import Any, Dict, List, Tuple from common.proto.kpi_sample_types_pb2 import KpiSampleType -from . import openconfig from ._Handler import _Handler +from .YangHandler import YangHandler LOGGER = logging.getLogger(__name__) -PATH_IF_CTR = "/openconfig-interfaces:interfaces/interface[name={:s}]/state/counters/{:s}" +PATH_IF_CTR = '/openconfig-interfaces:interfaces/interface[name={:s}]/state/counters/{:s}' #pylint: disable=abstract-method class ComponentHandler(_Handler): def get_resource_key(self) -> str: return '/endpoints/endpoint' def get_path(self) -> str: return '/openconfig-platform:components' - def parse(self, json_data : Dict) -> List[Tuple[str, Dict[str, Any]]]: - #LOGGER.info('json_data = {:s}'.format(json.dumps(json_data))) + def parse( + self, json_data : Dict, yang_handler : YangHandler + ) -> List[Tuple[str, Dict[str, Any]]]: + LOGGER.debug('json_data = {:s}'.format(json.dumps(json_data))) - oc_components = pybindJSON.loads_ietf(json_data, openconfig.components, 'components') - #LOGGER.info('oc_components = {:s}'.format(pybindJSON.dumps(oc_components, mode='ietf'))) + yang_components_path = self.get_path() + json_data_valid = yang_handler.parse_to_dict(yang_components_path, json_data, fmt='json') entries = [] - for component_key, oc_component in oc_components.component.items(): - #LOGGER.info('component_key={:s} oc_component={:s}'.format( - # component_key, pybindJSON.dumps(oc_component, mode='ietf') - #)) + for component in json_data_valid['components']['component']: + LOGGER.debug('component={:s}'.format(str(component))) - component_name = oc_component.config.name + component_name = component['name'] + #component_config = component.get('config', {}) - component_type = oc_component.state.type + #yang_components : libyang.DContainer = yang_handler.get_data_path(yang_components_path) + #yang_component_path = 'component[name="{:s}"]'.format(component_name) + #yang_component : libyang.DContainer = yang_components.create_path(yang_component_path) + #yang_component.merge_data_dict(component, strict=True, validate=False) + + component_state = component.get('state', {}) + component_type = component_state.get('type') + if component_type is None: continue component_type = component_type.split(':')[-1] if component_type not in {'PORT'}: continue @@ -58,8 +65,6 @@ class ComponentHandler(_Handler): KpiSampleType.KPISAMPLETYPE_PACKETS_TRANSMITTED: PATH_IF_CTR.format(interface_name, 'out-pkts' ), } - if len(endpoint) == 0: continue - entries.append(('/endpoints/endpoint[{:s}]'.format(endpoint['uuid']), endpoint)) - + return entries diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py b/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py index 77310d51d544339f10858445b0260f645cfdee60..f28cdcf361e468fb30134c6cce806e8617b966f4 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json, logging -import pyangbind.lib.pybindJSON as pybindJSON +import json, libyang, logging from typing import Any, Dict, List, Tuple -from . import openconfig from ._Handler import _Handler +from .Tools import get_bool, get_int, get_str +from .YangHandler import YangHandler LOGGER = logging.getLogger(__name__) @@ -24,9 +24,11 @@ class InterfaceHandler(_Handler): def get_resource_key(self) -> str: return '/interface' def get_path(self) -> str: return '/openconfig-interfaces:interfaces' - def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: - if_name = str (resource_value['name' ]) # ethernet-1/1 - sif_index = int (resource_value.get('sub_if_index' , 0 )) # 0 + def compose( + self, resource_key : str, resource_value : Dict, yang_handler : YangHandler, delete : bool = False + ) -> Tuple[str, str]: + if_name = get_str(resource_value, 'name' ) # ethernet-1/1 + sif_index = get_int(resource_value, 'sub_if_index', 0) # 0 if delete: PATH_TMPL = '/interfaces/interface[name={:s}]/subinterfaces/subinterface[index={:d}]' @@ -34,118 +36,166 @@ class InterfaceHandler(_Handler): str_data = json.dumps({}) return str_path, str_data - if_enabled = bool(resource_value.get('enabled' , True)) # True/False - sif_enabled = bool(resource_value.get('sub_if_enabled' , True)) # True/False - sif_ipv4_enabled = bool(resource_value.get('sub_if_ipv4_enabled', True)) # True/False - sif_ipv4_address = str (resource_value['sub_if_ipv4_address' ]) # 172.16.0.1 - sif_ipv4_prefix = int (resource_value['sub_if_ipv4_prefix' ]) # 24 + if_enabled = get_bool(resource_value, 'enabled', True) # True/False + sif_enabled = get_bool(resource_value, 'sub_if_enabled', True) # True/False + sif_vlan_id = get_int (resource_value, 'sif_vlan_id', ) # 127 + sif_ipv4_enabled = get_bool(resource_value, 'sub_if_ipv4_enabled', True) # True/False + sif_ipv4_address = get_str (resource_value, 'sub_if_ipv4_address' ) # 172.16.0.1 + sif_ipv4_prefix = get_int (resource_value, 'sub_if_ipv4_prefix' ) # 24 + + yang_ifs : libyang.DContainer = yang_handler.get_data_path('/openconfig-interfaces:interfaces') + yang_if_path = 'interface[name="{:s}"]'.format(if_name) + yang_if : libyang.DContainer = yang_ifs.create_path(yang_if_path) + yang_if.create_path('config/name', if_name ) + if if_enabled is not None: yang_if.create_path('config/enabled', if_enabled) + + yang_sifs : libyang.DContainer = yang_if.create_path('subinterfaces') + yang_sif_path = 'subinterface[index="{:d}"]'.format(sif_index) + yang_sif : libyang.DContainer = yang_sifs.create_path(yang_sif_path) + yang_sif.create_path('config/index', sif_index) + if sif_enabled is not None: yang_sif.create_path('config/enabled', sif_enabled) + + if sif_vlan_id is not None: + yang_subif_vlan : libyang.DContainer = yang_sif.create_path('openconfig-vlan:vlan') + yang_subif_vlan.create_path('match/single-tagged/config/vlan-id', sif_vlan_id) + + yang_ipv4 : libyang.DContainer = yang_sif.create_path('openconfig-if-ip:ipv4') + if sif_ipv4_enabled is not None: yang_ipv4.create_path('config/enabled', sif_ipv4_enabled) + + if sif_ipv4_address is not None: + yang_ipv4_addrs : libyang.DContainer = yang_ipv4.create_path('addresses') + yang_ipv4_addr_path = 'address[ip="{:s}"]'.format(sif_ipv4_address) + yang_ipv4_addr : libyang.DContainer = yang_ipv4_addrs.create_path(yang_ipv4_addr_path) + yang_ipv4_addr.create_path('config/ip', sif_ipv4_address) + yang_ipv4_addr.create_path('config/prefix-length', sif_ipv4_prefix ) str_path = '/interfaces/interface[name={:s}]'.format(if_name) - str_data = json.dumps({ - 'name': if_name, - 'config': {'name': if_name, 'enabled': if_enabled}, - 'subinterfaces': { - 'subinterface': { - 'index': sif_index, - 'config': {'index': sif_index, 'enabled': sif_enabled}, - 'ipv4': { - 'config': {'enabled': sif_ipv4_enabled}, - 'addresses': { - 'address': { - 'ip': sif_ipv4_address, - 'config': {'ip': sif_ipv4_address, 'prefix_length': sif_ipv4_prefix}, - } - } - } - } - } - }) + str_data = yang_if.print_mem('json') + json_data = json.loads(str_data) + json_data = json_data['openconfig-interfaces:interface'][0] + str_data = json.dumps(json_data) return str_path, str_data - def parse(self, json_data : Dict) -> List[Tuple[str, Dict[str, Any]]]: - #LOGGER.info('json_data = {:s}'.format(json.dumps(json_data))) - oc_interfaces = pybindJSON.loads_ietf(json_data, openconfig.interfaces, 'interfaces') - #LOGGER.info('oc_interfaces = {:s}'.format(pybindJSON.dumps(oc_interfaces, mode='ietf'))) + def parse( + self, json_data : Dict, yang_handler : YangHandler + ) -> List[Tuple[str, Dict[str, Any]]]: + LOGGER.debug('json_data = {:s}'.format(json.dumps(json_data))) + + yang_interfaces_path = self.get_path() + json_data_valid = yang_handler.parse_to_dict(yang_interfaces_path, json_data, fmt='json') entries = [] - for interface_key, oc_interface in oc_interfaces.interface.items(): - #LOGGER.info('interface_key={:s} oc_interfaces={:s}'.format( - # interface_key, pybindJSON.dumps(oc_interface, mode='ietf') - #)) - - interface = {} - interface['name'] = oc_interface.config.name - - interface_type = oc_interface.config.type - interface_type = interface_type.replace('ianaift:', '') - interface_type = interface_type.replace('iana-if-type:', '') - interface['type'] = interface_type - - interface['mtu' ] = oc_interface.config.mtu - interface['enabled' ] = oc_interface.config.enabled - interface['description' ] = oc_interface.config.description - interface['admin-status'] = oc_interface.state.admin_status - interface['oper-status' ] = oc_interface.state.oper_status - interface['management' ] = oc_interface.state.management - - entry_interface_key = '/interface[{:s}]'.format(interface['name']) - entries.append((entry_interface_key, interface)) - - for subinterface_key, oc_subinterface in oc_interface.subinterfaces.subinterface.items(): - #LOGGER.info('subinterface_key={:d} oc_subinterfaces={:s}'.format( - # subinterface_key, pybindJSON.dumps(oc_subinterface, mode='ietf') - #)) - - subinterface = {} - subinterface['index' ] = oc_subinterface.state.index - subinterface['name' ] = oc_subinterface.state.name - subinterface['enabled'] = oc_subinterface.state.enabled - - entry_subinterface_key = '{:s}/subinterface[{:d}]'.format(entry_interface_key, subinterface['index']) - entries.append((entry_subinterface_key, subinterface)) - - #VLAN_FIELDS = ('vlan', 'openconfig-vlan:vlan', 'ocv:vlan') - #json_vlan = dict_get_first(json_subinterface, VLAN_FIELDS, default={}) - - #MATCH_FIELDS = ('match', 'openconfig-vlan:match', 'ocv:match') - #json_vlan = dict_get_first(json_vlan, MATCH_FIELDS, default={}) - - #SIN_TAG_FIELDS = ('single-tagged', 'openconfig-vlan:single-tagged', 'ocv:single-tagged') - #json_vlan = dict_get_first(json_vlan, SIN_TAG_FIELDS, default={}) - - #CONFIG_FIELDS = ('config', 'openconfig-vlan:config', 'ocv:config') - #json_vlan = dict_get_first(json_vlan, CONFIG_FIELDS, default={}) - - #VLAN_ID_FIELDS = ('vlan-id', 'openconfig-vlan:vlan-id', 'ocv:vlan-id') - #subinterface_vlan_id = dict_get_first(json_vlan, VLAN_ID_FIELDS) - #if subinterface_vlan_id is not None: subinterface['vlan_id'] = subinterface_vlan_id - - for address_key, oc_address in oc_subinterface.ipv4.addresses.address.items(): - #LOGGER.info('ipv4: address_key={:s} oc_address={:s}'.format( - # address_key, pybindJSON.dumps(oc_address, mode='ietf') - #)) - - address_ipv4 = { - 'ip' : oc_address.state.ip, - 'origin': oc_address.state.origin, - 'prefix': oc_address.state.prefix_length, - } - - entry_address_ipv4_key = '{:s}/ipv4[{:s}]'.format(entry_subinterface_key, address_ipv4['ip']) - entries.append((entry_address_ipv4_key, address_ipv4)) - - for address_key, oc_address in oc_subinterface.ipv6.addresses.address.items(): - #LOGGER.info('ipv6: address_key={:s} oc_address={:s}'.format( - # address_key, pybindJSON.dumps(oc_address, mode='ietf') - #)) - - address_ipv6 = { - 'ip' : oc_address.state.ip, - 'origin': oc_address.state.origin, - 'prefix': oc_address.state.prefix_length, - } - - entry_address_ipv6_key = '{:s}/ipv6[{:s}]'.format(entry_subinterface_key, address_ipv6['ip']) - entries.append((entry_address_ipv6_key, address_ipv6)) + for interface in json_data_valid['interfaces']['interface']: + LOGGER.debug('interface={:s}'.format(str(interface))) + + interface_name = interface['name'] + interface_config = interface.get('config', {}) + + #yang_interfaces : libyang.DContainer = yang_handler.get_data_path(yang_interfaces_path) + #yang_interface_path = 'interface[name="{:s}"]'.format(interface_name) + #yang_interface : libyang.DContainer = yang_interfaces.create_path(yang_interface_path) + #yang_interface.merge_data_dict(interface, strict=True, validate=False) + + interface_state = interface.get('state', {}) + interface_type = interface_state.get('type') + if interface_type is None: continue + interface_type = interface_type.split(':')[-1] + if interface_type not in {'ethernetCsmacd'}: continue + + _interface = { + 'name' : interface_name, + 'type' : interface_type, + 'mtu' : interface_state['mtu'], + 'ifindex' : interface_state['ifindex'], + 'admin-status' : interface_state['admin-status'], + 'oper-status' : interface_state['oper-status'], + 'management' : interface_state['management'], + } + if 'description' in interface_config: + _interface['description'] = interface_config['description'] + if 'enabled' in interface_config: + _interface['enabled'] = interface_config['enabled'] + if 'hardware-port' in interface_state: + _interface['hardware-port'] = interface_state['hardware-port'] + if 'transceiver' in interface_state: + _interface['transceiver'] = interface_state['transceiver'] + + entry_interface_key = '/interface[{:s}]'.format(interface_name) + entries.append((entry_interface_key, _interface)) + + if interface_type == 'ethernetCsmacd': + ethernet_state = interface['ethernet']['state'] + + _ethernet = { + 'mac-address' : ethernet_state['mac-address'], + 'hw-mac-address' : ethernet_state['hw-mac-address'], + 'port-speed' : ethernet_state['port-speed'].split(':')[-1], + 'negotiated-port-speed' : ethernet_state['negotiated-port-speed'].split(':')[-1], + } + entry_ethernet_key = '{:s}/ethernet'.format(entry_interface_key) + entries.append((entry_ethernet_key, _ethernet)) + + subinterfaces = interface.get('subinterfaces', {}).get('subinterface', []) + for subinterface in subinterfaces: + LOGGER.debug('subinterface={:s}'.format(str(subinterface))) + + subinterface_index = subinterface['index'] + subinterface_state = subinterface.get('state', {}) + + _subinterface = {'index': subinterface_index} + if 'name' in subinterface_state: + _subinterface['name'] = subinterface_state['name'] + if 'enabled' in subinterface_state: + _subinterface['enabled'] = subinterface_state['enabled'] + entry_subinterface_key = '{:s}/subinterface[{:d}]'.format(entry_interface_key, subinterface_index) + entries.append((entry_subinterface_key, _subinterface)) + + if 'vlan' in subinterface: + vlan = subinterface['vlan'] + vlan_match = vlan['match'] + + single_tagged = vlan_match.pop('single-tagged', None) + if single_tagged is not None: + single_tagged_config = single_tagged['config'] + vlan_id = single_tagged_config['vlan-id'] + + _vlan = {'vlan_id': vlan_id} + entry_vlan_key = '{:s}/vlan[single:{:s}]'.format(entry_subinterface_key, vlan_id) + entries.append((entry_vlan_key, _vlan)) + + if len(vlan_match) > 0: + raise Exception('Unsupported VLAN schema: {:s}'.format(str(vlan))) + + ipv4_addresses = subinterface.get('ipv4', {}).get('addresses', {}).get('address', []) + for ipv4_address in ipv4_addresses: + LOGGER.debug('ipv4_address={:s}'.format(str(ipv4_address))) + + ipv4_address_ip = ipv4_address['ip'] + ipv4_address_state = ipv4_address.get('state', {}) + + _ipv4_address = {'ip': ipv4_address_ip} + if 'origin' in ipv4_address_state: + _ipv4_address['origin'] = ipv4_address_state['origin'] + if 'prefix-length' in ipv4_address_state: + _ipv4_address['prefix'] = ipv4_address_state['prefix-length'] + + entry_ipv4_address_key = '{:s}/ipv4[{:s}]'.format(entry_subinterface_key, ipv4_address_ip) + entries.append((entry_ipv4_address_key, _ipv4_address)) + + ipv6_addresses = subinterface.get('ipv6', {}).get('addresses', {}).get('address', []) + for ipv6_address in ipv6_addresses: + LOGGER.debug('ipv6_address={:s}'.format(str(ipv6_address))) + + ipv6_address_ip = ipv6_address['ip'] + ipv6_address_state = ipv6_address.get('state', {}) + + _ipv6_address = {'ip': ipv6_address_ip} + if 'origin' in ipv6_address_state: + _ipv6_address['origin'] = ipv6_address_state['origin'] + if 'prefix-length' in ipv6_address_state: + _ipv6_address['prefix'] = ipv6_address_state['prefix-length'] + + entry_ipv6_address_key = '{:s}/ipv6[{:s}]'.format(entry_subinterface_key, ipv6_address_ip) + entries.append((entry_ipv6_address_key, _ipv6_address)) return entries diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/NetworkInstance.py b/src/device/service/drivers/gnmi_openconfig/handlers/NetworkInstance.py index c29ed263a81246b9084c389664e6064d9bc12772..0b4d157452b66d74efadc883b5f59c49b4bee47b 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/NetworkInstance.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/NetworkInstance.py @@ -12,20 +12,40 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json, logging -import pyangbind.lib.pybindJSON as pybindJSON +import json, libyang, logging +import operator from typing import Any, Dict, List, Tuple -from . import openconfig from ._Handler import _Handler +from .Tools import get_bool, get_int, get_str +from .YangHandler import YangHandler LOGGER = logging.getLogger(__name__) +MAP_NETWORK_INSTANCE_TYPE = { + # special routing instance; acts as default/global routing instance for a network device + 'DEFAULT': 'openconfig-network-instance-types:DEFAULT_INSTANCE', + + # private L3-only routing instance; formed of one or more RIBs + 'L3VRF': 'openconfig-network-instance-types:L3VRF', + + # private L2-only switch instance; formed of one or more L2 forwarding tables + 'L2VSI': 'openconfig-network-instance-types:L2VSI', + + # private L2-only forwarding instance; point to point connection between two endpoints + 'L2P2P': 'openconfig-network-instance-types:L2P2P', + + # private Layer 2 and Layer 3 forwarding instance + 'L2L3': 'openconfig-network-instance-types:L2L3', +} + class NetworkInstanceHandler(_Handler): def get_resource_key(self) -> str: return '/network_instance' def get_path(self) -> str: return '/openconfig-network-instance:network-instances' - def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: - ni_name = str(resource_value['name']) # test-svc + def compose( + self, resource_key : str, resource_value : Dict, yang_handler : YangHandler, delete : bool = False + ) -> Tuple[str, str]: + ni_name = get_str(resource_value, 'name') # test-svc if delete: PATH_TMPL = '/network-instances/network-instance[name={:s}]' @@ -33,15 +53,11 @@ class NetworkInstanceHandler(_Handler): str_data = json.dumps({}) return str_path, str_data - ni_type = str(resource_value['type']) # L3VRF / L2VSI / ... + ni_type = get_str(resource_value, 'type') # L3VRF / L2VSI / ... + ni_type = MAP_NETWORK_INSTANCE_TYPE.get(ni_type, ni_type) - # not works: [FailedPrecondition] unsupported identifier 'DIRECTLY_CONNECTED' - #protocols = [self._compose_directly_connected()] + # 'DIRECTLY_CONNECTED' is implicitly added - MAP_OC_NI_TYPE = { - 'L3VRF': 'openconfig-network-instance-types:L3VRF', - } - ni_type = MAP_OC_NI_TYPE.get(ni_type, ni_type) str_path = '/network-instances/network-instance[name={:s}]'.format(ni_name) str_data = json.dumps({ @@ -51,19 +67,92 @@ class NetworkInstanceHandler(_Handler): }) return str_path, str_data - def _compose_directly_connected(self, name=None, enabled=True) -> Dict: - identifier = 'DIRECTLY_CONNECTED' - if name is None: name = 'DIRECTLY_CONNECTED' - return { - 'identifier': identifier, 'name': name, - 'config': {'identifier': identifier, 'name': name, 'enabled': enabled}, - } - - def parse(self, json_data : Dict) -> List[Tuple[str, Dict[str, Any]]]: - LOGGER.info('json_data = {:s}'.format(json.dumps(json_data))) - oc_network_instances = pybindJSON.loads_ietf(json_data, openconfig., 'interfaces') - #LOGGER.info('oc_interfaces = {:s}'.format(pybindJSON.dumps(oc_interfaces, mode='ietf'))) - response = [] - return response - -openconfig-network-instance:network-instance \ No newline at end of file + def parse( + self, json_data : Dict, yang_handler : YangHandler + ) -> List[Tuple[str, Dict[str, Any]]]: + LOGGER.debug('json_data = {:s}'.format(json.dumps(json_data))) + + # Arista Parsing Fixes: + # - Default instance comes with mpls/signaling-protocols/rsvp-te/global/hellos/state/hello-interval set to 0 + # overwrite with .../hellos/config/hello-interval + network_instances = json_data.get('openconfig-network-instance:network-instance', []) + for network_instance in network_instances: + if network_instance['name'] != 'default': continue + mpls_rsvp_te = network_instance.get('mpls', {}).get('signaling-protocols', {}).get('rsvp-te', {}) + mpls_rsvp_te_hellos = mpls_rsvp_te.get('global', {}).get('hellos', {}) + hello_interval = mpls_rsvp_te_hellos.get('config', {}).get('hello-interval', 9000) + mpls_rsvp_te_hellos.get('state', {})['hello-interval'] = hello_interval + + yang_network_instances_path = self.get_path() + json_data_valid = yang_handler.parse_to_dict(yang_network_instances_path, json_data, fmt='json', strict=False) + + entries = [] + for network_instance in json_data_valid['network-instances']['network-instance']: + LOGGER.debug('network_instance={:s}'.format(str(network_instance))) + ni_name = network_instance['name'] + + ni_config = network_instance['config'] + ni_type = ni_config['type'].split(':')[-1] + + _net_inst = {'name': ni_name, 'type': ni_type} + entry_net_inst_key = '/network_instance[{:s}]'.format(ni_name) + entries.append((entry_net_inst_key, _net_inst)) + + ni_protocols = network_instance.get('protocols', {}).get('protocol', []) + for ni_protocol in ni_protocols: + ni_protocol_id = ni_protocol['identifier'].split(':')[-1] + ni_protocol_name = ni_protocol['name'] + + _protocol = {'id': ni_protocol_id, 'name': ni_protocol_name} + entry_protocol_key = '{:s}/protocol[{:s}]'.format(entry_net_inst_key, ni_protocol_id) + entries.append((entry_protocol_key, _protocol)) + + if ni_protocol_id == 'STATIC': + static_routes = ni_protocol.get('static-routes', {}).get('static', []) + for static_route in static_routes: + static_route_prefix = static_route['prefix'] + + next_hops = static_route.get('next-hops', {}).get('next-hop', []) + _next_hops = [ + { + 'index' : next_hop['index'], + 'gateway': next_hop['config']['next-hop'], + 'metric' : next_hop['config']['metric'], + } + for next_hop in next_hops + ] + _next_hops = sorted(_next_hops, key=operator.itemgetter('index')) + + _static_route = {'prefix': static_route_prefix, 'next_hops': _next_hops} + entry_static_route_key = '{:s}/static_routes[{:s}]'.format( + entry_protocol_key, static_route_prefix + ) + entries.append((entry_static_route_key, _static_route)) + + ni_tables = network_instance.get('tables', {}).get('table', []) + for ni_table in ni_tables: + ni_table_protocol = ni_table['protocol'].split(':')[-1] + ni_table_address_family = ni_table['address-family'].split(':')[-1] + _table = {'protocol': ni_table_protocol, 'address_family': ni_table_address_family} + entry_table_key = '{:s}/table[{:s},{:s}]'.format( + entry_net_inst_key, ni_table_protocol, ni_table_address_family + ) + entries.append((entry_table_key, _table)) + + ni_vlans = network_instance.get('vlans', {}).get('vlan', []) + for ni_vlan in ni_vlans: + ni_vlan_id = ni_vlan['vlan-id'] + + #ni_vlan_config = ni_vlan['config'] + ni_vlan_state = ni_vlan['state'] + ni_vlan_name = ni_vlan_state['name'] + + _members = [ + member['state']['interface'] + for member in ni_vlan.get('members', {}).get('member', []) + ] + _vlan = {'vlan_id': ni_vlan_id, 'name': ni_vlan_name, 'members': _members} + entry_vlan_key = '{:s}/vlan[{:d}]'.format(entry_net_inst_key, ni_vlan_id) + entries.append((entry_vlan_key, _vlan)) + + return entries diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/Tools.py b/src/device/service/drivers/gnmi_openconfig/handlers/Tools.py index 8cf704e2980e94e86e6d8445b8ad71a863434fe2..dfb8eabaf04493c798ca0874754d254e09af94ca 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/Tools.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/Tools.py @@ -13,7 +13,7 @@ # limitations under the License. import re -from typing import Any, Dict, Iterable, Optional +from typing import Any, Callable, Dict, Iterable, Optional RE_REMOVE_FILTERS = re.compile(r'\[[^\]]+\]') RE_REMOVE_NAMESPACES = re.compile(r'\/[a-zA-Z0-9\_\-]+:') @@ -40,3 +40,22 @@ def container_get_first( if namespace_key_name in container: return container[namespace_key_name] return default + +def get_value( + resource_value : Dict, field_name : str, cast_func : Callable = lambda x:x, default : Optional[Any] = None +) -> Optional[Any]: + field_value = resource_value.get(field_name, default) + if field_value is not None: field_value = cast_func(field_value) + return field_value + +def get_bool(resource_value : Dict, field_name : bool, default : Optional[Any] = None) -> bool: + return get_value(resource_value, field_name, cast_func=bool, default=default) + +def get_float(resource_value : Dict, field_name : float, default : Optional[Any] = None) -> float: + return get_value(resource_value, field_name, cast_func=float, default=default) + +def get_int(resource_value : Dict, field_name : int, default : Optional[Any] = None) -> int: + return get_value(resource_value, field_name, cast_func=int, default=default) + +def get_str(resource_value : Dict, field_name : str, default : Optional[Any] = None) -> str: + return get_value(resource_value, field_name, cast_func=str, default=default) diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/YangHandler.py b/src/device/service/drivers/gnmi_openconfig/handlers/YangHandler.py new file mode 100644 index 0000000000000000000000000000000000000000..fe8672187f4d2affdf0db1f76b76a4ba2600c6f7 --- /dev/null +++ b/src/device/service/drivers/gnmi_openconfig/handlers/YangHandler.py @@ -0,0 +1,109 @@ +# 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. + +import json, libyang, logging, os +from typing import Dict, Optional + +YANG_BASE_PATH = os.path.join(os.path.dirname(__file__), '..', 'git', 'openconfig', 'public') +YANG_SEARCH_PATHS = ':'.join([ + os.path.join(YANG_BASE_PATH, 'release'), + os.path.join(YANG_BASE_PATH, 'third_party'), +]) + +YANG_MODULES = [ + 'iana-if-type', + 'openconfig-vlan-types', + + 'openconfig-interfaces', + 'openconfig-if-8021x', + 'openconfig-if-aggregate', + 'openconfig-if-ethernet-ext', + 'openconfig-if-ethernet', + 'openconfig-if-ip-ext', + 'openconfig-if-ip', + 'openconfig-if-poe', + 'openconfig-if-sdn-ext', + 'openconfig-if-tunnel', + + 'openconfig-vlan', + + 'openconfig-types', + 'openconfig-policy-types', + 'openconfig-mpls-types', + 'openconfig-network-instance-types', + 'openconfig-network-instance', + + 'openconfig-platform', + 'openconfig-platform-controller-card', + 'openconfig-platform-cpu', + 'openconfig-platform-ext', + 'openconfig-platform-fabric', + 'openconfig-platform-fan', + 'openconfig-platform-integrated-circuit', + 'openconfig-platform-linecard', + 'openconfig-platform-pipeline-counters', + 'openconfig-platform-port', + 'openconfig-platform-psu', + 'openconfig-platform-software', + 'openconfig-platform-transceiver', + 'openconfig-platform-types', +] + +LOGGER = logging.getLogger(__name__) + +class YangHandler: + def __init__(self) -> None: + self._yang_context = libyang.Context(YANG_SEARCH_PATHS) + self._loaded_modules = set() + for yang_module_name in YANG_MODULES: + LOGGER.info('Loading module: {:s}'.format(str(yang_module_name))) + self._yang_context.load_module(yang_module_name).feature_enable_all() + self._loaded_modules.add(yang_module_name) + self._data_path_instances = dict() + + def get_data_paths(self) -> Dict[str, libyang.DNode]: + return self._data_path_instances + + def get_data_path(self, path : str) -> libyang.DNode: + data_path_instance = self._data_path_instances.get(path) + if data_path_instance is None: + data_path_instance = self._yang_context.create_data_path(path) + self._data_path_instances[path] = data_path_instance + return data_path_instance + + def parse_to_dict( + self, request_path : str, json_data : Dict, fmt : str = 'json', strict : bool = True + ) -> Dict: + if fmt != 'json': raise Exception('Unsupported format: {:s}'.format(str(fmt))) + LOGGER.debug('request_path = {:s}'.format(str(request_path))) + LOGGER.debug('json_data = {:s}'.format(str(json_data))) + LOGGER.debug('format = {:s}'.format(str(fmt))) + + parent_path_parts = list(filter(lambda s: len(s) > 0, request_path.split('/'))) + for parent_path_part in reversed(parent_path_parts): + json_data = {parent_path_part: json_data} + str_data = json.dumps(json_data) + + dnode : Optional[libyang.DNode] = self._yang_context.parse_data_mem( + str_data, fmt, strict=strict, parse_only=True, #validate_present=True, #validate=True, + ) + if dnode is None: raise Exception('Unable to parse Data({:s})'.format(str(json_data))) + + parsed = dnode.print_dict() + LOGGER.debug('parsed = {:s}'.format(json.dumps(parsed))) + dnode.free() + return parsed + + def destroy(self) -> None: + self._yang_context.destroy() diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/_Handler.py b/src/device/service/drivers/gnmi_openconfig/handlers/_Handler.py index d20c77b1165decce7ea07243beb782a6b749734b..a03692d9551125f3da1fc8965c912cd2eceb229f 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/_Handler.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/_Handler.py @@ -13,6 +13,7 @@ # limitations under the License. from typing import Any, Dict, List, Tuple +from .YangHandler import YangHandler class _Handler: def get_resource_key(self) -> str: @@ -23,10 +24,14 @@ class _Handler: # Retrieve the OpenConfig path schema used to interrogate the device raise NotImplementedError() - def compose(self, resource_key : str, resource_value : Dict, delete : bool = False) -> Tuple[str, str]: + def compose( + self, resource_key : str, resource_value : Dict, yang_handler : YangHandler, delete : bool = False + ) -> Tuple[str, str]: # Compose a Set/Delete message based on the resource_key/resource_value fields, and the delete flag raise NotImplementedError() - def parse(self, json_data : Dict) -> List[Tuple[str, Dict[str, Any]]]: + def parse( + self, json_data : Dict, yang_handler : YangHandler + ) -> List[Tuple[str, Dict[str, Any]]]: # Parse a Reply from the device and return a list of resource_key/resource_value pairs raise NotImplementedError() diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/__init__.py b/src/device/service/drivers/gnmi_openconfig/handlers/__init__.py index 6d54ef28d4bf1af1697e0fccd2bb1b6253366422..38bc4db404919bfd22ed7974524bac8d97169bc9 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/__init__.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/__init__.py @@ -13,7 +13,7 @@ # limitations under the License. import logging -from typing import Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple, Union from device.service.driver_api._Driver import RESOURCE_ENDPOINTS, RESOURCE_INTERFACES, RESOURCE_NETWORK_INSTANCES from ._Handler import _Handler from .Component import ComponentHandler @@ -23,6 +23,7 @@ from .NetworkInstance import NetworkInstanceHandler from .NetworkInstanceInterface import NetworkInstanceInterfaceHandler from .NetworkInstanceStaticRoute import NetworkInstanceStaticRouteHandler from .Tools import get_schema +from .YangHandler import YangHandler LOGGER = logging.getLogger(__name__) @@ -71,7 +72,8 @@ PATH_TO_HANDLER = { } def get_handler( - resource_key : Optional[str] = None, path : Optional[str] = None, raise_if_not_found=True + resource_key : Optional[str] = None, path : Optional[str] = None, + raise_if_not_found=True ) -> Optional[_Handler]: if (resource_key is None) == (path is None): MSG = 'Exactly one of resource_key({:s}) or path({:s}) must be specified' @@ -95,10 +97,18 @@ def get_handler( return handler def get_path(resource_key : str) -> str: - return get_handler(resource_key=resource_key).get_path() + handler = get_handler(resource_key=resource_key) + return handler.get_path() -def parse(str_path : str, value : Union[Dict, List]): - return get_handler(path=str_path).parse(value) +def parse( + str_path : str, value : Union[Dict, List], yang_handler : YangHandler +) -> List[Tuple[str, Dict[str, Any]]]: + handler = get_handler(path=str_path) + return handler.parse(value, yang_handler) -def compose(resource_key : str, resource_value : Union[Dict, List], delete : bool = False) -> Tuple[str, str]: - return get_handler(resource_key=resource_key).compose(resource_key, resource_value, delete=delete) +def compose( + resource_key : str, resource_value : Union[Dict, List], + yang_handler : YangHandler, delete : bool = False +) -> Tuple[str, str]: + handler = get_handler(resource_key=resource_key) + return handler.compose(resource_key, resource_value, yang_handler, delete=delete) diff --git a/src/device/service/drivers/gnmi_openconfig/tools/Capabilities.py b/src/device/service/drivers/gnmi_openconfig/tools/Capabilities.py index b90bf3db887874d3c9015336cc105b3429c8e64e..4c202da2c220c8bf6dfa32d5ba0b84878bba0ab6 100644 --- a/src/device/service/drivers/gnmi_openconfig/tools/Capabilities.py +++ b/src/device/service/drivers/gnmi_openconfig/tools/Capabilities.py @@ -17,7 +17,7 @@ from common.tools.grpc.Tools import grpc_message_to_json from ..gnmi.gnmi_pb2 import CapabilityRequest # pylint: disable=no-name-in-module from ..gnmi.gnmi_pb2_grpc import gNMIStub -def get_supported_encodings( +def check_capabilities( stub : gNMIStub, username : str, password : str, timeout : Optional[int] = None ) -> Set[Union[str, int]]: metadata = [('username', username), ('password', password)] @@ -25,6 +25,17 @@ def get_supported_encodings( reply = stub.Capabilities(req, metadata=metadata, timeout=timeout) data = grpc_message_to_json(reply) + + gnmi_version = data.get('gNMI_version') + if gnmi_version is None or gnmi_version != '0.7.0': + raise Exception('Unsupported gNMI version: {:s}'.format(str(gnmi_version))) + + #supported_models = { + # supported_model['name']: supported_model['version'] + # for supported_model in data.get('supported_models', []) + #} + # TODO: check supported models and versions + supported_encodings = { supported_encoding for supported_encoding in data.get('supported_encodings', []) @@ -33,4 +44,6 @@ def get_supported_encodings( if len(supported_encodings) == 0: # pylint: disable=broad-exception-raised raise Exception('No supported encodings found') - return supported_encodings + if 'JSON_IETF' not in supported_encodings: + # pylint: disable=broad-exception-raised + raise Exception('JSON_IETF encoding not supported') diff --git a/src/device/service/drivers/gnmi_openconfig/tools/Value.py b/src/device/service/drivers/gnmi_openconfig/tools/Value.py index 9933cb8584e657608dd9e2010021597a6083605a..73e43b87c4962431db94a674c65f8505f9273c13 100644 --- a/src/device/service/drivers/gnmi_openconfig/tools/Value.py +++ b/src/device/service/drivers/gnmi_openconfig/tools/Value.py @@ -61,7 +61,7 @@ def decode_value(value : TypedValue) -> Any: str_value : str = value.json_ietf_val.decode('UTF-8') try: # Cleanup and normalize the records according to OpenConfig - str_value = str_value.replace('openconfig-platform-types:', 'oc-platform-types:') + #str_value = str_value.replace('openconfig-platform-types:', 'oc-platform-types:') json_value = json.loads(str_value) recursive_remove_keys(json_value) return json_value diff --git a/src/device/tests/test_gnmi.py b/src/device/tests/test_gnmi.py deleted file mode 100644 index 684b9f4c3b752dc099f276d2d60f9549e5cede19..0000000000000000000000000000000000000000 --- a/src/device/tests/test_gnmi.py +++ /dev/null @@ -1,115 +0,0 @@ -# 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. - -import logging, os, sys, time -from typing import Dict, Tuple -os.environ['DEVICE_EMULATED_ONLY'] = 'YES' -from device.service.drivers.gnmi_openconfig.GnmiOpenConfigDriver import GnmiOpenConfigDriver # pylint: disable=wrong-import-position -from device.service.driver_api._Driver import ( - RESOURCE_ENDPOINTS, RESOURCE_INTERFACES, RESOURCE_NETWORK_INSTANCES, RESOURCE_ROUTING_POLICIES, RESOURCE_SERVICES -) - -logging.basicConfig(level=logging.DEBUG) -LOGGER = logging.getLogger(__name__) -LOGGER.setLevel(logging.DEBUG) - -# +---+---------------------------+--------------+---------------------------------+-------+---------+--------------------+--------------+ -# | # | Name | Container ID | Image | Kind | State | IPv4 Address | IPv6 Address | -# +---+---------------------------+--------------+---------------------------------+-------+---------+--------------------+--------------+ -# | 1 | clab-tfs-scenario-client1 | a8d48ec3265a | ghcr.io/hellt/network-multitool | linux | running | 172.100.100.201/24 | N/A | -# | 2 | clab-tfs-scenario-client2 | fc88436d2b32 | ghcr.io/hellt/network-multitool | linux | running | 172.100.100.202/24 | N/A | -# | 3 | clab-tfs-scenario-srl1 | b995b9bdadda | ghcr.io/nokia/srlinux | srl | running | 172.100.100.101/24 | N/A | -# | 4 | clab-tfs-scenario-srl2 | aacfc38cc376 | ghcr.io/nokia/srlinux | srl | running | 172.100.100.102/24 | N/A | -# +---+---------------------------+--------------+---------------------------------+-------+---------+--------------------+--------------+ - -def interface(if_name, sif_index, ipv4_address, ipv4_prefix, enabled) -> Tuple[str, Dict]: - str_path = '/interface[{:s}]'.format(if_name) - str_data = {'name': if_name, 'enabled': enabled, 'sub_if_index': sif_index, 'sub_if_enabled': enabled, - 'sub_if_ipv4_enabled': enabled, 'sub_if_ipv4_address': ipv4_address, 'sub_if_ipv4_prefix': ipv4_prefix} - return str_path, str_data - -def network_instance(ni_name, ni_type) -> Tuple[str, Dict]: - str_path = '/network_instance[{:s}]'.format(ni_name) - str_data = {'name': ni_name, 'type': ni_type} - return str_path, str_data - -def network_instance_static_route(ni_name, prefix, next_hop, next_hop_index=0) -> Tuple[str, Dict]: - str_path = '/network_instance[{:s}]/static_route[{:s}]'.format(ni_name, prefix) - str_data = {'name': ni_name, 'prefix': prefix, 'next_hop': next_hop, 'next_hop_index': next_hop_index} - return str_path, str_data - -def network_instance_interface(ni_name, if_name, sif_index) -> Tuple[str, Dict]: - str_path = '/network_instance[{:s}]/interface[{:s}.{:d}]'.format(ni_name, if_name, sif_index) - str_data = {'name': ni_name, 'if_name': if_name, 'sif_index': sif_index} - return str_path, str_data - -def main(): - driver_settings = { - 'protocol': 'gnmi', - 'username': 'admin', - 'password': 'admin', - 'use_tls' : False, - } - driver = GnmiOpenConfigDriver('172.20.20.101', 6030, **driver_settings) - driver.Connect() - - #resources_to_get = [] - #resources_to_get = [RESOURCE_ENDPOINTS] - #resources_to_get = [RESOURCE_INTERFACES] - resources_to_get = [RESOURCE_NETWORK_INSTANCES] - #resources_to_get = [RESOURCE_ROUTING_POLICIES] - #resources_to_get = [RESOURCE_SERVICES] - LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) - results_getconfig = driver.GetConfig(resources_to_get) - LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) - - #resources_to_set = [ - # network_instance('test-svc', 'L3VRF'), - # - # interface('ethernet-1/1', 0, '172.16.0.1', 24, True), - # network_instance_interface('test-svc', 'ethernet-1/1', 0), - # - # interface('ethernet-1/2', 0, '172.0.0.1', 24, True), - # network_instance_interface('test-svc', 'ethernet-1/2', 0), - # - # network_instance_static_route('test-svc', '172.0.0.0/24', '172.16.0.2'), - # network_instance_static_route('test-svc', '172.2.0.0/24', '172.16.0.3'), - #] - #LOGGER.info('resources_to_set = {:s}'.format(str(resources_to_set))) - #results_setconfig = driver.SetConfig(resources_to_set) - #LOGGER.info('results_setconfig = {:s}'.format(str(results_setconfig))) - - #resources_to_delete = [ - # #network_instance_static_route('d35fc1d9', '172.0.0.0/24', '172.16.0.2'), - # #network_instance_static_route('d35fc1d9', '172.2.0.0/24', '172.16.0.3'), - # - # #network_instance_interface('d35fc1d9', 'ethernet-1/1', 0), - # #network_instance_interface('d35fc1d9', 'ethernet-1/2', 0), - # - # #interface('ethernet-1/1', 0, '172.16.1.1', 24, True), - # #interface('ethernet-1/2', 0, '172.0.0.2', 24, True), - # - # #network_instance('20f66fb5', 'L3VRF'), - #] - #LOGGER.info('resources_to_delete = {:s}'.format(str(resources_to_delete))) - #results_deleteconfig = driver.DeleteConfig(resources_to_delete) - #LOGGER.info('results_deleteconfig = {:s}'.format(str(results_deleteconfig))) - - time.sleep(1) - - driver.Disconnect() - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/device/tests/test_unitary_gnmi_openconfig.py b/src/device/tests/test_unitary_gnmi_openconfig.py new file mode 100644 index 0000000000000000000000000000000000000000..4c2dca5d56aa34d7d51a8d92bf98417e856d3774 --- /dev/null +++ b/src/device/tests/test_unitary_gnmi_openconfig.py @@ -0,0 +1,616 @@ +# 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. + +import deepdiff, logging, os, pytest, re, time +from typing import Dict, List, Tuple +os.environ['DEVICE_EMULATED_ONLY'] = 'YES' + +# pylint: disable=wrong-import-position +from device.service.driver_api._Driver import ( + RESOURCE_ENDPOINTS, RESOURCE_INTERFACES, RESOURCE_NETWORK_INSTANCES, RESOURCE_ROUTING_POLICIES, RESOURCE_SERVICES +) +from device.service.drivers.gnmi_openconfig.GnmiOpenConfigDriver import GnmiOpenConfigDriver + +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +DRIVER_SETTING_ADDRESS = '172.20.20.101' +DRIVER_SETTING_PORT = 6030 +DRIVER_SETTING_USERNAME = 'admin' +DRIVER_SETTING_PASSWORD = 'admin' +DRIVER_SETTING_USE_TLS = False + +@pytest.fixture(scope='session') +def driver() -> GnmiOpenConfigDriver: + _driver = GnmiOpenConfigDriver( + DRIVER_SETTING_ADDRESS, DRIVER_SETTING_PORT, + username=DRIVER_SETTING_USERNAME, + password=DRIVER_SETTING_PASSWORD, + use_tls=DRIVER_SETTING_USE_TLS, + ) + _driver.Connect() + yield _driver + time.sleep(1) + _driver.Disconnect() + +@pytest.fixture(scope='session') +def storage() -> Dict: + yield dict() + + +##### STORAGE POPULATORS ############################################################################################### + +def populate_interfaces_storage( + storage : Dict, # pylint: disable=redefined-outer-name + resources : List[Tuple[str, Dict]], +) -> None: + interfaces_storage : Dict = storage.setdefault('interfaces', dict()) + subinterfaces_storage : Dict = storage.setdefault('interface_subinterfaces', dict()) + ipv4_addresses_storage : Dict = storage.setdefault('interface_subinterface_ipv4_addresses', dict()) + + for resource_key, resource_value in resources: + match = re.match(r'^\/interface\[([^\]]+)\]$', resource_key) + if match is not None: + if_name = match.group(1) + if_storage = interfaces_storage.setdefault(if_name, dict()) + if_storage['name' ] = if_name + if_storage['type' ] = resource_value.get('type' ) + if_storage['admin-status' ] = resource_value.get('admin-status' ) + if_storage['oper-status' ] = resource_value.get('oper-status' ) + if_storage['ifindex' ] = resource_value.get('ifindex' ) + if_storage['mtu' ] = resource_value.get('mtu' ) + if_storage['management' ] = resource_value.get('management' ) + if_storage['hardware-port'] = resource_value.get('hardware-port') + if_storage['transceiver' ] = resource_value.get('transceiver' ) + continue + + match = re.match(r'^\/interface\[([^\]]+)\]\/ethernet$', resource_key) + if match is not None: + if_name = match.group(1) + if_storage = interfaces_storage.setdefault(if_name, dict()) + if_storage['port-speed' ] = resource_value.get('port-speed' ) + if_storage['negotiated-port-speed'] = resource_value.get('negotiated-port-speed') + if_storage['mac-address' ] = resource_value.get('mac-address' ) + if_storage['hw-mac-address' ] = resource_value.get('hw-mac-address' ) + continue + + match = re.match(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$', resource_key) + if match is not None: + if_name = match.group(1) + subif_index = int(match.group(2)) + subif_storage = subinterfaces_storage.setdefault((if_name, subif_index), dict()) + subif_storage['index'] = subif_index + continue + + match = re.match(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]\/ipv4\[([^\]]+)\]$', resource_key) + if match is not None: + if_name = match.group(1) + subif_index = int(match.group(2)) + ipv4_addr = match.group(3) + ipv4_address_storage = ipv4_addresses_storage.setdefault((if_name, subif_index, ipv4_addr), dict()) + ipv4_address_storage['ip' ] = ipv4_addr + ipv4_address_storage['origin'] = resource_value.get('origin') + ipv4_address_storage['prefix'] = resource_value.get('prefix') + continue + +def populate_network_instances_storage( + storage : Dict, # pylint: disable=redefined-outer-name + resources : List[Tuple[str, Dict]], +) -> None: + network_instances_storage : Dict = storage.setdefault('network_instances', dict()) + network_instance_protocols_storage : Dict = storage.setdefault('network_instance_protocols', dict()) + network_instance_protocol_static_storage : Dict = storage.setdefault('network_instance_protocol_static', dict()) + network_instance_tables_storage : Dict = storage.setdefault('network_instance_tables', dict()) + network_instance_vlans_storage : Dict = storage.setdefault('network_instance_vlans', dict()) + + for resource_key, resource_value in resources: + match = re.match(r'^\/network\_instance\[([^\]]+)\]$', resource_key) + if match is not None: + name = match.group(1) + ni_storage = network_instances_storage.setdefault(name, dict()) + ni_storage['name'] = name + ni_storage['type'] = resource_value.get('type') + continue + + match = re.match(r'^\/network\_instance\[([^\]]+)\]\/protocol\[([^\]]+)\]$', resource_key) + if match is not None: + name = match.group(1) + protocol = match.group(2) + ni_p_storage = network_instance_protocols_storage.setdefault((name, protocol), dict()) + ni_p_storage['id' ] = protocol + ni_p_storage['name'] = protocol + continue + + pattern = r'^\/network\_instance\[([^\]]+)\]\/protocol\[([^\]]+)\]\/static\_routes\[([^\]]+)\]$' + match = re.match(pattern, resource_key) + if match is not None: + name = match.group(1) + protocol = match.group(2) + prefix = match.group(3) + ni_p_s_storage = network_instance_protocol_static_storage.setdefault((name, protocol, prefix), dict()) + ni_p_s_storage['prefix' ] = prefix + ni_p_s_storage['next_hops'] = resource_value.get('next_hops') + continue + + match = re.match(r'^\/network\_instance\[([^\]]+)\]\/table\[([^\,]+)\,([^\]]+)\]$', resource_key) + if match is not None: + name = match.group(1) + protocol = match.group(2) + address_family = match.group(3) + ni_t_storage = network_instance_tables_storage.setdefault((name, protocol, address_family), dict()) + ni_t_storage['protocol' ] = protocol + ni_t_storage['address_family'] = address_family + continue + + match = re.match(r'^\/network\_instance\[([^\]]+)\]\/vlan\[([^\]]+)\]$', resource_key) + if match is not None: + name = match.group(1) + vlan_id = int(match.group(2)) + ni_v_storage = network_instance_vlans_storage.setdefault((name, vlan_id), dict()) + ni_v_storage['vlan_id'] = vlan_id + ni_v_storage['name' ] = resource_value.get('name') + ni_v_storage['members'] = resource_value.get('members') + continue + + +##### EXPECTED CONFIG COMPOSERS ######################################################################################## + +INTERFACE_CONFIG_STRUCTURE : List[Tuple[str, List[str]]] = [ + ('/interface[{if_name:s}]', [ + 'name', 'type', 'admin-status', 'oper-status', 'management', 'mtu', 'ifindex', 'hardware-port', 'transceiver' + ]), + ('/interface[{if_name:s}]/ethernet', [ + 'port-speed', 'negotiated-port-speed', 'mac-address', 'hw-mac-address' + ]), +] + +INTERFACE_SUBINTERFACE_CONFIG_STRUCTURE : List[Tuple[str, List[str]]] = [ + ('/interface[{if_name:s}]/subinterface[{subif_index:d}]', ['index']), +] + +INTERFACE_SUBINTERFACE_IPV4_ADDRESS_CONFIG_STRUCTURE : List[Tuple[str, List[str]]] = [ + ('/interface[{if_name:s}]/subinterface[{subif_index:d}]/ipv4[{ipv4_addr:s}]', ['ip', 'origin', 'prefix']), +] + +def get_expected_interface_config( + storage : Dict, # pylint: disable=redefined-outer-name +) -> List[Tuple[str, Dict]]: + interfaces_storage : Dict = storage.setdefault('interfaces', dict()) + subinterfaces_storage : Dict = storage.setdefault('interface_subinterfaces', dict()) + ipv4_addresses_storage : Dict = storage.setdefault('interface_subinterface_ipv4_addresses', dict()) + + expected_interface_config = list() + for if_name, if_storage in interfaces_storage.items(): + for resource_key_template, resource_key_field_names in INTERFACE_CONFIG_STRUCTURE: + resource_key = resource_key_template.format(if_name=if_name) + resource_value = { + field_name : if_storage[field_name] + for field_name in resource_key_field_names + if field_name in if_storage and if_storage[field_name] is not None + } + expected_interface_config.append((resource_key, resource_value)) + + for (if_name, subif_index), subif_storage in subinterfaces_storage.items(): + for resource_key_template, resource_key_field_names in INTERFACE_SUBINTERFACE_CONFIG_STRUCTURE: + resource_key = resource_key_template.format(if_name=if_name, subif_index=subif_index) + resource_value = { + field_name : subif_storage[field_name] + for field_name in resource_key_field_names + if field_name in subif_storage and subif_storage[field_name] is not None + } + expected_interface_config.append((resource_key, resource_value)) + + for (if_name, subif_index, ipv4_addr), ipv4_storage in ipv4_addresses_storage.items(): + for resource_key_template, resource_key_field_names in INTERFACE_SUBINTERFACE_IPV4_ADDRESS_CONFIG_STRUCTURE: + resource_key = resource_key_template.format(if_name=if_name, subif_index=subif_index, ipv4_addr=ipv4_addr) + resource_value = { + field_name : ipv4_storage[field_name] + for field_name in resource_key_field_names + if field_name in ipv4_storage and ipv4_storage[field_name] is not None + } + expected_interface_config.append((resource_key, resource_value)) + + return expected_interface_config + +NETWORK_INSTANCE_CONFIG_STRUCTURE : List[Tuple[str, List[str]]] = [ + ('/network_instance[{ni_name:s}]', ['name', 'type']), +] + +NETWORK_INSTANCE_PROTOCOL_CONFIG_STRUCTURE : List[Tuple[str, List[str]]] = [ + ('/network_instance[{ni_name:s}]/protocol[{protocol:s}]', ['id', 'name']), +] + +NETWORK_INSTANCE_PROTOCOL_STATIC_CONFIG_STRUCTURE : List[Tuple[str, List[str]]] = [ + ('/network_instance[{ni_name:s}]/protocol[{protocol:s}]/static_routes[{prefix:s}]', ['prefix', 'next_hops']), +] + +NETWORK_INSTANCE_TABLE_CONFIG_STRUCTURE : List[Tuple[str, List[str]]] = [ + ('/network_instance[{ni_name:s}]/table[{protocol:s},{address_family:s}]', ['protocol', 'address_family']), +] + +NETWORK_INSTANCE_VLAN_CONFIG_STRUCTURE : List[Tuple[str, List[str]]] = [ + ('/network_instance[{ni_name:s}]/vlan[{vlan_id:d}]', ['vlan_id', 'name', 'members']), +] + +def get_expected_network_instance_config( + storage : Dict, # pylint: disable=redefined-outer-name +) -> List[Tuple[str, Dict]]: + network_instances_storage : Dict = storage.setdefault('network_instances', dict()) + network_instance_protocols_storage : Dict = storage.setdefault('network_instance_protocols', dict()) + network_instance_protocol_static_storage : Dict = storage.setdefault('network_instance_protocol_static', dict()) + network_instance_tables_storage : Dict = storage.setdefault('network_instance_tables', dict()) + network_instance_vlans_storage : Dict = storage.setdefault('network_instance_vlans', dict()) + + expected_network_instance_config = list() + for ni_name, ni_storage in network_instances_storage.items(): + for resource_key_template, resource_key_field_names in NETWORK_INSTANCE_CONFIG_STRUCTURE: + resource_key = resource_key_template.format(ni_name=ni_name) + resource_value = { + field_name : ni_storage[field_name] + for field_name in resource_key_field_names + if field_name in ni_storage and ni_storage[field_name] is not None + } + expected_network_instance_config.append((resource_key, resource_value)) + + for (ni_name, protocol), ni_p_storage in network_instance_protocols_storage.items(): + for resource_key_template, resource_key_field_names in NETWORK_INSTANCE_PROTOCOL_CONFIG_STRUCTURE: + resource_key = resource_key_template.format(ni_name=ni_name, protocol=protocol) + resource_value = { + field_name : ni_p_storage[field_name] + for field_name in resource_key_field_names + if field_name in ni_p_storage and ni_p_storage[field_name] is not None + } + expected_network_instance_config.append((resource_key, resource_value)) + + for (ni_name, protocol, prefix), ni_p_s_storage in network_instance_protocol_static_storage.items(): + for resource_key_template, resource_key_field_names in NETWORK_INSTANCE_PROTOCOL_STATIC_CONFIG_STRUCTURE: + resource_key = resource_key_template.format(ni_name=ni_name, protocol=protocol, prefix=prefix) + resource_value = { + field_name : ni_p_s_storage[field_name] + for field_name in resource_key_field_names + if field_name in ni_p_s_storage and ni_p_s_storage[field_name] is not None + } + expected_network_instance_config.append((resource_key, resource_value)) + + for (ni_name, protocol, address_family), ni_t_storage in network_instance_tables_storage.items(): + for resource_key_template, resource_key_field_names in NETWORK_INSTANCE_TABLE_CONFIG_STRUCTURE: + resource_key = resource_key_template.format( + ni_name=ni_name, protocol=protocol, address_family=address_family + ) + resource_value = { + field_name : ni_t_storage[field_name] + for field_name in resource_key_field_names + if field_name in ni_t_storage and ni_t_storage[field_name] is not None + } + expected_network_instance_config.append((resource_key, resource_value)) + + for (ni_name, vlan_id), ni_v_storage in network_instance_vlans_storage.items(): + for resource_key_template, resource_key_field_names in NETWORK_INSTANCE_VLAN_CONFIG_STRUCTURE: + resource_key = resource_key_template.format(ni_name=ni_name, vlan_id=vlan_id) + resource_value = { + field_name : ni_v_storage[field_name] + for field_name in resource_key_field_names + if field_name in ni_v_storage and ni_v_storage[field_name] is not None + } + expected_network_instance_config.append((resource_key, resource_value)) + + return expected_network_instance_config + + +##### REQUEST COMPOSERS ################################################################################################ + +def interface(if_name, sif_index, ipv4_address, ipv4_prefix, enabled) -> Tuple[str, Dict]: + str_path = '/interface[{:s}]'.format(if_name) + str_data = { + 'name': if_name, 'enabled': enabled, 'sub_if_index': sif_index, 'sub_if_enabled': enabled, + 'sub_if_ipv4_enabled': enabled, 'sub_if_ipv4_address': ipv4_address, 'sub_if_ipv4_prefix': ipv4_prefix + } + return str_path, str_data + +def network_instance(ni_name, ni_type) -> Tuple[str, Dict]: + str_path = '/network_instance[{:s}]'.format(ni_name) + str_data = {'name': ni_name, 'type': ni_type} + return str_path, str_data + +def network_instance_static_route(ni_name, prefix, next_hop, next_hop_index=0) -> Tuple[str, Dict]: + str_path = '/network_instance[{:s}]/static_route[{:s}]'.format(ni_name, prefix) + str_data = {'name': ni_name, 'prefix': prefix, 'next_hop': next_hop, 'next_hop_index': next_hop_index} + return str_path, str_data + +def network_instance_interface(ni_name, if_name, sif_index) -> Tuple[str, Dict]: + str_path = '/network_instance[{:s}]/interface[{:s}.{:d}]'.format(ni_name, if_name, sif_index) + str_data = {'name': ni_name, 'if_name': if_name, 'sif_index': sif_index} + return str_path, str_data + +def test_get_endpoints( + driver : GnmiOpenConfigDriver, # pylint: disable=redefined-outer-name +) -> None: + resources_to_get = [RESOURCE_ENDPOINTS] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + expected_getconfig = [ + ('/endpoints/endpoint[ethernet1]', {'uuid': 'ethernet1', 'type': '-', 'sample_types': { + 202: '/openconfig-interfaces:interfaces/interface[name=ethernet1]/state/counters/in-octets', + 201: '/openconfig-interfaces:interfaces/interface[name=ethernet1]/state/counters/out-octets', + 102: '/openconfig-interfaces:interfaces/interface[name=ethernet1]/state/counters/in-pkts', + 101: '/openconfig-interfaces:interfaces/interface[name=ethernet1]/state/counters/out-pkts' + }}), + ('/endpoints/endpoint[ethernet10]', {'uuid': 'ethernet10', 'type': '-', 'sample_types': { + 202: '/openconfig-interfaces:interfaces/interface[name=ethernet10]/state/counters/in-octets', + 201: '/openconfig-interfaces:interfaces/interface[name=ethernet10]/state/counters/out-octets', + 102: '/openconfig-interfaces:interfaces/interface[name=ethernet10]/state/counters/in-pkts', + 101: '/openconfig-interfaces:interfaces/interface[name=ethernet10]/state/counters/out-pkts' + }}) + ] + diff_data = deepdiff.DeepDiff(sorted(expected_getconfig), sorted(results_getconfig)) + num_diffs = len(diff_data) + if num_diffs > 0: LOGGER.error('Differences[{:d}]:\n{:s}'.format(num_diffs, str(diff_data.pretty()))) + assert num_diffs == 0 + +def test_get_interfaces( + driver : GnmiOpenConfigDriver, # pylint: disable=redefined-outer-name + storage : Dict, # pylint: disable=redefined-outer-name +) -> None: + resources_to_get = [RESOURCE_INTERFACES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + populate_interfaces_storage(storage, results_getconfig) + expected_getconfig = get_expected_interface_config(storage) + + diff_data = deepdiff.DeepDiff(sorted(expected_getconfig), sorted(results_getconfig)) + num_diffs = len(diff_data) + if num_diffs > 0: LOGGER.error('Differences[{:d}]:\n{:s}'.format(num_diffs, str(diff_data.pretty()))) + assert num_diffs == 0 + +def test_get_network_instances( + driver : GnmiOpenConfigDriver, # pylint: disable=redefined-outer-name + storage : Dict, # pylint: disable=redefined-outer-name +) -> None: + resources_to_get = [RESOURCE_NETWORK_INSTANCES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + populate_network_instances_storage(storage, results_getconfig) + expected_getconfig = get_expected_network_instance_config(storage) + + diff_data = deepdiff.DeepDiff(sorted(expected_getconfig), sorted(results_getconfig)) + num_diffs = len(diff_data) + if num_diffs > 0: LOGGER.error('Differences[{:d}]:\n{:s}'.format(num_diffs, str(diff_data.pretty()))) + assert num_diffs == 0 + +def test_set_interfaces( + driver : GnmiOpenConfigDriver, # pylint: disable=redefined-outer-name + storage : Dict, # pylint: disable=redefined-outer-name +) -> None: + resources_to_get = [RESOURCE_INTERFACES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + resources_to_set = [ + interface('Ethernet1', 0, '192.168.1.1', 24, True), + interface('Ethernet10', 0, '192.168.10.1', 24, True), + ] + LOGGER.info('resources_to_set = {:s}'.format(str(resources_to_set))) + results_setconfig = driver.SetConfig(resources_to_set) + LOGGER.info('results_setconfig = {:s}'.format(str(results_setconfig))) + + interfaces = sorted(['Ethernet1', 'Ethernet10']) + results = set(results_setconfig) + assert len(results) == len(interfaces) + for if_name in interfaces: + assert ('/interface[{:s}]'.format(if_name), True) in results + + expected_getconfig = get_expected_interface_config(storage) + expected_getconfig.extend([ + ('/interface[Ethernet1]/subinterface[0]/ipv4[192.168.1.1]', { + 'ip': '192.168.1.1', 'origin': 'STATIC', 'prefix': 24 + }), + ('/interface[Ethernet10]/subinterface[0]/ipv4[192.168.10.1]', { + 'ip': '192.168.10.1', 'origin': 'STATIC', 'prefix': 24 + }) + ]) + + permitted_retries = 5 + while permitted_retries > 0: + resources_to_get = [RESOURCE_INTERFACES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + diff_data = deepdiff.DeepDiff(sorted(expected_getconfig), sorted(results_getconfig)) + num_diffs = len(diff_data) + if num_diffs == 0: break + # let the device take some time to reconfigure + time.sleep(0.5) + permitted_retries -= 1 + + if num_diffs > 0: LOGGER.error('Differences[{:d}]:\n{:s}'.format(num_diffs, str(diff_data.pretty()))) + assert num_diffs == 0 + +def test_set_network_instances( + driver : GnmiOpenConfigDriver, # pylint: disable=redefined-outer-name + storage : Dict, # pylint: disable=redefined-outer-name +) -> None: + resources_to_get = [RESOURCE_NETWORK_INSTANCES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + resources_to_set = [ + network_instance('test-l3-svc', 'L3VRF'), + ] + LOGGER.info('resources_to_set = {:s}'.format(str(resources_to_set))) + results_setconfig = driver.SetConfig(resources_to_set) + LOGGER.info('results_setconfig = {:s}'.format(str(results_setconfig))) + + network_instances = sorted(['test-l3-svc']) + results = set(results_setconfig) + assert len(results) == len(network_instances) + for ni_name in network_instances: + assert ('/network_instance[{:s}]'.format(ni_name), True) in results + + expected_getconfig = get_expected_network_instance_config(storage) + expected_getconfig.extend([ + ('/network_instance[test-l3-svc]', { + 'name': 'test-l3-svc', 'type': 'L3VRF' + }), + ('/network_instance[test-l3-svc]/protocol[DIRECTLY_CONNECTED]', { + 'id': 'DIRECTLY_CONNECTED', 'name': 'DIRECTLY_CONNECTED' + }), + ('/network_instance[test-l3-svc]/table[DIRECTLY_CONNECTED,IPV4]', { + 'protocol': 'DIRECTLY_CONNECTED', 'address_family': 'IPV4' + }), + ('/network_instance[test-l3-svc]/table[DIRECTLY_CONNECTED,IPV6]', { + 'protocol': 'DIRECTLY_CONNECTED', 'address_family': 'IPV6' + }) + ]) + LOGGER.info('expected_getconfig = {:s}'.format(str(sorted(expected_getconfig)))) + + permitted_retries = 5 + while permitted_retries > 0: + resources_to_get = [RESOURCE_NETWORK_INSTANCES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + diff_data = deepdiff.DeepDiff(sorted(expected_getconfig), sorted(results_getconfig)) + num_diffs = len(diff_data) + if num_diffs == 0: break + # let the device take some time to reconfigure + time.sleep(0.5) + permitted_retries -= 1 + + if num_diffs > 0: LOGGER.error('Differences[{:d}]:\n{:s}'.format(num_diffs, str(diff_data.pretty()))) + assert num_diffs == 0 + +def test_del_interfaces( + driver : GnmiOpenConfigDriver, # pylint: disable=redefined-outer-name + storage : Dict, # pylint: disable=redefined-outer-name +) -> None: + resources_to_get = [RESOURCE_INTERFACES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + resources_to_delete = [ + interface('Ethernet1', 0, '192.168.1.1', 24, True), + interface('Ethernet10', 0, '192.168.10.1', 24, True), + ] + LOGGER.info('resources_to_delete = {:s}'.format(str(resources_to_delete))) + results_deleteconfig = driver.DeleteConfig(resources_to_delete) + LOGGER.info('results_deleteconfig = {:s}'.format(str(results_deleteconfig))) + + interfaces = sorted(['Ethernet1', 'Ethernet10']) + results = set(results_deleteconfig) + assert len(results) == len(interfaces) + for if_name in interfaces: + assert ('/interface[{:s}]'.format(if_name), True) in results + + resources_to_get = [RESOURCE_INTERFACES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + expected_getconfig = get_expected_interface_config(storage) + + diff_data = deepdiff.DeepDiff(sorted(expected_getconfig), sorted(results_getconfig)) + num_diffs = len(diff_data) + if num_diffs > 0: LOGGER.error('Differences[{:d}]:\n{:s}'.format(num_diffs, str(diff_data.pretty()))) + assert num_diffs == 0 + +def test_del_network_instances( + driver : GnmiOpenConfigDriver, # pylint: disable=redefined-outer-name + storage : Dict, # pylint: disable=redefined-outer-name +) -> None: + resources_to_get = [RESOURCE_NETWORK_INSTANCES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + resources_to_delete = [ + network_instance('test-l3-svc', 'L3VRF'), + ] + LOGGER.info('resources_to_delete = {:s}'.format(str(resources_to_delete))) + results_deleteconfig = driver.DeleteConfig(resources_to_delete) + LOGGER.info('results_deleteconfig = {:s}'.format(str(results_deleteconfig))) + + network_instances = sorted(['test-l3-svc']) + results = set(results_deleteconfig) + assert len(results) == len(network_instances) + for ni_name in network_instances: + assert ('/network_instance[{:s}]'.format(ni_name), True) in results + + resources_to_get = [RESOURCE_NETWORK_INSTANCES] + LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) + results_getconfig = driver.GetConfig(resources_to_get) + LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) + + expected_getconfig = get_expected_network_instance_config(storage) + + diff_data = deepdiff.DeepDiff(sorted(expected_getconfig), sorted(results_getconfig)) + num_diffs = len(diff_data) + if num_diffs > 0: LOGGER.error('Differences[{:d}]:\n{:s}'.format(num_diffs, str(diff_data.pretty()))) + assert num_diffs == 0 + + +#def test_unitary_gnmi_openconfig( +# driver : GnmiOpenConfigDriver, # pylint: disable=redefined-outer-name +#) -> None: +# #resources_to_get = [] +# resources_to_get = [RESOURCE_ENDPOINTS] +# #resources_to_get = [RESOURCE_INTERFACES] +# #resources_to_get = [RESOURCE_NETWORK_INSTANCES] +# #resources_to_get = [RESOURCE_ROUTING_POLICIES] +# #resources_to_get = [RESOURCE_SERVICES] +# LOGGER.info('resources_to_get = {:s}'.format(str(resources_to_get))) +# results_getconfig = driver.GetConfig(resources_to_get) +# LOGGER.info('results_getconfig = {:s}'.format(str(results_getconfig))) +# +# #resources_to_set = [ +# # network_instance('test-svc', 'L3VRF'), +# # +# # interface('ethernet-1/1', 0, '172.16.0.1', 24, True), +# # network_instance_interface('test-svc', 'ethernet-1/1', 0), +# # +# # interface('ethernet-1/2', 0, '172.0.0.1', 24, True), +# # network_instance_interface('test-svc', 'ethernet-1/2', 0), +# # +# # network_instance_static_route('test-svc', '172.0.0.0/24', '172.16.0.2'), +# # network_instance_static_route('test-svc', '172.2.0.0/24', '172.16.0.3'), +# #] +# #LOGGER.info('resources_to_set = {:s}'.format(str(resources_to_set))) +# #results_setconfig = driver.SetConfig(resources_to_set) +# #LOGGER.info('results_setconfig = {:s}'.format(str(results_setconfig))) +# +# #resources_to_delete = [ +# # #network_instance_static_route('d35fc1d9', '172.0.0.0/24', '172.16.0.2'), +# # #network_instance_static_route('d35fc1d9', '172.2.0.0/24', '172.16.0.3'), +# # +# # #network_instance_interface('d35fc1d9', 'ethernet-1/1', 0), +# # #network_instance_interface('d35fc1d9', 'ethernet-1/2', 0), +# # +# # #interface('ethernet-1/1', 0, '172.16.1.1', 24, True), +# # #interface('ethernet-1/2', 0, '172.0.0.2', 24, True), +# # +# # #network_instance('20f66fb5', 'L3VRF'), +# #] +# #LOGGER.info('resources_to_delete = {:s}'.format(str(resources_to_delete))) +# #results_deleteconfig = driver.DeleteConfig(resources_to_delete) +# #LOGGER.info('results_deleteconfig = {:s}'.format(str(results_deleteconfig)))