diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index be314a8c1ee5112dd9d321dd2c1ee1dc6173aca4..5db2c5b2f2f5a73520bcea8a3fb419e0a91a577f 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -12,33 +12,62 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, List, Optional, Tuple -from common.proto.context_pb2 import Device, EndPoint +import json, netaddr, re +from typing import Dict, List, Optional, Set, Tuple +from common.DeviceTypes import DeviceTypeEnum +from common.proto.context_pb2 import ConfigActionEnum, Device, EndPoint, Service from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set - from service.service.service_handler_api.AnyTreeTools import TreeNode -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 +NETWORK_INSTANCE = 'teraflowsdn' + +RE_IF = re.compile(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$') +RE_SR = re.compile(r'^\/network_instance\[([^\]]+)\]\/protocols\[STATIC\]/route\[ ([^\:]+)\:([^\]]+)\]$') + +def _interface( + interface : str, if_type : Optional[str] = 'l3ipvlan', index : int = 0, vlan_id : Optional[int] = None, + address_ip : Optional[str] = None, address_prefix : Optional[int] = None, mtu : Optional[int] = None, + enabled : bool = True +) -> Tuple[str, Dict]: + path = '/interface[{:s}]/subinterface[{:d}]'.format(interface, index) + data = {'name': interface, 'type': if_type, 'index': index, 'enabled': enabled} + if if_type is not None: data['type'] = if_type + if vlan_id is not None: data['vlan_id'] = vlan_id + if address_ip is not None: data['address_ip'] = address_ip + if address_prefix is not None: data['address_prefix'] = address_prefix + if mtu is not None: data['mtu'] = mtu + return path, 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(ni_name : str, ni_type : str) -> Tuple[str, Dict]: + path = '/network_instance[{:s}]'.format(ni_name) + data = {'name': ni_name, 'type': ni_type} + return path, 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_protocol(ni_name : str, protocol : str) -> Tuple[str, Dict]: + path = '/network_instance[{:s}]/protocols[{:s}]'.format(ni_name, protocol) + data = {'name': ni_name, 'identifier': protocol, 'protocol_name': protocol} + return path, 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 _network_instance_protocol_static(ni_name : str) -> Tuple[str, Dict]: + return _network_instance_protocol(ni_name, 'STATIC') + +def _network_instance_protocol_static_route( + ni_name : str, prefix : str, next_hop : str, index : int = 0, metric : Optional[int] = None +) -> Tuple[str, Dict]: + protocol = 'STATIC' + path = '/network_instance[{:s}]/protocols[{:s}]/static_route[{:s}:{:d}]'.format(ni_name, protocol, prefix, index) + data = { + 'name': ni_name, 'identifier': protocol, 'protocol_name': protocol, + 'prefix': prefix, 'index': index, 'next_hop': next_hop + } + if metric is not None: data['metric'] = metric + return path, data + +def _network_instance_interface(ni_name : str, interface : str, sub_interface_index : int) -> Tuple[str, Dict]: + sub_interface_name = '{:s}.{:d}'.format(interface, sub_interface_index) + path = '/network_instance[{:s}]/interface[{:s}]'.format(ni_name, sub_interface_name) + data = {'name': ni_name, 'id': sub_interface_name, 'interface': interface, 'subinterface': sub_interface_index} + return path, data class EndpointComposer: def __init__(self, endpoint_uuid : str) -> None: @@ -46,33 +75,47 @@ class EndpointComposer: self.objekt : Optional[EndPoint] = None self.sub_interface_index = 0 self.ipv4_address = None - self.ipv4_prefix = None + self.ipv4_prefix_len = None - def configure(self, endpoint_obj : EndPoint, settings : Optional[TreeNode]) -> None: - self.objekt = endpoint_obj + def configure(self, endpoint_obj : Optional[EndPoint], settings : Optional[TreeNode]) -> None: + if endpoint_obj is not None: + self.objekt = endpoint_obj if settings is None: return json_settings : Dict = settings.value self.ipv4_address = json_settings['ipv4_address'] - self.ipv4_prefix = json_settings['ipv4_prefix'] + self.ipv4_prefix_len = json_settings['ipv4_prefix_len'] self.sub_interface_index = json_settings['sub_interface_index'] def get_config_rules(self, network_instance_name : str, delete : bool = False) -> List[Dict]: + if self.ipv4_address is None: return [] + if self.ipv4_prefix_len is None: return [] json_config_rule = json_config_rule_delete if delete else json_config_rule_set return [ json_config_rule(*_interface( - self.objekt.name, self.sub_interface_index, self.ipv4_address, self.ipv4_prefix, True + self.objekt.name, index=self.sub_interface_index, address_ip=self.ipv4_address, + address_prefix=self.ipv4_prefix_len, enabled=True )), json_config_rule(*_network_instance_interface( network_instance_name, self.objekt.name, self.sub_interface_index )), ] + def dump(self) -> Dict: + return { + 'sub_interface_index' : self.sub_interface_index, + 'ipv4_address' : self.ipv4_address, + 'ipv4_prefix_len' : self.ipv4_prefix_len, + } + class DeviceComposer: def __init__(self, device_uuid : str) -> None: self.uuid = device_uuid self.objekt : Optional[Device] = None self.endpoints : Dict[str, EndpointComposer] = dict() - self.static_routes : Dict[str, str] = dict() + self.connected : Set[str] = set() + + # {prefix => {index => (next_hop, metric)}} + self.static_routes : Dict[str, Dict[int, Tuple[str, Optional[int]]]] = dict() def get_endpoint(self, endpoint_uuid : str) -> EndpointComposer: if endpoint_uuid not in self.endpoints: @@ -81,39 +124,108 @@ class DeviceComposer: def configure(self, device_obj : Device, settings : Optional[TreeNode]) -> None: self.objekt = device_obj + for endpoint_obj in device_obj.device_endpoints: + self.get_endpoint(endpoint_obj.name).configure(endpoint_obj, None) + + for config_rule in device_obj.device_config.config_rules: + if config_rule.action != ConfigActionEnum.CONFIGACTION_SET: continue + if config_rule.WhichOneof('config_rule') != 'custom': continue + config_rule_custom = config_rule.custom + + match = RE_IF.match(config_rule_custom.resource_key) + if match is not None: + if_name, subif_index = match.groups() + resource_value = json.loads(config_rule_custom.resource_value) + ipv4_network = str(resource_value['address_ip']) + ipv4_prefix_len = int(resource_value['address_prefix']) + endpoint = self.get_endpoint(if_name) + endpoint.ipv4_address = ipv4_network + endpoint.ipv4_prefix_len = ipv4_prefix_len + endpoint.sub_interface_index = int(subif_index) + endpoint_ip_network = netaddr.IPNetwork('{:s}/{:d}'.format(ipv4_network, ipv4_prefix_len)) + self.connected.add(str(endpoint_ip_network.cidr)) + + match = RE_SR.match(config_rule_custom.resource_key) + if match is not None: + ni_name, prefix, index = match.groups() + if ni_name != NETWORK_INSTANCE: continue + resource_value : Dict = json.loads(config_rule_custom.resource_value) + next_hop = resource_value['next_hop'] + metric = resource_value.get('metric') + self.static_routes.setdefault(prefix, dict())[index] = (next_hop, metric) + if settings is None: return json_settings : Dict = settings.value - static_routes = json_settings.get('static_routes', []) + static_routes : List[Dict] = json_settings.get('static_routes', []) for static_route in static_routes: prefix = static_route['prefix'] + index = static_route.get('index', 0) next_hop = static_route['next_hop'] - self.static_routes[prefix] = next_hop + metric = static_route.get('metric') + self.static_routes.setdefault(prefix, dict())[index] = (next_hop, metric) def get_config_rules(self, network_instance_name : str, delete : bool = False) -> List[Dict]: + SELECTED_DEVICES = {DeviceTypeEnum.PACKET_ROUTER.value, DeviceTypeEnum.EMULATED_PACKET_ROUTER.value} + if self.objekt.device_type not in SELECTED_DEVICES: return [] + json_config_rule = json_config_rule_delete if delete else json_config_rule_set config_rules = [ json_config_rule(*_network_instance(network_instance_name, 'L3VRF')) ] for endpoint in self.endpoints.values(): config_rules.extend(endpoint.get_config_rules(network_instance_name, delete=delete)) - for prefix, next_hop in self.static_routes.items(): + if len(self.static_routes) > 0: config_rules.append( - json_config_rule(*_network_instance_static_route(network_instance_name, prefix, next_hop)) + json_config_rule(*_network_instance_protocol_static(network_instance_name)) ) + for prefix, indexed_static_rule in self.static_routes.items(): + for index, (next_hop, metric) in indexed_static_rule.items(): + config_rules.append( + json_config_rule(*_network_instance_protocol_static_route( + network_instance_name, prefix, next_hop, index=index, metric=metric + )) + ) if delete: config_rules = list(reversed(config_rules)) return config_rules + def dump(self) -> Dict: + return { + 'endpoints' : { + endpoint_uuid : endpoint.dump() + for endpoint_uuid, endpoint in self.endpoints.items() + }, + 'connected' : list(self.connected), + 'static_routes' : self.static_routes, + } + class ConfigRuleComposer: def __init__(self) -> None: + self.objekt : Optional[Service] = None self.devices : Dict[str, DeviceComposer] = dict() + def configure(self, service_obj : Service, settings : Optional[TreeNode]) -> None: + self.objekt = service_obj + if settings is None: return + #json_settings : Dict = settings.value + # For future use + def get_device(self, device_uuid : str) -> DeviceComposer: if device_uuid not in self.devices: self.devices[device_uuid] = DeviceComposer(device_uuid) return self.devices[device_uuid] - def get_config_rules(self, network_instance_name : str, delete : bool = False) -> Dict[str, List[Dict]]: + def get_config_rules( + self, network_instance_name : str = NETWORK_INSTANCE, delete : bool = False + ) -> Dict[str, List[Dict]]: return { device_uuid : device.get_config_rules(network_instance_name, delete=delete) for device_uuid, device in self.devices.items() } + + def dump(self) -> Dict: + return { + 'devices' : { + device_uuid : device.dump() + for device_uuid, device in self.devices.items() + } + } diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py index 5856b5f61893174a92ce02a303ae9ad30be16005..9142c9d1e32c698aef8cc1c461d6212a64b303dc 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py @@ -18,11 +18,12 @@ from common.method_wrappers.Decorator import MetricsPool, metered_subclass_metho from common.proto.context_pb2 import ConfigRule, DeviceId, Service from common.tools.object_factory.Device import json_device_id from common.type_checkers.Checkers import chk_type -from service.service.service_handler_api.Tools import get_device_endpoint_uuids, get_endpoint_matching from service.service.service_handler_api._ServiceHandler import _ServiceHandler from service.service.service_handler_api.SettingsHandler import SettingsHandler +from service.service.service_handler_api.Tools import get_device_endpoint_uuids, get_endpoint_matching from service.service.task_scheduler.TaskExecutor import TaskExecutor from .ConfigRuleComposer import ConfigRuleComposer +from .StaticRouteGenerator import StaticRouteGenerator LOGGER = logging.getLogger(__name__) @@ -35,16 +36,22 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): self.__service = service self.__task_executor = task_executor self.__settings_handler = SettingsHandler(service.service_config, **settings) - self.__composer = ConfigRuleComposer() - self.__endpoint_map : Dict[Tuple[str, str], str] = dict() + self.__config_rule_composer = ConfigRuleComposer() + self.__static_route_generator = StaticRouteGenerator(self.__config_rule_composer) + self.__endpoint_map : Dict[Tuple[str, str], Tuple[str, str]] = dict() def _compose_config_rules(self, endpoints : List[Tuple[str, str, Optional[str]]]) -> None: + if len(endpoints) % 2 != 0: raise Exception('Number of endpoints should be even') + + service_settings = self.__settings_handler.get_service_settings() + self.__config_rule_composer.configure(self.__service, service_settings) + for endpoint in endpoints: device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) device_settings = self.__settings_handler.get_device_settings(device_obj) - _device = self.__composer.get_device(device_obj.name) + _device = self.__config_rule_composer.get_device(device_obj.name) _device.configure(device_obj, device_settings) endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) @@ -52,7 +59,9 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): _endpoint = _device.get_endpoint(endpoint_obj.name) _endpoint.configure(endpoint_obj, endpoint_settings) - self.__endpoint_map[(device_uuid, endpoint_uuid)] = device_obj.name + self.__endpoint_map[(device_uuid, endpoint_uuid)] = (device_obj.name, endpoint_obj.name) + + self.__static_route_generator.compose(endpoints) def _do_configurations( self, config_rules_per_device : Dict[str, List[Dict]], endpoints : List[Tuple[str, str, Optional[str]]], @@ -62,7 +71,7 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): results_per_device = dict() for device_name,json_config_rules in config_rules_per_device.items(): try: - device_obj = self.__composer.get_device(device_name).objekt + device_obj = self.__config_rule_composer.get_device(device_name).objekt if len(json_config_rules) == 0: continue del device_obj.device_config.config_rules[:] for json_config_rule in json_config_rules: @@ -78,7 +87,8 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): results = [] for endpoint in endpoints: device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) - device_name = self.__endpoint_map[(device_uuid, endpoint_uuid)] + device_name, _ = self.__endpoint_map[(device_uuid, endpoint_uuid)] + if device_name not in results_per_device: continue results.append(results_per_device[device_name]) return results @@ -88,12 +98,14 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): ) -> List[Union[bool, Exception]]: chk_type('endpoints', endpoints, list) if len(endpoints) == 0: return [] - service_uuid = self.__service.service_id.service_uuid.uuid - #settings = self.__settings_handler.get('/settings') + #service_uuid = self.__service.service_id.service_uuid.uuid self._compose_config_rules(endpoints) - network_instance_name = service_uuid.split('-')[0] - config_rules_per_device = self.__composer.get_config_rules(network_instance_name, delete=False) + #network_instance_name = service_uuid.split('-')[0] + #config_rules_per_device = self.__config_rule_composer.get_config_rules(network_instance_name, delete=False) + config_rules_per_device = self.__config_rule_composer.get_config_rules(delete=False) + LOGGER.debug('config_rules_per_device={:s}'.format(str(config_rules_per_device))) results = self._do_configurations(config_rules_per_device, endpoints) + LOGGER.debug('results={:s}'.format(str(results))) return results @metered_subclass_method(METRICS_POOL) @@ -102,12 +114,14 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): ) -> List[Union[bool, Exception]]: chk_type('endpoints', endpoints, list) if len(endpoints) == 0: return [] - service_uuid = self.__service.service_id.service_uuid.uuid - #settings = self.__settings_handler.get('/settings') + #service_uuid = self.__service.service_id.service_uuid.uuid self._compose_config_rules(endpoints) - network_instance_name = service_uuid.split('-')[0] - config_rules_per_device = self.__composer.get_config_rules(network_instance_name, delete=True) + #network_instance_name = service_uuid.split('-')[0] + #config_rules_per_device = self.__config_rule_composer.get_config_rules(network_instance_name, delete=True) + config_rules_per_device = self.__config_rule_composer.get_config_rules(delete=True) + LOGGER.debug('config_rules_per_device={:s}'.format(str(config_rules_per_device))) results = self._do_configurations(config_rules_per_device, endpoints, delete=True) + LOGGER.debug('results={:s}'.format(str(results))) return results @metered_subclass_method(METRICS_POOL) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/StaticRouteGenerator.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/StaticRouteGenerator.py new file mode 100644 index 0000000000000000000000000000000000000000..6479a07fe98db313cf1d0e5d0ee371ae8e599388 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/StaticRouteGenerator.py @@ -0,0 +1,183 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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, logging, netaddr +from typing import List, Optional, Tuple +from .ConfigRuleComposer import ConfigRuleComposer + +LOGGER = logging.getLogger(__name__) + +# Used to infer routing networks for adjacent ports when there is no hint in device/endpoint settings +ROOT_NEIGHBOR_ROUTING_NETWORK = netaddr.IPNetwork('10.254.254.0/16') +NEIGHBOR_ROUTING_NETWORKS_PREFIX_LEN = 30 +NEIGHBOR_ROUTING_NETWORKS = set(ROOT_NEIGHBOR_ROUTING_NETWORK.subnet(NEIGHBOR_ROUTING_NETWORKS_PREFIX_LEN)) + +def _generate_neighbor_addresses() -> Tuple[netaddr.IPAddress, netaddr.IPAddress, int]: + ip_network = NEIGHBOR_ROUTING_NETWORKS.pop() + ip_addresses = list(ip_network.iter_hosts()) + ip_addresses.append(NEIGHBOR_ROUTING_NETWORKS_PREFIX_LEN) + return ip_addresses + +def _compute_gateway(ip_network : netaddr.IPNetwork, gateway_host=1) -> netaddr.IPAddress: + return netaddr.IPAddress(ip_network.cidr.first + gateway_host) + +def _compose_ipv4_network(ipv4_network, ipv4_prefix_len) -> netaddr.IPNetwork: + return netaddr.IPNetwork('{:s}/{:d}'.format(str(ipv4_network), int(ipv4_prefix_len))) + +class StaticRouteGenerator: + def __init__(self, config_rule_composer : ConfigRuleComposer) -> None: + self._config_rule_composer = config_rule_composer + + def compose(self, connection_hop_list : List[Tuple[str, str, Optional[str]]]) -> None: + link_endpoints = self._compute_link_endpoints(connection_hop_list) + LOGGER.debug('link_endpoints = {:s}'.format(str(link_endpoints))) + + self._compute_link_addresses(link_endpoints) + LOGGER.debug('config_rule_composer = {:s}'.format(json.dumps(self._config_rule_composer.dump()))) + + self._discover_connected_networks(connection_hop_list) + LOGGER.debug('config_rule_composer = {:s}'.format(json.dumps(self._config_rule_composer.dump()))) + + # Compute and propagate static routes forward (service_endpoint_a => service_endpoint_b) + self._compute_static_routes(link_endpoints) + + # Compute and propagate static routes backward (service_endpoint_b => service_endpoint_a) + reversed_endpoints = list(reversed(connection_hop_list)) + reversed_link_endpoints = self._compute_link_endpoints(reversed_endpoints) + LOGGER.debug('reversed_link_endpoints = {:s}'.format(str(reversed_link_endpoints))) + self._compute_static_routes(reversed_link_endpoints) + + LOGGER.debug('config_rule_composer = {:s}'.format(json.dumps(self._config_rule_composer.dump()))) + + def _compute_link_endpoints( + self, connection_hop_list : List[Tuple[str, str, Optional[str]]] + ) -> List[Tuple[Tuple[str, str, Optional[str]], Tuple[str, str, Optional[str]]]]: + num_connection_hops = len(connection_hop_list) + if num_connection_hops % 2 != 0: raise Exception('Number of connection hops must be even') + if num_connection_hops < 4: raise Exception('Number of connection hops must be >= 4') + + # Skip service endpoints (first and last) + it_connection_hops = iter(connection_hop_list[1:-1]) + return list(zip(it_connection_hops, it_connection_hops)) + + def _compute_link_addresses( + self, link_endpoints_list : List[Tuple[Tuple[str, str, Optional[str]], Tuple[str, str, Optional[str]]]] + ) -> None: + for link_endpoints in link_endpoints_list: + device_endpoint_a, device_endpoint_b = link_endpoints + + device_uuid_a, endpoint_uuid_a = device_endpoint_a[0:2] + endpoint_a = self._config_rule_composer.get_device(device_uuid_a).get_endpoint(endpoint_uuid_a) + + device_uuid_b, endpoint_uuid_b = device_endpoint_b[0:2] + endpoint_b = self._config_rule_composer.get_device(device_uuid_b).get_endpoint(endpoint_uuid_b) + + if endpoint_a.ipv4_address is None and endpoint_b.ipv4_address is None: + ip_endpoint_a, ip_endpoint_b, prefix_len = _generate_neighbor_addresses() + endpoint_a.ipv4_address = str(ip_endpoint_a) + endpoint_a.ipv4_prefix_len = prefix_len + endpoint_b.ipv4_address = str(ip_endpoint_b) + endpoint_b.ipv4_prefix_len = prefix_len + elif endpoint_a.ipv4_address is not None and endpoint_b.ipv4_address is None: + prefix_len = endpoint_a.ipv4_prefix_len + ip_network_a = _compose_ipv4_network(endpoint_a.ipv4_address, prefix_len) + if prefix_len > 30: + MSG = 'Unsupported prefix_len for {:s}: {:s}' + raise Exception(MSG.format(str(endpoint_a), str(prefix_len))) + ip_endpoint_b = _compute_gateway(ip_network_a, gateway_host=1) + if ip_endpoint_b == ip_network_a.ip: + ip_endpoint_b = _compute_gateway(ip_network_a, gateway_host=2) + endpoint_b.ipv4_address = str(ip_endpoint_b) + endpoint_b.ipv4_prefix_len = prefix_len + elif endpoint_a.ipv4_address is None and endpoint_b.ipv4_address is not None: + prefix_len = endpoint_b.ipv4_prefix_len + ip_network_b = _compose_ipv4_network(endpoint_b.ipv4_address, prefix_len) + if prefix_len > 30: + MSG = 'Unsupported prefix_len for {:s}: {:s}' + raise Exception(MSG.format(str(endpoint_b), str(prefix_len))) + ip_endpoint_a = _compute_gateway(ip_network_b, gateway_host=1) + if ip_endpoint_a == ip_network_b.ip: + ip_endpoint_a = _compute_gateway(ip_network_b, gateway_host=2) + endpoint_a.ipv4_address = str(ip_endpoint_a) + endpoint_a.ipv4_prefix_len = prefix_len + elif endpoint_a.ipv4_address is not None and endpoint_b.ipv4_address is not None: + ip_network_a = _compose_ipv4_network(endpoint_a.ipv4_address, endpoint_a.ipv4_prefix_len) + ip_network_b = _compose_ipv4_network(endpoint_b.ipv4_address, endpoint_b.ipv4_prefix_len) + if ip_network_a.cidr != ip_network_b.cidr: + MSG = 'Incompatible CIDRs: endpoint_a({:s})=>{:s} endpoint_b({:s})=>{:s}' + raise Exception(MSG.format(str(endpoint_a), str(ip_network_a), str(endpoint_b), str(ip_network_b))) + if ip_network_a.ip == ip_network_b.ip: + MSG = 'Duplicated IP: endpoint_a({:s})=>{:s} endpoint_b({:s})=>{:s}' + raise Exception(MSG.format(str(endpoint_a), str(ip_network_a), str(endpoint_b), str(ip_network_b))) + + def _discover_connected_networks(self, connection_hop_list : List[Tuple[str, str, Optional[str]]]) -> None: + for connection_hop in connection_hop_list: + device_uuid, endpoint_uuid = connection_hop[0:2] + device = self._config_rule_composer.get_device(device_uuid) + endpoint = device.get_endpoint(endpoint_uuid) + + if endpoint.ipv4_address is None: continue + ip_network = _compose_ipv4_network(endpoint.ipv4_address, endpoint.ipv4_prefix_len) + + device.connected.add(str(ip_network.cidr)) + + def _compute_static_routes( + self, link_endpoints_list : List[Tuple[Tuple[str, str, Optional[str]], Tuple[str, str, Optional[str]]]] + ) -> None: + for link_endpoints in link_endpoints_list: + device_endpoint_a, device_endpoint_b = link_endpoints + + device_uuid_a, endpoint_uuid_a = device_endpoint_a[0:2] + device_a = self._config_rule_composer.get_device(device_uuid_a) + endpoint_a = device_a.get_endpoint(endpoint_uuid_a) + + device_uuid_b, endpoint_uuid_b = device_endpoint_b[0:2] + device_b = self._config_rule_composer.get_device(device_uuid_b) + endpoint_b = device_b.get_endpoint(endpoint_uuid_b) + + # Compute static routes from networks connected in device_a + for ip_network_a in device_a.connected: + if ip_network_a in device_b.connected: continue + if ip_network_a in device_b.static_routes: continue + if ip_network_a in ROOT_NEIGHBOR_ROUTING_NETWORK: continue + endpoint_a_ip_network = _compose_ipv4_network(endpoint_a.ipv4_address, endpoint_a.ipv4_prefix_len) + next_hop = str(endpoint_a_ip_network.ip) + device_b.static_routes.setdefault(ip_network_a, dict())[0] = (next_hop, None) + + # Compute static routes from networks connected in device_b + for ip_network_b in device_b.connected: + if ip_network_b in device_a.connected: continue + if ip_network_b in device_a.static_routes: continue + if ip_network_b in ROOT_NEIGHBOR_ROUTING_NETWORK: continue + endpoint_b_ip_network = _compose_ipv4_network(endpoint_b.ipv4_address, endpoint_b.ipv4_prefix_len) + next_hop = str(endpoint_b_ip_network.ip) + device_a.static_routes.setdefault(ip_network_b, dict())[0] = (next_hop, None) + + # Propagate static routes from networks connected in device_a + for ip_network_a in device_a.static_routes.keys(): + if ip_network_a in device_b.connected: continue + if ip_network_a in device_b.static_routes: continue + if ip_network_a in ROOT_NEIGHBOR_ROUTING_NETWORK: continue + endpoint_a_ip_network = _compose_ipv4_network(endpoint_a.ipv4_address, endpoint_a.ipv4_prefix_len) + next_hop = str(endpoint_a_ip_network.ip) + device_b.static_routes.setdefault(ip_network_a, dict())[0] = (next_hop, None) + + # Propagate static routes from networks connected in device_b + for ip_network_b in device_b.static_routes.keys(): + if ip_network_b in device_a.connected: continue + if ip_network_b in device_a.static_routes: continue + if ip_network_b in ROOT_NEIGHBOR_ROUTING_NETWORK: continue + endpoint_b_ip_network = _compose_ipv4_network(endpoint_b.ipv4_address, endpoint_b.ipv4_prefix_len) + next_hop = str(endpoint_b_ip_network.ip) + device_a.static_routes.setdefault(ip_network_b, dict())[0] = (next_hop, None) diff --git a/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockServiceHandler.py b/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockServiceHandler.py new file mode 100644 index 0000000000000000000000000000000000000000..9b3f76566c9d8e5b2c8bdfb05f4b2448c29b7eae --- /dev/null +++ b/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockServiceHandler.py @@ -0,0 +1,160 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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, logging +from typing import Any, Dict, List, Optional, Tuple, Union +from common.proto.context_pb2 import ConfigRule, DeviceId, Service +from common.tools.object_factory.Device import json_device_id +from common.type_checkers.Checkers import chk_type +from service.service.service_handler_api._ServiceHandler import _ServiceHandler +from service.service.service_handler_api.SettingsHandler import SettingsHandler +from service.service.service_handler_api.Tools import get_device_endpoint_uuids, get_endpoint_matching +from .MockTaskExecutor import MockTaskExecutor +from service.service.service_handlers.l3nm_gnmi_openconfig.ConfigRuleComposer import ConfigRuleComposer +from service.service.service_handlers.l3nm_gnmi_openconfig.StaticRouteGenerator import StaticRouteGenerator + +LOGGER = logging.getLogger(__name__) + +class MockServiceHandler(_ServiceHandler): + def __init__( # pylint: disable=super-init-not-called + self, service : Service, task_executor : MockTaskExecutor, **settings + ) -> None: + self.__service = service + self.__task_executor = task_executor + self.__settings_handler = SettingsHandler(service.service_config, **settings) + self.__config_rule_composer = ConfigRuleComposer() + self.__static_route_generator = StaticRouteGenerator(self.__config_rule_composer) + self.__endpoint_map : Dict[Tuple[str, str], Tuple[str, str]] = dict() + + def _compose_config_rules(self, endpoints : List[Tuple[str, str, Optional[str]]]) -> None: + if len(endpoints) % 2 != 0: raise Exception('Number of endpoints should be even') + + service_settings = self.__settings_handler.get_service_settings() + self.__config_rule_composer.configure(self.__service, service_settings) + + for endpoint in endpoints: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + device_settings = self.__settings_handler.get_device_settings(device_obj) + _device = self.__config_rule_composer.get_device(device_obj.name) + _device.configure(device_obj, device_settings) + + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + endpoint_settings = self.__settings_handler.get_endpoint_settings(device_obj, endpoint_obj) + _endpoint = _device.get_endpoint(endpoint_obj.name) + _endpoint.configure(endpoint_obj, endpoint_settings) + + self.__endpoint_map[(device_uuid, endpoint_uuid)] = (device_obj.name, endpoint_obj.name) + + self.__static_route_generator.compose(endpoints) + + def _do_configurations( + self, config_rules_per_device : Dict[str, List[Dict]], endpoints : List[Tuple[str, str, Optional[str]]], + delete : bool = False + ) -> List[Union[bool, Exception]]: + # Configuration is done atomically on each device, all OK / all KO per device + results_per_device = dict() + for device_name,json_config_rules in config_rules_per_device.items(): + try: + device_obj = self.__config_rule_composer.get_device(device_name).objekt + if len(json_config_rules) == 0: continue + del device_obj.device_config.config_rules[:] + for json_config_rule in json_config_rules: + device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) + self.__task_executor.configure_device(device_obj) + results_per_device[device_name] = True + except Exception as e: # pylint: disable=broad-exception-caught + verb = 'deconfigure' if delete else 'configure' + MSG = 'Unable to {:s} Device({:s}) : ConfigRules({:s})' + LOGGER.exception(MSG.format(verb, str(device_name), str(json_config_rules))) + results_per_device[device_name] = e + + results = [] + for endpoint in endpoints: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + device_name, _ = self.__endpoint_map[(device_uuid, endpoint_uuid)] + if device_name not in results_per_device: continue + results.append(results_per_device[device_name]) + return results + + def SetEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + self._compose_config_rules(endpoints) + config_rules_per_device = self.__config_rule_composer.get_config_rules(delete=False) + LOGGER.debug('config_rules_per_device={:s}'.format(str(config_rules_per_device))) + results = self._do_configurations(config_rules_per_device, endpoints) + LOGGER.debug('results={:s}'.format(str(results))) + return results + + def DeleteEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + self._compose_config_rules(endpoints) + config_rules_per_device = self.__config_rule_composer.get_config_rules(delete=True) + LOGGER.debug('config_rules_per_device={:s}'.format(str(config_rules_per_device))) + results = self._do_configurations(config_rules_per_device, endpoints, delete=True) + LOGGER.debug('results={:s}'.format(str(results))) + return results + + def SetConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[SetConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + def DeleteConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[DeleteConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + resource_value = json.loads(resource[1]) + self.__settings_handler.set(resource[0], resource_value) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to SetConfig({:s})'.format(str(resource))) + results.append(e) + + return results + + def DeleteConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + self.__settings_handler.delete(resource[0]) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteConfig({:s})'.format(str(resource))) + results.append(e) + + return results diff --git a/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockTaskExecutor.py b/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockTaskExecutor.py new file mode 100644 index 0000000000000000000000000000000000000000..765b04477efdf06bfef934e96329887e898aa1b4 --- /dev/null +++ b/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockTaskExecutor.py @@ -0,0 +1,57 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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 +from enum import Enum +from typing import Dict, Optional, Union +from common.method_wrappers.ServiceExceptions import NotFoundException +from common.proto.context_pb2 import Connection, Device, DeviceId, Service +from service.service.tools.ObjectKeys import get_device_key + +LOGGER = logging.getLogger(__name__) + +CacheableObject = Union[Connection, Device, Service] + +class CacheableObjectType(Enum): + CONNECTION = 'connection' + DEVICE = 'device' + SERVICE = 'service' + +class MockTaskExecutor: + def __init__(self) -> None: + self._grpc_objects_cache : Dict[str, CacheableObject] = dict() + + # ----- Common methods --------------------------------------------------------------------------------------------- + + def _load_grpc_object(self, object_type : CacheableObjectType, object_key : str) -> Optional[CacheableObject]: + object_key = '{:s}:{:s}'.format(object_type.value, object_key) + return self._grpc_objects_cache.get(object_key) + + def _store_grpc_object(self, object_type : CacheableObjectType, object_key : str, grpc_object) -> None: + object_key = '{:s}:{:s}'.format(object_type.value, object_key) + self._grpc_objects_cache[object_key] = grpc_object + + def _delete_grpc_object(self, object_type : CacheableObjectType, object_key : str) -> None: + object_key = '{:s}:{:s}'.format(object_type.value, object_key) + self._grpc_objects_cache.pop(object_key, None) + + def get_device(self, device_id : DeviceId) -> Device: + device_key = get_device_key(device_id) + device = self._load_grpc_object(CacheableObjectType.DEVICE, device_key) + if device is None: raise NotFoundException('Device', device_key) + return device + + def configure_device(self, device : Device) -> None: + device_key = get_device_key(device.device_id) + self._store_grpc_object(CacheableObjectType.DEVICE, device_key, device) diff --git a/src/service/tests/test_l3nm_gnmi_static_rule_gen/__init__.py b/src/service/tests/test_l3nm_gnmi_static_rule_gen/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 --- /dev/null +++ b/src/service/tests/test_l3nm_gnmi_static_rule_gen/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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. + diff --git a/src/service/tests/test_l3nm_gnmi_static_rule_gen/test_unitary.py b/src/service/tests/test_l3nm_gnmi_static_rule_gen/test_unitary.py new file mode 100644 index 0000000000000000000000000000000000000000..43709b036b8158ddfc59453aa798fa2d303906e0 --- /dev/null +++ b/src/service/tests/test_l3nm_gnmi_static_rule_gen/test_unitary.py @@ -0,0 +1,147 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (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. + +# Run with: +# $ PYTHONPATH=./src python -m service.tests.test_l3nm_gnmi_static_rule_gen.test_unitary + +import logging +from typing import List, Optional, Tuple +from common.DeviceTypes import DeviceTypeEnum +from common.proto.context_pb2 import Device, DeviceOperationalStatusEnum, Service +from common.tools.object_factory.ConfigRule import json_config_rule_set +from common.tools.object_factory.Device import json_device, json_device_id +from common.tools.object_factory.EndPoint import json_endpoint, json_endpoint_id +from common.tools.object_factory.Service import json_service_l3nm_planned +from .MockServiceHandler import MockServiceHandler +from .MockTaskExecutor import CacheableObjectType, MockTaskExecutor + +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger(__name__) + +SERVICE_DC1_DC2 = Service(**json_service_l3nm_planned( + 'svc-dc1-dc2-uuid', + endpoint_ids=[ + json_endpoint_id(json_device_id('DC1'), 'int'), + json_endpoint_id(json_device_id('DC2'), 'int'), + ], + config_rules=[ + json_config_rule_set('/device[DC1]/endpoint[eth0]/settings', { + 'ipv4_address': '192.168.10.10', 'ipv4_prefix_len': 24, 'sub_interface_index': 0 + }), + json_config_rule_set('/device[R1]/endpoint[1/2]/settings', { + 'ipv4_address': '10.0.1.1', 'ipv4_prefix_len': 24, 'sub_interface_index': 0 + }), + #json_config_rule_set('/device[R2]/endpoint[1/2]/settings', { + # 'ipv4_address': '10.0.2.1', 'ipv4_prefix_len': 24, 'sub_interface_index': 0 + #}), + json_config_rule_set('/device[DC2]/endpoint[eth0]/settings', { + 'ipv4_address': '192.168.20.10', 'ipv4_prefix_len': 24, 'sub_interface_index': 0 + }), + ] +)) + +SERVICE_DC1_DC3 = Service(**json_service_l3nm_planned( + 'svc-dc1-dc3-uuid', + endpoint_ids=[ + json_endpoint_id(json_device_id('DC1'), 'int'), + json_endpoint_id(json_device_id('DC3'), 'int'), + ], + config_rules=[ + json_config_rule_set('/device[DC1]/endpoint[eth0]/settings', { + 'ipv4_address': '192.168.10.10', 'ipv4_prefix_len': 24, 'sub_interface_index': 0 + }), + #json_config_rule_set('/device[R1]/endpoint[1/2]/settings', { + # 'ipv4_address': '10.0.1.1', 'ipv4_prefix_len': 24, 'sub_interface_index': 0 + #}), + json_config_rule_set('/device[R4]/endpoint[1/1]/settings', { + 'ipv4_address': '10.0.4.1', 'ipv4_prefix_len': 24, 'sub_interface_index': 0 + }), + json_config_rule_set('/device[DC3]/endpoint[eth0]/settings', { + 'ipv4_address': '192.168.30.10', 'ipv4_prefix_len': 24, 'sub_interface_index': 0 + }), + ] +)) + +CONNECTION_ENDPOINTS_DC1_DC2 : List[Tuple[str, str, Optional[str]]] = [ + ('DC1', 'int', None), ('DC1', 'eth0', None), + ('R1', '1/1', None), ('R1', '1/2', None), + ('R2', '1/1', None), ('R2', '1/2', None), + ('R3', '1/1', None), ('R3', '1/2', None), + ('DC2', 'eth0', None), ('DC2', 'int', None), +] + +CONNECTION_ENDPOINTS_DC1_DC3 : List[Tuple[str, str, Optional[str]]] = [ + ('DC1', 'int', None), ('DC1', 'eth0', None), + ('R1', '1/1', None), ('R1', '1/2', None), + ('R2', '1/1', None), ('R2', '1/3', None), + ('R4', '1/1', None), ('R4', '1/2', None), + ('DC3', 'eth0', None), ('DC3', 'int', None), +] + +def test_l3nm_gnmi_static_rule_gen() -> None: + dev_op_st_enabled = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED + + mock_task_executor = MockTaskExecutor() + mock_task_executor._store_grpc_object(CacheableObjectType.DEVICE, 'DC1', Device(**json_device( + 'uuid-DC1', DeviceTypeEnum.EMULATED_DATACENTER.value, dev_op_st_enabled, name='DC1', endpoints=[ + json_endpoint(json_device_id('uuid-DC1'), 'uuid-int', 'packet', name='int' ), + json_endpoint(json_device_id('uuid-DC1'), 'uuid-eth0', 'packet', name='eth0'), + ] + ))) + mock_task_executor._store_grpc_object(CacheableObjectType.DEVICE, 'DC2', Device(**json_device( + 'uuid-DC2', DeviceTypeEnum.EMULATED_DATACENTER.value, dev_op_st_enabled, name='DC2', endpoints=[ + json_endpoint(json_device_id('uuid-DC2'), 'uuid-int', 'packet', name='int' ), + json_endpoint(json_device_id('uuid-DC2'), 'uuid-eth0', 'packet', name='eth0'), + ] + ))) + mock_task_executor._store_grpc_object(CacheableObjectType.DEVICE, 'DC3', Device(**json_device( + 'uuid-DC3', DeviceTypeEnum.EMULATED_DATACENTER.value, dev_op_st_enabled, name='DC3', endpoints=[ + json_endpoint(json_device_id('uuid-DC3'), 'uuid-int', 'packet', name='int' ), + json_endpoint(json_device_id('uuid-DC3'), 'uuid-eth0', 'packet', name='eth0'), + ] + ))) + mock_task_executor._store_grpc_object(CacheableObjectType.DEVICE, 'R1', Device(**json_device( + 'uuid-R1', DeviceTypeEnum.EMULATED_PACKET_ROUTER.value, dev_op_st_enabled, name='R1', endpoints=[ + json_endpoint(json_device_id('uuid-R1'), 'uuid-1/1', 'packet', name='1/1'), + json_endpoint(json_device_id('uuid-R1'), 'uuid-1/2', 'packet', name='1/2'), + ] + ))) + mock_task_executor._store_grpc_object(CacheableObjectType.DEVICE, 'R2', Device(**json_device( + 'uuid-R2', DeviceTypeEnum.EMULATED_PACKET_ROUTER.value, dev_op_st_enabled, name='R2', endpoints=[ + json_endpoint(json_device_id('uuid-R2'), 'uuid-1/1', 'packet', name='1/1'), + json_endpoint(json_device_id('uuid-R2'), 'uuid-1/2', 'packet', name='1/2'), + json_endpoint(json_device_id('uuid-R2'), 'uuid-1/3', 'packet', name='1/3'), + ] + ))) + mock_task_executor._store_grpc_object(CacheableObjectType.DEVICE, 'R3', Device(**json_device( + 'uuid-R3', DeviceTypeEnum.EMULATED_PACKET_ROUTER.value, dev_op_st_enabled, name='R3', endpoints=[ + json_endpoint(json_device_id('uuid-R3'), 'uuid-1/1', 'packet', name='1/1'), + json_endpoint(json_device_id('uuid-R3'), 'uuid-1/2', 'packet', name='1/2'), + ] + ))) + mock_task_executor._store_grpc_object(CacheableObjectType.DEVICE, 'R4', Device(**json_device( + 'uuid-R4', DeviceTypeEnum.EMULATED_PACKET_ROUTER.value, dev_op_st_enabled, name='R4', endpoints=[ + json_endpoint(json_device_id('uuid-R4'), 'uuid-1/1', 'packet', name='1/1'), + json_endpoint(json_device_id('uuid-R4'), 'uuid-1/2', 'packet', name='1/2'), + ] + ))) + + mock_service_handler = MockServiceHandler(SERVICE_DC1_DC2, mock_task_executor) + mock_service_handler.SetEndpoint(CONNECTION_ENDPOINTS_DC1_DC2) + + mock_service_handler = MockServiceHandler(SERVICE_DC1_DC3, mock_task_executor) + mock_service_handler.SetEndpoint(CONNECTION_ENDPOINTS_DC1_DC3) + +if __name__ == '__main__': + test_l3nm_gnmi_static_rule_gen()