diff --git a/src/common/DeviceTypes.py b/src/common/DeviceTypes.py index f88f931d4c465349a37a92b5891fd0acd8fe6a48..72b3e21fdecd1019099eec03b3b473f56bcd403a 100644 --- a/src/common/DeviceTypes.py +++ b/src/common/DeviceTypes.py @@ -22,6 +22,7 @@ class DeviceTypeEnum(Enum): # Emulated device types EMULATED_CLIENT = 'emu-client' EMULATED_DATACENTER = 'emu-datacenter' + EMULATED_IP_SDN_CONTROLLER = 'emu-ip-sdn-controller' EMULATED_MICROWAVE_RADIO_SYSTEM = 'emu-microwave-radio-system' EMULATED_OPEN_LINE_SYSTEM = 'emu-open-line-system' EMULATED_OPTICAL_ROADM = 'emu-optical-roadm' @@ -36,6 +37,7 @@ class DeviceTypeEnum(Enum): # Real device types CLIENT = 'client' DATACENTER = 'datacenter' + IP_SDN_CONTROLLER = 'ip-sdn-controller' MICROWAVE_RADIO_SYSTEM = 'microwave-radio-system' OPEN_LINE_SYSTEM = 'open-line-system' OPTICAL_ROADM = 'optical-roadm' diff --git a/src/common/tools/descriptor/Loader.py b/src/common/tools/descriptor/Loader.py index c5468c19ccc063387460ed7f7b28ee70c5f1d907..4ab33beae8987ff2b38e5e2bc0252bacf557120c 100644 --- a/src/common/tools/descriptor/Loader.py +++ b/src/common/tools/descriptor/Loader.py @@ -46,7 +46,7 @@ from slice.client.SliceClient import SliceClient from .Tools import ( format_device_custom_config_rules, format_service_custom_config_rules, format_slice_custom_config_rules, get_descriptors_add_contexts, get_descriptors_add_services, get_descriptors_add_slices, - get_descriptors_add_topologies, split_devices_by_rules) + get_descriptors_add_topologies, split_controllers_and_network_devices, split_devices_by_rules) LOGGER = logging.getLogger(__name__) LOGGERS = { @@ -56,9 +56,10 @@ LOGGERS = { } ENTITY_TO_TEXT = { - # name => singular, plural + # name => singular, plural 'context' : ('Context', 'Contexts' ), 'topology' : ('Topology', 'Topologies' ), + 'controller': ('Controller', 'Controllers'), 'device' : ('Device', 'Devices' ), 'link' : ('Link', 'Links' ), 'service' : ('Service', 'Services' ), @@ -68,8 +69,8 @@ ENTITY_TO_TEXT = { ACTION_TO_TEXT = { # action => infinitive, past - 'add' : ('Add', 'Added'), - 'update' : ('Update', 'Updated'), + 'add' : ('Add', 'Added' ), + 'update' : ('Update', 'Updated' ), 'config' : ('Configure', 'Configured'), } @@ -231,10 +232,12 @@ class DescriptorLoader: def _load_dummy_mode(self) -> None: # Dummy Mode: used to pre-load databases (WebUI debugging purposes) with no smart or automated tasks. + controllers, network_devices = split_controllers_and_network_devices(self.__devices) self.__ctx_cli.connect() self._process_descr('context', 'add', self.__ctx_cli.SetContext, Context, self.__contexts_add ) self._process_descr('topology', 'add', self.__ctx_cli.SetTopology, Topology, self.__topologies_add) - self._process_descr('device', 'add', self.__ctx_cli.SetDevice, Device, self.__devices ) + self._process_descr('controller', 'add', self.__ctx_cli.SetDevice, Device, controllers ) + self._process_descr('device', 'add', self.__ctx_cli.SetDevice, Device, network_devices ) self._process_descr('link', 'add', self.__ctx_cli.SetLink, Link, self.__links ) self._process_descr('service', 'add', self.__ctx_cli.SetService, Service, self.__services ) self._process_descr('slice', 'add', self.__ctx_cli.SetSlice, Slice, self.__slices ) @@ -262,20 +265,23 @@ class DescriptorLoader: self.__services_add = get_descriptors_add_services(self.__services) self.__slices_add = get_descriptors_add_slices(self.__slices) + controllers_add, network_devices_add = split_controllers_and_network_devices(self.__devices_add) + self.__ctx_cli.connect() self.__dev_cli.connect() self.__svc_cli.connect() self.__slc_cli.connect() - self._process_descr('context', 'add', self.__ctx_cli.SetContext, Context, self.__contexts_add ) - self._process_descr('topology', 'add', self.__ctx_cli.SetTopology, Topology, self.__topologies_add) - self._process_descr('device', 'add', self.__dev_cli.AddDevice, Device, self.__devices_add ) - self._process_descr('device', 'config', self.__dev_cli.ConfigureDevice, Device, self.__devices_config) - self._process_descr('link', 'add', self.__ctx_cli.SetLink, Link, self.__links ) - self._process_descr('service', 'add', self.__svc_cli.CreateService, Service, self.__services_add ) - self._process_descr('service', 'update', self.__svc_cli.UpdateService, Service, self.__services ) - self._process_descr('slice', 'add', self.__slc_cli.CreateSlice, Slice, self.__slices_add ) - self._process_descr('slice', 'update', self.__slc_cli.UpdateSlice, Slice, self.__slices ) + self._process_descr('context', 'add', self.__ctx_cli.SetContext, Context, self.__contexts_add ) + self._process_descr('topology', 'add', self.__ctx_cli.SetTopology, Topology, self.__topologies_add) + self._process_descr('controller', 'add', self.__dev_cli.AddDevice, Device, controllers_add ) + self._process_descr('device', 'add', self.__dev_cli.AddDevice, Device, network_devices_add ) + self._process_descr('device', 'config', self.__dev_cli.ConfigureDevice, Device, self.__devices_config) + self._process_descr('link', 'add', self.__ctx_cli.SetLink, Link, self.__links ) + self._process_descr('service', 'add', self.__svc_cli.CreateService, Service, self.__services_add ) + self._process_descr('service', 'update', self.__svc_cli.UpdateService, Service, self.__services ) + self._process_descr('slice', 'add', self.__slc_cli.CreateSlice, Slice, self.__slices_add ) + self._process_descr('slice', 'update', self.__slc_cli.UpdateSlice, Slice, self.__slices ) # By default the Context component automatically assigns devices and links to topologies based on their # endpoints, and assigns topologies, services, and slices to contexts based on their identifiers. diff --git a/src/common/tools/descriptor/Tools.py b/src/common/tools/descriptor/Tools.py index 3126f2bcedb1aaeeda660674d41a249af03679fe..b4a76ff4f00d0f6886895cca0ab6f27f7aa8aa43 100644 --- a/src/common/tools/descriptor/Tools.py +++ b/src/common/tools/descriptor/Tools.py @@ -14,6 +14,7 @@ import copy, json from typing import Dict, List, Optional, Tuple, Union +from common.DeviceTypes import DeviceTypeEnum def get_descriptors_add_contexts(contexts : List[Dict]) -> List[Dict]: contexts_add = copy.deepcopy(contexts) @@ -103,3 +104,24 @@ def split_devices_by_rules(devices : List[Dict]) -> Tuple[List[Dict], List[Dict] devices_config.append(device) return devices_add, devices_config + +CONTROLLER_DEVICE_TYPES = { + DeviceTypeEnum.EMULATED_IP_SDN_CONTROLLER.value, + DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM.value, + DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value, + DeviceTypeEnum.IP_SDN_CONTROLLER.value, + DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value, + DeviceTypeEnum.OPEN_LINE_SYSTEM.value, + DeviceTypeEnum.TERAFLOWSDN_CONTROLLER.value, +} + +def split_controllers_and_network_devices(devices : List[Dict]) -> Tuple[List[Dict], List[Dict]]: + controllers : List[Dict] = list() + network_devices : List[Dict] = list() + for device in devices: + device_type = device.get('device_type') + if device_type in CONTROLLER_DEVICE_TYPES: + controllers.append(device) + else: + network_devices.append(device) + return controllers, network_devices diff --git a/src/common/tools/grpc/Constraints.py b/src/common/tools/grpc/Constraints.py index 07f0b7782dbd93479774af6324683753f906c5a1..63e707c6f0232486de7761cc97b214ce16b524fd 100644 --- a/src/common/tools/grpc/Constraints.py +++ b/src/common/tools/grpc/Constraints.py @@ -18,11 +18,12 @@ import json from typing import Any, Dict, List, Optional, Tuple -from common.proto.context_pb2 import Constraint, EndPointId +from common.proto.context_pb2 import Constraint, ConstraintActionEnum, EndPointId from common.tools.grpc.Tools import grpc_message_to_json_string def update_constraint_custom_scalar( - constraints, constraint_type : str, value : Any, raise_if_differs : bool = False + constraints, constraint_type : str, value : Any, raise_if_differs : bool = False, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET ) -> Constraint: for constraint in constraints: @@ -36,6 +37,8 @@ def update_constraint_custom_scalar( constraint.custom.constraint_type = constraint_type json_constraint_value = None + constraint.action = new_action + if (json_constraint_value is None) or not raise_if_differs: # missing or raise_if_differs=False, add/update it json_constraint_value = value @@ -47,7 +50,10 @@ def update_constraint_custom_scalar( constraint.custom.constraint_value = json.dumps(json_constraint_value, sort_keys=True) return constraint -def update_constraint_custom_dict(constraints, constraint_type : str, fields : Dict[str, Tuple[Any, bool]]) -> Constraint: +def update_constraint_custom_dict( + constraints, constraint_type : str, fields : Dict[str, Tuple[Any, bool]], + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: # fields: Dict[field_name : str, Tuple[field_value : Any, raise_if_differs : bool]] for constraint in constraints: @@ -61,6 +67,8 @@ def update_constraint_custom_dict(constraints, constraint_type : str, fields : D constraint.custom.constraint_type = constraint_type json_constraint_value = {} + constraint.action = new_action + for field_name,(field_value, raise_if_differs) in fields.items(): if (field_name not in json_constraint_value) or not raise_if_differs: # missing or raise_if_differs=False, add/update it @@ -75,7 +83,8 @@ def update_constraint_custom_dict(constraints, constraint_type : str, fields : D def update_constraint_endpoint_location( constraints, endpoint_id : EndPointId, - region : Optional[str] = None, gps_position : Optional[Tuple[float, float]] = None + region : Optional[str] = None, gps_position : Optional[Tuple[float, float]] = None, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET ) -> Constraint: # gps_position: (latitude, longitude) if region is not None and gps_position is not None: @@ -103,6 +112,8 @@ def update_constraint_endpoint_location( _endpoint_id.topology_id.topology_uuid.uuid = topology_uuid _endpoint_id.topology_id.context_id.context_uuid.uuid = context_uuid + constraint.action = new_action + location = constraint.endpoint_location.location if region is not None: location.region = region @@ -111,7 +122,10 @@ def update_constraint_endpoint_location( location.gps_position.longitude = gps_position[1] return constraint -def update_constraint_endpoint_priority(constraints, endpoint_id : EndPointId, priority : int) -> Constraint: +def update_constraint_endpoint_priority( + constraints, endpoint_id : EndPointId, priority : int, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: endpoint_uuid = endpoint_id.endpoint_uuid.uuid device_uuid = endpoint_id.device_id.device_uuid.uuid topology_uuid = endpoint_id.topology_id.topology_uuid.uuid @@ -134,10 +148,15 @@ def update_constraint_endpoint_priority(constraints, endpoint_id : EndPointId, p _endpoint_id.topology_id.topology_uuid.uuid = topology_uuid _endpoint_id.topology_id.context_id.context_uuid.uuid = context_uuid + constraint.action = new_action + constraint.endpoint_priority.priority = priority return constraint -def update_constraint_sla_capacity(constraints, capacity_gbps : float) -> Constraint: +def update_constraint_sla_capacity( + constraints, capacity_gbps : float, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: for constraint in constraints: if constraint.WhichOneof('constraint') != 'sla_capacity': continue break # found, end loop @@ -145,10 +164,15 @@ def update_constraint_sla_capacity(constraints, capacity_gbps : float) -> Constr # not found, add it constraint = constraints.add() # pylint: disable=no-member + constraint.action = new_action + constraint.sla_capacity.capacity_gbps = capacity_gbps return constraint -def update_constraint_sla_latency(constraints, e2e_latency_ms : float) -> Constraint: +def update_constraint_sla_latency( + constraints, e2e_latency_ms : float, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: for constraint in constraints: if constraint.WhichOneof('constraint') != 'sla_latency': continue break # found, end loop @@ -156,11 +180,14 @@ def update_constraint_sla_latency(constraints, e2e_latency_ms : float) -> Constr # not found, add it constraint = constraints.add() # pylint: disable=no-member + constraint.action = new_action + constraint.sla_latency.e2e_latency_ms = e2e_latency_ms return constraint def update_constraint_sla_availability( - constraints, num_disjoint_paths : int, all_active : bool, availability : float + constraints, num_disjoint_paths : int, all_active : bool, availability : float, + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET ) -> Constraint: for constraint in constraints: if constraint.WhichOneof('constraint') != 'sla_availability': continue @@ -169,12 +196,17 @@ def update_constraint_sla_availability( # not found, add it constraint = constraints.add() # pylint: disable=no-member + constraint.action = new_action + constraint.sla_availability.num_disjoint_paths = num_disjoint_paths constraint.sla_availability.all_active = all_active constraint.sla_availability.availability = availability return constraint -def update_constraint_sla_isolation(constraints, isolation_levels : List[int]) -> Constraint: +def update_constraint_sla_isolation( + constraints, isolation_levels : List[int], + new_action : ConstraintActionEnum = ConstraintActionEnum.CONSTRAINTACTION_SET +) -> Constraint: for constraint in constraints: if constraint.WhichOneof('constraint') != 'sla_isolation': continue break # found, end loop @@ -182,6 +214,8 @@ def update_constraint_sla_isolation(constraints, isolation_levels : List[int]) - # not found, add it constraint = constraints.add() # pylint: disable=no-member + constraint.action = new_action + for isolation_level in isolation_levels: if isolation_level in constraint.sla_isolation.isolation_level: continue constraint.sla_isolation.isolation_level.append(isolation_level) diff --git a/src/device/service/DeviceServiceServicerImpl.py b/src/device/service/DeviceServiceServicerImpl.py index eeffdd7b0592b5166c06c1597e17f79adcfd25bb..3df7c482272804eb2589ed1e7569f0a2e822ad21 100644 --- a/src/device/service/DeviceServiceServicerImpl.py +++ b/src/device/service/DeviceServiceServicerImpl.py @@ -73,6 +73,13 @@ class DeviceServiceServicerImpl(DeviceServiceServicer): device.device_operational_status = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_UNDEFINED device.device_drivers.extend(request.device_drivers) # pylint: disable=no-member device.device_config.CopyFrom(request.device_config) # pylint: disable=no-member + + if request.HasField('controller_id'): + controller_id = request.controller_id + if controller_id.HasField('device_uuid'): + controller_device_uuid = controller_id.device_uuid.uuid + device.controller_id.device_uuid.uuid = controller_device_uuid + device_id = context_client.SetDevice(device) device = get_device(context_client, device_id.device_uuid.uuid, rw_copy=True) diff --git a/src/device/service/drivers/ietf_actn/IetfActnDriver.py b/src/device/service/drivers/ietf_actn/IetfActnDriver.py index a33c403f3202ca5ee3025a7b7808ad53a89ede4a..5f80f5333cc55ccddebd971d4aadbfa1c195ee21 100644 --- a/src/device/service/drivers/ietf_actn/IetfActnDriver.py +++ b/src/device/service/drivers/ietf_actn/IetfActnDriver.py @@ -16,7 +16,7 @@ import json, logging, requests, threading from typing import Any, Iterator, List, Optional, Tuple, Union from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method from common.type_checkers.Checkers import chk_string, chk_type -from device.service.driver_api._Driver import _Driver, RESOURCE_SERVICES +from device.service.driver_api._Driver import _Driver, RESOURCE_ENDPOINTS, RESOURCE_SERVICES from .handlers.EthtServiceHandler import EthtServiceHandler from .handlers.OsuTunnelHandler import OsuTunnelHandler from .handlers.RestApiClient import RestApiClient @@ -25,6 +25,7 @@ from .Tools import get_etht_services, get_osu_tunnels, parse_resource_key LOGGER = logging.getLogger(__name__) ALL_RESOURCE_KEYS = [ + RESOURCE_ENDPOINTS, RESOURCE_SERVICES, ] @@ -78,7 +79,12 @@ class IetfActnDriver(_Driver): try: _results = list() - if resource_key == RESOURCE_SERVICES: + if resource_key == RESOURCE_ENDPOINTS: + # Add mgmt endpoint by default + resource_key = '/endpoints/endpoint[mgmt]' + resource_value = {'uuid': 'mgmt', 'name': 'mgmt', 'type': 'mgmt'} + results.append((resource_key, resource_value)) + elif resource_key == RESOURCE_SERVICES: get_osu_tunnels(self._handler_osu_tunnel, _results) get_etht_services(self._handler_etht_service, _results) else: diff --git a/src/device/tests/data/ietf_actn/config_rules.json b/src/device/tests/data/ietf_actn/config_rules.json index d73a68674731cf4af7321044d345470c03d68134..d106a5a8f9eb705e975c14aacd489ecfa042625f 100644 --- a/src/device/tests/data/ietf_actn/config_rules.json +++ b/src/device/tests/data/ietf_actn/config_rules.json @@ -26,7 +26,7 @@ ["128.32.20.5", 24, "128.32.33.5"] ], "dst_node_id": "10.0.30.1", "dst_tp_id": "200", "dst_vlan_tag": 201, "dst_static_routes": [ - ["172.1.101.22", 24, "172.10.33.5"] + ["172.1.201.22", 24, "172.10.33.5"] ] }}} ] diff --git a/src/device/tests/data/ietf_actn/expected_etht_services.json b/src/device/tests/data/ietf_actn/expected_etht_services.json index d9f41052692936fd5fffd855b1ba3f3e0478a3b6..72c48e6b3351a4adc5737bbc1f63952363893f96 100644 --- a/src/device/tests/data/ietf_actn/expected_etht_services.json +++ b/src/device/tests/data/ietf_actn/expected_etht_services.json @@ -139,7 +139,7 @@ "is-terminal": true, "static-route-list": [ { - "destination": "172.1.101.22", + "destination": "172.1.201.22", "destination-mask": 24, "next-hop": "172.10.33.5" } diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py index 2192ea94214201697d196ca7546a906a13197a93..80c7b32ddf6cabf8a6c124ec20ddae2f5cd181ad 100644 --- a/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/Handlers.py @@ -55,7 +55,7 @@ def process_vpn_service( def update_service_endpoint( service_uuid : str, site_id : str, device_uuid : str, endpoint_uuid : str, - vlan_tag : int, ipv4_address : str, ipv4_prefix_length : int, + vlan_tag : int, ipv4_address : str, neighbor_ipv4_address : str, ipv4_prefix_length : int, capacity_gbps : Optional[float] = None, e2e_latency_ms : Optional[float] = None, availability : Optional[float] = None, mtu : Optional[int] = None, static_routing : Optional[Dict[Tuple[str, str], str]] = None, @@ -91,12 +91,15 @@ def update_service_endpoint( for (ip_range, ip_prefix_len, lan_tag), next_hop in static_routing.items() }) - ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' - endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid, vlan_tag) + #ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' + #endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid, vlan_tag) + ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/settings' + endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid) field_updates = {} - if vlan_tag is not None: field_updates['vlan_tag' ] = (vlan_tag, True) - if ipv4_address is not None: field_updates['ip_address' ] = (ipv4_address, True) - if ipv4_prefix_length is not None: field_updates['prefix_length'] = (ipv4_prefix_length, True) + if vlan_tag is not None: field_updates['vlan_tag' ] = (vlan_tag, True) + if ipv4_address is not None: field_updates['ip_address' ] = (ipv4_address, True) + if neighbor_ipv4_address is not None: field_updates['neighbor_address'] = (neighbor_ipv4_address, True) + if ipv4_prefix_length is not None: field_updates['prefix_length' ] = (ipv4_prefix_length, True) update_config_rule_custom(config_rules, endpoint_settings_key, field_updates) try: @@ -131,7 +134,7 @@ def process_site_network_access( raise NotImplementedError(MSG.format(str(ipv4_allocation['address-allocation-type']))) ipv4_allocation_addresses = ipv4_allocation['addresses'] ipv4_provider_address = ipv4_allocation_addresses['provider-address'] - #ipv4_customer_address = ipv4_allocation_addresses['customer-address'] + ipv4_customer_address = ipv4_allocation_addresses['customer-address'] ipv4_prefix_length = ipv4_allocation_addresses['prefix-length' ] vlan_tag = None @@ -176,7 +179,8 @@ def process_site_network_access( availability = qos_profile_class['bandwidth']['guaranteed-bw-percent'] exc = update_service_endpoint( - service_uuid, site_id, device_uuid, endpoint_uuid, vlan_tag, ipv4_provider_address, ipv4_prefix_length, + service_uuid, site_id, device_uuid, endpoint_uuid, + vlan_tag, ipv4_customer_address, ipv4_provider_address, ipv4_prefix_length, capacity_gbps=service_bandwidth_gbps, e2e_latency_ms=max_e2e_latency_ms, availability=availability, mtu=service_mtu, static_routing=site_static_routing ) diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/yang/ietf_l3vpn_tree.txt b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/yang/ietf_l3vpn_tree.txt new file mode 100644 index 0000000000000000000000000000000000000000..e811c7c1b1a25214926341dc0205ee9f1b150c63 --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_l3vpn/yang/ietf_l3vpn_tree.txt @@ -0,0 +1,413 @@ +module: ietf-l3vpn-svc + +--rw l3vpn-svc + +--rw vpn-profiles + | +--rw valid-provider-identifiers + | +--rw cloud-identifier* [id] {cloud-access}? + | | +--rw id string + | +--rw encryption-profile-identifier* [id] + | | +--rw id string + | +--rw qos-profile-identifier* [id] + | | +--rw id string + | +--rw bfd-profile-identifier* [id] + | +--rw id string + +--rw vpn-services + | +--rw vpn-service* [vpn-id] + | +--rw vpn-id svc-id + | +--rw customer-name? string + | +--rw vpn-service-topology? identityref + | +--rw cloud-accesses {cloud-access}? + | | +--rw cloud-access* [cloud-identifier] + | | +--rw cloud-identifier -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/cloud-identifier/id + | | +--rw (list-flavor)? + | | | +--:(permit-any) + | | | | +--rw permit-any? empty + | | | +--:(deny-any-except) + | | | | +--rw permit-site* -> /l3vpn-svc/sites/site/site-id + | | | +--:(permit-any-except) + | | | +--rw deny-site* -> /l3vpn-svc/sites/site/site-id + | | +--rw address-translation + | | +--rw nat44 + | | +--rw enabled? boolean + | | +--rw nat44-customer-address? inet:ipv4-address + | +--rw multicast {multicast}? + | | +--rw enabled? boolean + | | +--rw customer-tree-flavors + | | | +--rw tree-flavor* identityref + | | +--rw rp + | | +--rw rp-group-mappings + | | | +--rw rp-group-mapping* [id] + | | | +--rw id uint16 + | | | +--rw provider-managed + | | | | +--rw enabled? boolean + | | | | +--rw rp-redundancy? boolean + | | | | +--rw optimal-traffic-delivery? boolean + | | | +--rw rp-address inet:ip-address + | | | +--rw groups + | | | +--rw group* [id] + | | | +--rw id uint16 + | | | +--rw (group-format) + | | | +--:(singleaddress) + | | | | +--rw group-address? inet:ip-address + | | | +--:(startend) + | | | +--rw group-start? inet:ip-address + | | | +--rw group-end? inet:ip-address + | | +--rw rp-discovery + | | +--rw rp-discovery-type? identityref + | | +--rw bsr-candidates + | | +--rw bsr-candidate-address* inet:ip-address + | +--rw carrierscarrier? boolean {carrierscarrier}? + | +--rw extranet-vpns {extranet-vpn}? + | +--rw extranet-vpn* [vpn-id] + | +--rw vpn-id svc-id + | +--rw local-sites-role? identityref + +--rw sites + +--rw site* [site-id] + +--rw site-id svc-id + +--rw requested-site-start? yang:date-and-time + +--rw requested-site-stop? yang:date-and-time + +--rw locations + | +--rw location* [location-id] + | +--rw location-id svc-id + | +--rw address? string + | +--rw postal-code? string + | +--rw state? string + | +--rw city? string + | +--rw country-code? string + +--rw devices + | +--rw device* [device-id] + | +--rw device-id svc-id + | +--rw location -> ../../../locations/location/location-id + | +--rw management + | +--rw address-family? address-family + | +--rw address inet:ip-address + +--rw site-diversity {site-diversity}? + | +--rw groups + | +--rw group* [group-id] + | +--rw group-id string + +--rw management + | +--rw type identityref + +--rw vpn-policies + | +--rw vpn-policy* [vpn-policy-id] + | +--rw vpn-policy-id svc-id + | +--rw entries* [id] + | +--rw id svc-id + | +--rw filters + | | +--rw filter* [type] + | | +--rw type identityref + | | +--rw lan-tag* string {lan-tag}? + | | +--rw ipv4-lan-prefix* inet:ipv4-prefix {ipv4}? + | | +--rw ipv6-lan-prefix* inet:ipv6-prefix {ipv6}? + | +--rw vpn* [vpn-id] + | +--rw vpn-id -> /l3vpn-svc/vpn-services/vpn-service/vpn-id + | +--rw site-role? identityref + +--rw site-vpn-flavor? identityref + +--rw maximum-routes + | +--rw address-family* [af] + | +--rw af address-family + | +--rw maximum-routes? uint32 + +--rw security + | +--rw authentication + | +--rw encryption {encryption}? + | +--rw enabled? boolean + | +--rw layer? enumeration + | +--rw encryption-profile + | +--rw (profile)? + | +--:(provider-profile) + | | +--rw profile-name? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/encryption-profile-identifier/id + | +--:(customer-profile) + | +--rw algorithm? string + | +--rw (key-type)? + | +--:(psk) + | +--rw preshared-key? string + +--rw service + | +--rw qos {qos}? + | | +--rw qos-classification-policy + | | | +--rw rule* [id] + | | | +--rw id string + | | | +--rw (match-type)? + | | | | +--:(match-flow) + | | | | | +--rw match-flow + | | | | | +--rw dscp? inet:dscp + | | | | | +--rw dot1p? uint8 + | | | | | +--rw ipv4-src-prefix? inet:ipv4-prefix + | | | | | +--rw ipv6-src-prefix? inet:ipv6-prefix + | | | | | +--rw ipv4-dst-prefix? inet:ipv4-prefix + | | | | | +--rw ipv6-dst-prefix? inet:ipv6-prefix + | | | | | +--rw l4-src-port? inet:port-number + | | | | | +--rw target-sites* svc-id {target-sites}? + | | | | | +--rw l4-src-port-range + | | | | | | +--rw lower-port? inet:port-number + | | | | | | +--rw upper-port? inet:port-number + | | | | | +--rw l4-dst-port? inet:port-number + | | | | | +--rw l4-dst-port-range + | | | | | | +--rw lower-port? inet:port-number + | | | | | | +--rw upper-port? inet:port-number + | | | | | +--rw protocol-field? union + | | | | +--:(match-application) + | | | | +--rw match-application? identityref + | | | +--rw target-class-id? string + | | +--rw qos-profile + | | +--rw (qos-profile)? + | | +--:(standard) + | | | +--rw profile? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/qos-profile-identifier/id + | | +--:(custom) + | | +--rw classes {qos-custom}? + | | +--rw class* [class-id] + | | +--rw class-id string + | | +--rw direction? identityref + | | +--rw rate-limit? decimal64 + | | +--rw latency + | | | +--rw (flavor)? + | | | +--:(lowest) + | | | | +--rw use-lowest-latency? empty + | | | +--:(boundary) + | | | +--rw latency-boundary? uint16 + | | +--rw jitter + | | | +--rw (flavor)? + | | | +--:(lowest) + | | | | +--rw use-lowest-jitter? empty + | | | +--:(boundary) + | | | +--rw latency-boundary? uint32 + | | +--rw bandwidth + | | +--rw guaranteed-bw-percent decimal64 + | | +--rw end-to-end? empty + | +--rw carrierscarrier {carrierscarrier}? + | | +--rw signalling-type? enumeration + | +--rw multicast {multicast}? + | +--rw multicast-site-type? enumeration + | +--rw multicast-address-family + | | +--rw ipv4? boolean {ipv4}? + | | +--rw ipv6? boolean {ipv6}? + | +--rw protocol-type? enumeration + +--rw traffic-protection {fast-reroute}? + | +--rw enabled? boolean + +--rw routing-protocols + | +--rw routing-protocol* [type] + | +--rw type identityref + | +--rw ospf {rtg-ospf}? + | | +--rw address-family* address-family + | | +--rw area-address yang:dotted-quad + | | +--rw metric? uint16 + | | +--rw sham-links {rtg-ospf-sham-link}? + | | +--rw sham-link* [target-site] + | | +--rw target-site svc-id + | | +--rw metric? uint16 + | +--rw bgp {rtg-bgp}? + | | +--rw autonomous-system uint32 + | | +--rw address-family* address-family + | +--rw static + | | +--rw cascaded-lan-prefixes + | | +--rw ipv4-lan-prefixes* [lan next-hop] {ipv4}? + | | | +--rw lan inet:ipv4-prefix + | | | +--rw next-hop inet:ipv4-address + | | | +--rw lan-tag? string + | | +--rw ipv6-lan-prefixes* [lan next-hop] {ipv6}? + | | +--rw lan inet:ipv6-prefix + | | +--rw next-hop inet:ipv6-address + | | +--rw lan-tag? string + | +--rw rip {rtg-rip}? + | | +--rw address-family* address-family + | +--rw vrrp {rtg-vrrp}? + | +--rw address-family* address-family + +--ro actual-site-start? yang:date-and-time + +--ro actual-site-stop? yang:date-and-time + +--rw site-network-accesses + +--rw site-network-access* [site-network-access-id] + +--rw site-network-access-id svc-id + +--rw site-network-access-type? identityref + +--rw (location-flavor) + | +--:(location) + | | +--rw location-reference? -> ../../../locations/location/location-id + | +--:(device) + | +--rw device-reference? -> ../../../devices/device/device-id + +--rw access-diversity {site-diversity}? + | +--rw groups + | | +--rw group* [group-id] + | | +--rw group-id string + | +--rw constraints + | +--rw constraint* [constraint-type] + | +--rw constraint-type identityref + | +--rw target + | +--rw (target-flavor)? + | +--:(id) + | | +--rw group* [group-id] + | | +--rw group-id string + | +--:(all-accesses) + | | +--rw all-other-accesses? empty + | +--:(all-groups) + | +--rw all-other-groups? empty + +--rw bearer + | +--rw requested-type {requested-type}? + | | +--rw requested-type? string + | | +--rw strict? boolean + | +--rw always-on? boolean {always-on}? + | +--rw bearer-reference? string {bearer-reference}? + +--rw ip-connection + | +--rw ipv4 {ipv4}? + | | +--rw address-allocation-type? identityref + | | +--rw provider-dhcp + | | | +--rw provider-address? inet:ipv4-address + | | | +--rw prefix-length? uint8 + | | | +--rw (address-assign)? + | | | +--:(number) + | | | | +--rw number-of-dynamic-address? uint16 + | | | +--:(explicit) + | | | +--rw customer-addresses + | | | +--rw address-group* [group-id] + | | | +--rw group-id string + | | | +--rw start-address? inet:ipv4-address + | | | +--rw end-address? inet:ipv4-address + | | +--rw dhcp-relay + | | | +--rw provider-address? inet:ipv4-address + | | | +--rw prefix-length? uint8 + | | | +--rw customer-dhcp-servers + | | | +--rw server-ip-address* inet:ipv4-address + | | +--rw addresses + | | +--rw provider-address? inet:ipv4-address + | | +--rw customer-address? inet:ipv4-address + | | +--rw prefix-length? uint8 + | +--rw ipv6 {ipv6}? + | | +--rw address-allocation-type? identityref + | | +--rw provider-dhcp + | | | +--rw provider-address? inet:ipv6-address + | | | +--rw prefix-length? uint8 + | | | +--rw (address-assign)? + | | | +--:(number) + | | | | +--rw number-of-dynamic-address? uint16 + | | | +--:(explicit) + | | | +--rw customer-addresses + | | | +--rw address-group* [group-id] + | | | +--rw group-id string + | | | +--rw start-address? inet:ipv6-address + | | | +--rw end-address? inet:ipv6-address + | | +--rw dhcp-relay + | | | +--rw provider-address? inet:ipv6-address + | | | +--rw prefix-length? uint8 + | | | +--rw customer-dhcp-servers + | | | +--rw server-ip-address* inet:ipv6-address + | | +--rw addresses + | | +--rw provider-address? inet:ipv6-address + | | +--rw customer-address? inet:ipv6-address + | | +--rw prefix-length? uint8 + | +--rw oam + | +--rw bfd {bfd}? + | +--rw enabled? boolean + | +--rw (holdtime)? + | +--:(fixed) + | | +--rw fixed-value? uint32 + | +--:(profile) + | +--rw profile-name? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/bfd-profile-identifier/id + +--rw security + | +--rw authentication + | +--rw encryption {encryption}? + | +--rw enabled? boolean + | +--rw layer? enumeration + | +--rw encryption-profile + | +--rw (profile)? + | +--:(provider-profile) + | | +--rw profile-name? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/encryption-profile-identifier/id + | +--:(customer-profile) + | +--rw algorithm? string + | +--rw (key-type)? + | +--:(psk) + | +--rw preshared-key? string + +--rw service + | +--rw svc-input-bandwidth uint64 + | +--rw svc-output-bandwidth uint64 + | +--rw svc-mtu uint16 + | +--rw qos {qos}? + | | +--rw qos-classification-policy + | | | +--rw rule* [id] + | | | +--rw id string + | | | +--rw (match-type)? + | | | | +--:(match-flow) + | | | | | +--rw match-flow + | | | | | +--rw dscp? inet:dscp + | | | | | +--rw dot1p? uint8 + | | | | | +--rw ipv4-src-prefix? inet:ipv4-prefix + | | | | | +--rw ipv6-src-prefix? inet:ipv6-prefix + | | | | | +--rw ipv4-dst-prefix? inet:ipv4-prefix + | | | | | +--rw ipv6-dst-prefix? inet:ipv6-prefix + | | | | | +--rw l4-src-port? inet:port-number + | | | | | +--rw target-sites* svc-id {target-sites}? + | | | | | +--rw l4-src-port-range + | | | | | | +--rw lower-port? inet:port-number + | | | | | | +--rw upper-port? inet:port-number + | | | | | +--rw l4-dst-port? inet:port-number + | | | | | +--rw l4-dst-port-range + | | | | | | +--rw lower-port? inet:port-number + | | | | | | +--rw upper-port? inet:port-number + | | | | | +--rw protocol-field? union + | | | | +--:(match-application) + | | | | +--rw match-application? identityref + | | | +--rw target-class-id? string + | | +--rw qos-profile + | | +--rw (qos-profile)? + | | +--:(standard) + | | | +--rw profile? -> /l3vpn-svc/vpn-profiles/valid-provider-identifiers/qos-profile-identifier/id + | | +--:(custom) + | | +--rw classes {qos-custom}? + | | +--rw class* [class-id] + | | +--rw class-id string + | | +--rw direction? identityref + | | +--rw rate-limit? decimal64 + | | +--rw latency + | | | +--rw (flavor)? + | | | +--:(lowest) + | | | | +--rw use-lowest-latency? empty + | | | +--:(boundary) + | | | +--rw latency-boundary? uint16 + | | +--rw jitter + | | | +--rw (flavor)? + | | | +--:(lowest) + | | | | +--rw use-lowest-jitter? empty + | | | +--:(boundary) + | | | +--rw latency-boundary? uint32 + | | +--rw bandwidth + | | +--rw guaranteed-bw-percent decimal64 + | | +--rw end-to-end? empty + | +--rw carrierscarrier {carrierscarrier}? + | | +--rw signalling-type? enumeration + | +--rw multicast {multicast}? + | +--rw multicast-site-type? enumeration + | +--rw multicast-address-family + | | +--rw ipv4? boolean {ipv4}? + | | +--rw ipv6? boolean {ipv6}? + | +--rw protocol-type? enumeration + +--rw routing-protocols + | +--rw routing-protocol* [type] + | +--rw type identityref + | +--rw ospf {rtg-ospf}? + | | +--rw address-family* address-family + | | +--rw area-address yang:dotted-quad + | | +--rw metric? uint16 + | | +--rw sham-links {rtg-ospf-sham-link}? + | | +--rw sham-link* [target-site] + | | +--rw target-site svc-id + | | +--rw metric? uint16 + | +--rw bgp {rtg-bgp}? + | | +--rw autonomous-system uint32 + | | +--rw address-family* address-family + | +--rw static + | | +--rw cascaded-lan-prefixes + | | +--rw ipv4-lan-prefixes* [lan next-hop] {ipv4}? + | | | +--rw lan inet:ipv4-prefix + | | | +--rw next-hop inet:ipv4-address + | | | +--rw lan-tag? string + | | +--rw ipv6-lan-prefixes* [lan next-hop] {ipv6}? + | | +--rw lan inet:ipv6-prefix + | | +--rw next-hop inet:ipv6-address + | | +--rw lan-tag? string + | +--rw rip {rtg-rip}? + | | +--rw address-family* address-family + | +--rw vrrp {rtg-vrrp}? + | +--rw address-family* address-family + +--rw availability + | +--rw access-priority? uint32 + +--rw vpn-attachment + +--rw (attachment-flavor) + +--:(vpn-policy-id) + | +--rw vpn-policy-id? -> ../../../../vpn-policies/vpn-policy/vpn-policy-id + +--:(vpn-id) + +--rw vpn-id? -> /l3vpn-svc/vpn-services/vpn-service/vpn-id + +--rw site-role? identityref diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_network/ComposeNetwork.py b/src/nbi/service/rest_server/nbi_plugins/ietf_network/ComposeNetwork.py index 6ffc85e387ce4f4691cdc9757d6bd60068bef991..2d3ef29fc1773b8e0c2f762e0978742797e8cce5 100644 --- a/src/nbi/service/rest_server/nbi_plugins/ietf_network/ComposeNetwork.py +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_network/ComposeNetwork.py @@ -28,9 +28,11 @@ IGNORE_DEVICE_TYPES = { DeviceTypeEnum.DATACENTER.value, DeviceTypeEnum.EMULATED_CLIENT.value, DeviceTypeEnum.EMULATED_DATACENTER.value, + DeviceTypeEnum.EMULATED_IP_SDN_CONTROLLER, DeviceTypeEnum.EMULATED_MICROWAVE_RADIO_SYSTEM.value, DeviceTypeEnum.EMULATED_OPEN_LINE_SYSTEM.value, DeviceTypeEnum.EMULATED_XR_CONSTELLATION.value, + DeviceTypeEnum.IP_SDN_CONTROLLER, DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM.value, DeviceTypeEnum.NETWORK.value, DeviceTypeEnum.OPEN_LINE_SYSTEM.value, @@ -39,10 +41,10 @@ IGNORE_DEVICE_TYPES = { IGNORE_DEVICE_NAMES = { NetworkTypeEnum.TE_OTN_TOPOLOGY: { - '128.32.10.1', '128.32.33.5', '128.32.20.5', '128.32.20.1', '128.32.10.5', 'nce-t' + 'nce-t', '128.32.10.1', '128.32.33.5', '128.32.20.5', '128.32.20.1', '128.32.10.5', }, NetworkTypeEnum.TE_ETH_TRAN_TOPOLOGY: { - + 'nce-t', }, } diff --git a/src/nbi/tests/data/ietf_l3vpn_req_svc1.json b/src/nbi/tests/data/ietf_l3vpn_req_svc1.json index 66e253cb5b99d3b758bba04e1dfa8799e1b13c08..bfeb93fb74c9513ef4d175d5962110127303a2a7 100644 --- a/src/nbi/tests/data/ietf_l3vpn_req_svc1.json +++ b/src/nbi/tests/data/ietf_l3vpn_req_svc1.json @@ -39,12 +39,12 @@ { "lan": "128.32.10.1/24", "lan-tag": "vlan21", - "next-hop": "128.32.33.5" + "next-hop": "128.32.33.2" }, { "lan": "128.32.20.1/24", "lan-tag": "vlan21", - "next-hop": "128.32.33.5" + "next-hop": "128.32.33.2" } ] } @@ -82,7 +82,7 @@ { "lan": "172.1.101.1/24", "lan-tag": "vlan21", - "next-hop": "10.0.10.1" + "next-hop": "128.32.33.254" } ] } @@ -147,7 +147,7 @@ { "lan": "172.1.101.1/24", "lan-tag": "vlan101", - "next-hop": "172.10.33.5" + "next-hop": "172.10.33.2" } ] } @@ -185,12 +185,12 @@ { "lan": "128.32.10.1/24", "lan-tag": "vlan101", - "next-hop": "10.0.30.1" + "next-hop": "172.10.33.254" }, { "lan": "128.32.20.1/24", "lan-tag": "vlan101", - "next-hop": "10.0.30.1" + "next-hop": "172.10.33.254" } ] } diff --git a/src/nbi/tests/data/ietf_l3vpn_req_svc2.json b/src/nbi/tests/data/ietf_l3vpn_req_svc2.json index 2d2ea2c22e2bb490027b8033bb4fb94a39b35049..2cc512e595c820a8df42ec06af973fefa4601095 100644 --- a/src/nbi/tests/data/ietf_l3vpn_req_svc2.json +++ b/src/nbi/tests/data/ietf_l3vpn_req_svc2.json @@ -39,12 +39,12 @@ { "lan": "128.32.10.1/24", "lan-tag": "vlan31", - "next-hop": "128.32.33.5" + "next-hop": "128.32.33.2" }, { "lan": "128.32.20.1/24", "lan-tag": "vlan31", - "next-hop": "128.32.33.5" + "next-hop": "128.32.33.2" } ] } @@ -80,9 +80,9 @@ "cascaded-lan-prefixes": { "ipv4-lan-prefixes": [ { - "lan": "172.1.101.1/24", + "lan": "172.1.201.1/24", "lan-tag": "vlan31", - "next-hop": "10.0.10.1" + "next-hop": "128.32.33.254" } ] } @@ -145,9 +145,9 @@ "cascaded-lan-prefixes": { "ipv4-lan-prefixes": [ { - "lan": "172.1.101.1/24", + "lan": "172.1.201.1/24", "lan-tag": "vlan201", - "next-hop": "172.10.33.1" + "next-hop": "172.10.33.2" } ] } @@ -185,12 +185,12 @@ { "lan": "128.32.10.1/24", "lan-tag": "vlan201", - "next-hop": "10.0.30.1" + "next-hop": "172.10.33.254" }, { "lan": "128.32.20.1/24", "lan-tag": "vlan201", - "next-hop": "10.0.30.1" + "next-hop": "172.10.33.254" } ] } diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py index 0a1b62040a81ee964d373132763e964381cbc19e..ca978310842d538efe2f83ed743446067f84c3eb 100644 --- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py +++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py @@ -15,12 +15,16 @@ import json, logging, requests, uuid from typing import Dict, List, Optional, Tuple, Union from common.proto.context_pb2 import ( - Connection, Device, DeviceList, EndPointId, Link, LinkList, Service, ServiceStatusEnum, ServiceTypeEnum) + ConfigRule, Connection, Device, DeviceList, EndPointId, Link, LinkList, Service, ServiceStatusEnum, ServiceTypeEnum +) from common.proto.pathcomp_pb2 import PathCompReply, PathCompRequest +from common.tools.grpc.Tools import grpc_message_list_to_json from pathcomp.frontend.Config import BACKEND_URL from .tools.EroPathToHops import eropath_to_hops from .tools.ComposeConfigRules import ( - compose_device_config_rules, compose_l2nm_config_rules, compose_l3nm_config_rules, compose_tapi_config_rules) + compose_device_config_rules, compose_l2nm_config_rules, compose_l3nm_config_rules, compose_tapi_config_rules, + generate_neighbor_endpoint_config_rules +) from .tools.ComposeRequest import compose_device, compose_link, compose_service from .tools.ComputeSubServices import ( convert_explicit_path_hops_to_connections, convert_explicit_path_hops_to_plain_connection) @@ -227,12 +231,25 @@ class _Algorithm: continue orig_config_rules = grpc_orig_service.service_config.config_rules + json_orig_config_rules = grpc_message_list_to_json(orig_config_rules) for service_path_ero in response['path']: self.logger.debug('service_path_ero["devices"] = {:s}'.format(str(service_path_ero['devices']))) _endpoint_to_link_dict = {k:v[0] for k,v in self.endpoint_to_link_dict.items()} self.logger.debug('self.endpoint_to_link_dict = {:s}'.format(str(_endpoint_to_link_dict))) path_hops = eropath_to_hops(service_path_ero['devices'], self.endpoint_to_link_dict) + + json_generated_config_rules = generate_neighbor_endpoint_config_rules( + json_orig_config_rules, path_hops, self.device_name_mapping, self.endpoint_name_mapping + ) + json_extended_config_rules = list() + json_extended_config_rules.extend(json_orig_config_rules) + json_extended_config_rules.extend(json_generated_config_rules) + extended_config_rules = [ + ConfigRule(**json_extended_config_rule) + for json_extended_config_rule in json_extended_config_rules + ] + self.logger.debug('path_hops = {:s}'.format(str(path_hops))) try: _device_dict = {k:v[0] for k,v in self.device_dict.items()} @@ -256,7 +273,7 @@ class _Algorithm: if service_key in grpc_services: continue grpc_service = self.add_service_to_reply( reply, context_uuid, service_uuid, service_type, path_hops=path_hops, - config_rules=orig_config_rules) + config_rules=extended_config_rules) grpc_services[service_key] = grpc_service for connection in connections: diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py index 329552a914e478f0e927bd6f04fce6725bef0b5e..2d4ff4fd59187e3581c8426435f80bca958ad655 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import itertools, json, logging, re -from typing import Dict, List, Optional, Tuple +import copy, itertools, json, logging, re +from typing import Dict, Iterable, List, Optional, Set, Tuple from common.proto.context_pb2 import ConfigRule from common.tools.grpc.Tools import grpc_message_to_json_string from common.tools.object_factory.ConfigRule import json_config_rule_set @@ -21,19 +21,26 @@ from common.tools.object_factory.ConfigRule import json_config_rule_set LOGGER = logging.getLogger(__name__) SETTINGS_RULE_NAME = '/settings' +STATIC_ROUTING_RULE_NAME = '/static_routing' -DEVICE_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/settings') -ENDPOINT_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/settings') +RE_UUID = re.compile(r'([0-9a-fA-F]{8})\-([0-9a-fA-F]{4})\-([0-9a-fA-F]{4})\-([0-9a-fA-F]{4})\-([0-9a-fA-F]{12})') + +RE_DEVICE_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/settings') +RE_ENDPOINT_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/settings') +RE_ENDPOINT_VLAN_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/vlan\[([^\]]+)\]\/settings') + +TMPL_ENDPOINT_SETTINGS = '/device[{:s}]/endpoint[{:s}]/settings' +TMPL_ENDPOINT_VLAN_SETTINGS = '/device[{:s}]/endpoint[{:s}]/vlan[{:s}]/settings' L2NM_SETTINGS_FIELD_DEFAULTS = { - 'encapsulation_type': 'dot1q', - 'vlan_id' : 100, + #'encapsulation_type': 'dot1q', + #'vlan_id' : 100, 'mtu' : 1450, } L3NM_SETTINGS_FIELD_DEFAULTS = { - 'encapsulation_type': 'dot1q', - 'vlan_id' : 100, + #'encapsulation_type': 'dot1q', + #'vlan_id' : 100, 'mtu' : 1450, } @@ -54,26 +61,48 @@ def find_custom_config_rule(config_rules : List, resource_name : str) -> Optiona return resource_value def compose_config_rules( - main_service_config_rules : List, subservice_config_rules : List, field_defaults : Dict + main_service_config_rules : List, subservice_config_rules : List, settings_rule_name : str, field_defaults : Dict ) -> None: - settings = find_custom_config_rule(main_service_config_rules, SETTINGS_RULE_NAME) + settings = find_custom_config_rule(main_service_config_rules, settings_rule_name) if settings is None: return json_settings = {} - for field_name,default_value in field_defaults.items(): - json_settings[field_name] = settings.get(field_name, default_value) - config_rule = ConfigRule(**json_config_rule_set('/settings', json_settings)) + if len(field_defaults) == 0: + for field_name,field_value in settings.items(): + json_settings[field_name] = field_value + else: + for field_name,default_value in field_defaults.items(): + field_value = settings.get(field_name, default_value) + if field_value is None: continue + json_settings[field_name] = field_value + + if len(json_settings) == 0: return + + config_rule = ConfigRule(**json_config_rule_set(settings_rule_name, json_settings)) subservice_config_rules.append(config_rule) def compose_l2nm_config_rules(main_service_config_rules : List, subservice_config_rules : List) -> None: - compose_config_rules(main_service_config_rules, subservice_config_rules, L2NM_SETTINGS_FIELD_DEFAULTS) + CONFIG_RULES = [ + (SETTINGS_RULE_NAME, L2NM_SETTINGS_FIELD_DEFAULTS), + ] + for rule_name, defaults in CONFIG_RULES: + compose_config_rules(main_service_config_rules, subservice_config_rules, rule_name, defaults) def compose_l3nm_config_rules(main_service_config_rules : List, subservice_config_rules : List) -> None: - compose_config_rules(main_service_config_rules, subservice_config_rules, L3NM_SETTINGS_FIELD_DEFAULTS) + CONFIG_RULES = [ + (SETTINGS_RULE_NAME, L3NM_SETTINGS_FIELD_DEFAULTS), + (STATIC_ROUTING_RULE_NAME, {}), + ] + for rule_name, defaults in CONFIG_RULES: + compose_config_rules(main_service_config_rules, subservice_config_rules, rule_name, defaults) def compose_tapi_config_rules(main_service_config_rules : List, subservice_config_rules : List) -> None: - compose_config_rules(main_service_config_rules, subservice_config_rules, TAPI_SETTINGS_FIELD_DEFAULTS) + CONFIG_RULES = [ + (SETTINGS_RULE_NAME, TAPI_SETTINGS_FIELD_DEFAULTS), + ] + for rule_name, defaults in CONFIG_RULES: + compose_config_rules(main_service_config_rules, subservice_config_rules, rule_name, defaults) def compose_device_config_rules( config_rules : List, subservice_config_rules : List, path_hops : List, @@ -127,25 +156,31 @@ def compose_device_config_rules( elif config_rule.WhichOneof('config_rule') == 'custom': LOGGER.debug('[compose_device_config_rules] is custom') - match = DEVICE_SETTINGS.match(config_rule.custom.resource_key) + match = RE_DEVICE_SETTINGS.match(config_rule.custom.resource_key) if match is not None: device_uuid_or_name = match.group(1) - device_name_or_uuid = device_name_mapping[device_uuid_or_name] - device_keys = {device_uuid_or_name, device_name_or_uuid} + device_keys = {device_uuid_or_name} + device_name_or_uuid = device_name_mapping.get(device_uuid_or_name) + if device_name_or_uuid is not None: device_keys.add(device_name_or_uuid) if len(device_keys.intersection(devices_traversed)) == 0: continue subservice_config_rules.append(config_rule) - match = ENDPOINT_SETTINGS.match(config_rule.custom.resource_key) + match = RE_ENDPOINT_SETTINGS.match(config_rule.custom.resource_key) + if match is None: + match = RE_ENDPOINT_VLAN_SETTINGS.match(config_rule.custom.resource_key) if match is not None: device_uuid_or_name = match.group(1) - device_name_or_uuid = device_name_mapping[device_uuid_or_name] - device_keys = {device_uuid_or_name, device_name_or_uuid} + device_keys = {device_uuid_or_name} + device_name_or_uuid = device_name_mapping.get(device_uuid_or_name) + if device_name_or_uuid is not None: device_keys.add(device_name_or_uuid) endpoint_uuid_or_name = match.group(2) - endpoint_name_or_uuid_1 = endpoint_name_mapping[(device_uuid_or_name, endpoint_uuid_or_name)] - endpoint_name_or_uuid_2 = endpoint_name_mapping[(device_name_or_uuid, endpoint_uuid_or_name)] - endpoint_keys = {endpoint_uuid_or_name, endpoint_name_or_uuid_1, endpoint_name_or_uuid_2} + endpoint_keys = {endpoint_uuid_or_name} + endpoint_name_or_uuid_1 = endpoint_name_mapping.get((device_uuid_or_name, endpoint_uuid_or_name)) + if endpoint_name_or_uuid_1 is not None: endpoint_keys.add(endpoint_name_or_uuid_1) + endpoint_name_or_uuid_2 = endpoint_name_mapping.get((device_name_or_uuid, endpoint_uuid_or_name)) + if endpoint_name_or_uuid_2 is not None: endpoint_keys.add(endpoint_name_or_uuid_2) device_endpoint_keys = set(itertools.product(device_keys, endpoint_keys)) if len(device_endpoint_keys.intersection(endpoints_traversed)) == 0: continue @@ -153,4 +188,146 @@ def compose_device_config_rules( else: continue + for config_rule in subservice_config_rules: + LOGGER.debug('[compose_device_config_rules] result config_rule: {:s}'.format( + grpc_message_to_json_string(config_rule))) + LOGGER.debug('[compose_device_config_rules] end') + +def pairwise(iterable : Iterable) -> Tuple[Iterable, Iterable]: + # TODO: To be replaced by itertools.pairwise() when we move to Python 3.10 + # Python 3.10 introduced method itertools.pairwise() + # Standalone method extracted from: + # - https://docs.python.org/3/library/itertools.html#itertools.pairwise + a, b = itertools.tee(iterable, 2) + next(b, None) + return zip(a, b) + +def compute_device_keys( + device_uuid_or_name : str, device_name_mapping : Dict[str, str] +) -> Set[str]: + LOGGER.debug('[compute_device_keys] begin') + LOGGER.debug('[compute_device_keys] device_uuid_or_name={:s}'.format(str(device_uuid_or_name))) + #LOGGER.debug('[compute_device_keys] device_name_mapping={:s}'.format(str(device_name_mapping))) + + device_keys = {device_uuid_or_name} + for k,v in device_name_mapping.items(): + if device_uuid_or_name not in {k, v}: continue + device_keys.add(k) + device_keys.add(v) + + LOGGER.debug('[compute_device_keys] device_keys={:s}'.format(str(device_keys))) + LOGGER.debug('[compute_device_keys] end') + return device_keys + +def compute_endpoint_keys( + device_keys : Set[str], endpoint_uuid_or_name : str, endpoint_name_mapping : Dict[str, str] +) -> Set[str]: + LOGGER.debug('[compute_endpoint_keys] begin') + LOGGER.debug('[compute_endpoint_keys] device_keys={:s}'.format(str(device_keys))) + LOGGER.debug('[compute_endpoint_keys] endpoint_uuid_or_name={:s}'.format(str(endpoint_uuid_or_name))) + #LOGGER.debug('[compute_device_endpoint_keys] endpoint_name_mapping={:s}'.format(str(endpoint_name_mapping))) + + endpoint_keys = {endpoint_uuid_or_name} + for k,v in endpoint_name_mapping.items(): + if (k[0] in device_keys or v in device_keys) and (endpoint_uuid_or_name in {k[1], v}): + endpoint_keys.add(k[1]) + endpoint_keys.add(v) + + LOGGER.debug('[compute_endpoint_keys] endpoint_keys={:s}'.format(str(endpoint_keys))) + LOGGER.debug('[compute_endpoint_keys] end') + return endpoint_keys + +def compute_device_endpoint_keys( + device_uuid_or_name : str, endpoint_uuid_or_name : str, + device_name_mapping : Dict[str, str], endpoint_name_mapping : Dict[Tuple[str, str], str] +) -> Set[Tuple[str, str]]: + LOGGER.debug('[compute_device_endpoint_keys] begin') + LOGGER.debug('[compute_device_endpoint_keys] device_uuid_or_name={:s}'.format(str(device_uuid_or_name))) + LOGGER.debug('[compute_device_endpoint_keys] endpoint_uuid_or_name={:s}'.format(str(endpoint_uuid_or_name))) + #LOGGER.debug('[compute_device_endpoint_keys] device_name_mapping={:s}'.format(str(device_name_mapping))) + #LOGGER.debug('[compute_device_endpoint_keys] endpoint_name_mapping={:s}'.format(str(endpoint_name_mapping))) + + device_keys = compute_device_keys(device_uuid_or_name, device_name_mapping) + endpoint_keys = compute_endpoint_keys(device_keys, endpoint_uuid_or_name, endpoint_name_mapping) + device_endpoint_keys = set(itertools.product(device_keys, endpoint_keys)) + + LOGGER.debug('[compute_device_endpoint_keys] device_endpoint_keys={:s}'.format(str(device_endpoint_keys))) + LOGGER.debug('[compute_device_endpoint_keys] end') + return device_endpoint_keys + +def generate_neighbor_endpoint_config_rules( + config_rules : List[Dict], path_hops : List[Dict], + device_name_mapping : Dict[str, str], endpoint_name_mapping : Dict[Tuple[str, str], str] +) -> List[Dict]: + LOGGER.debug('[generate_neighbor_endpoint_config_rules] begin') + LOGGER.debug('[generate_neighbor_endpoint_config_rules] config_rules={:s}'.format(str(config_rules))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] path_hops={:s}'.format(str(path_hops))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] device_name_mapping={:s}'.format(str(device_name_mapping))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] endpoint_name_mapping={:s}'.format(str(endpoint_name_mapping))) + + generated_config_rules = list() + for link_endpoint_a, link_endpoint_b in pairwise(path_hops): + LOGGER.debug('[generate_neighbor_endpoint_config_rules] loop begin') + LOGGER.debug('[generate_neighbor_endpoint_config_rules] link_endpoint_a={:s}'.format(str(link_endpoint_a))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] link_endpoint_b={:s}'.format(str(link_endpoint_b))) + + device_endpoint_keys_a = compute_device_endpoint_keys( + link_endpoint_a['device'], link_endpoint_a['egress_ep'], + device_name_mapping, endpoint_name_mapping + ) + + device_endpoint_keys_b = compute_device_endpoint_keys( + link_endpoint_b['device'], link_endpoint_b['ingress_ep'], + device_name_mapping, endpoint_name_mapping + ) + + for config_rule in config_rules: + # Only applicable, by now, to Custom Config Rules for endpoint settings + if 'custom' not in config_rule: continue + match = RE_ENDPOINT_SETTINGS.match(config_rule['custom']['resource_key']) + if match is None: + match = RE_ENDPOINT_VLAN_SETTINGS.match(config_rule['custom']['resource_key']) + if match is None: continue + + resource_key_values = match.groups() + if resource_key_values[0:2] in device_endpoint_keys_a: + resource_key_values = list(resource_key_values) + resource_key_values[0] = link_endpoint_b['device'] + resource_key_values[1] = link_endpoint_b['ingress_ep'] + elif resource_key_values[0:2] in device_endpoint_keys_b: + resource_key_values = list(resource_key_values) + resource_key_values[0] = link_endpoint_a['device'] + resource_key_values[1] = link_endpoint_a['egress_ep'] + else: + continue + + device_keys = compute_device_keys(resource_key_values[0], device_name_mapping) + device_names = {device_key for device_key in device_keys if RE_UUID.match(device_key) is None} + if len(device_names) != 1: + MSG = 'Unable to identify name for Device({:s}): device_keys({:s})' + raise Exception(MSG.format(str(resource_key_values[0]), str(device_keys))) + resource_key_values[0] = device_names.pop() + + endpoint_keys = compute_endpoint_keys(device_keys, resource_key_values[1], endpoint_name_mapping) + endpoint_names = {endpoint_key for endpoint_key in endpoint_keys if RE_UUID.match(endpoint_key) is None} + if len(endpoint_names) != 1: + MSG = 'Unable to identify name for Endpoint({:s}): endpoint_keys({:s})' + raise Exception(MSG.format(str(resource_key_values[1]), str(endpoint_keys))) + resource_key_values[1] = endpoint_names.pop() + + resource_value : Dict = json.loads(config_rule['custom']['resource_value']) + if 'neighbor_address' not in resource_value: continue + resource_value['ip_address'] = resource_value.pop('neighbor_address') + + # remove neighbor_address also from original rule as it is already consumed + + resource_key_template = TMPL_ENDPOINT_VLAN_SETTINGS if len(match.groups()) == 3 else TMPL_ENDPOINT_SETTINGS + generated_config_rule = copy.deepcopy(config_rule) + generated_config_rule['custom']['resource_key'] = resource_key_template.format(*resource_key_values) + generated_config_rule['custom']['resource_value'] = json.dumps(resource_value) + generated_config_rules.append(generated_config_rule) + + LOGGER.debug('[generate_neighbor_endpoint_config_rules] generated_config_rules={:s}'.format(str(generated_config_rules))) + LOGGER.debug('[generate_neighbor_endpoint_config_rules] end') + return generated_config_rules diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py b/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py index 06b24031bd82b6b51add6569b9c35b360f0491fe..86a91d00aca2b2b5465bab654d7ab3de0fbe148b 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComputeSubServices.py @@ -95,6 +95,36 @@ def convert_explicit_path_hops_to_connections( connections.append(connection) connection_stack.queue[-1][3].append(connection[0]) #connection_stack.queue[-1][2].append(path_hop) + elif prv_res_class[2] is None and res_class[2] is not None: + # entering domain of a device controller, create underlying connection + LOGGER.debug(' entering domain of a device controller, create underlying connection') + sub_service_uuid = str(uuid.uuid4()) + prv_service_type = connection_stack.queue[-1][1] + service_type = get_service_type(res_class[1], prv_service_type) + connection_stack.put((sub_service_uuid, service_type, [path_hop], [])) + elif prv_res_class[2] is not None and res_class[2] is None: + # leaving domain of a device controller, terminate underlying connection + LOGGER.debug(' leaving domain of a device controller, terminate underlying connection') + connection = connection_stack.get() + connections.append(connection) + connection_stack.queue[-1][3].append(connection[0]) + connection_stack.queue[-1][2].append(path_hop) + elif prv_res_class[2] is not None and res_class[2] is not None: + if prv_res_class[2] == res_class[2]: + # stay in domain of a device controller, connection continues + LOGGER.debug(' stay in domain of a device controller, connection continues') + connection_stack.queue[-1][2].append(path_hop) + else: + # switching to different device controller, chain connections + LOGGER.debug(' switching to different device controller, chain connections') + connection = connection_stack.get() + connections.append(connection) + connection_stack.queue[-1][3].append(connection[0]) + + sub_service_uuid = str(uuid.uuid4()) + prv_service_type = connection_stack.queue[-1][1] + service_type = get_service_type(res_class[1], prv_service_type) + connection_stack.put((sub_service_uuid, service_type, [path_hop], [])) elif prv_res_class[0] is None: # path ingress LOGGER.debug(' path ingress') diff --git a/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py b/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py index 843c41803805106e9f7575fb9ff6b1344d036994..7b5221c88fd1baa13ef44dc0a0c06e76cf8dc813 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ResourceGroups.py @@ -24,6 +24,9 @@ DEVICE_TYPE_TO_DEEPNESS = { DeviceTypeEnum.DATACENTER.value : 90, DeviceTypeEnum.TERAFLOWSDN_CONTROLLER.value : 80, + DeviceTypeEnum.EMULATED_IP_SDN_CONTROLLER.value : 80, + DeviceTypeEnum.IP_SDN_CONTROLLER.value : 80, + DeviceTypeEnum.EMULATED_PACKET_ROUTER.value : 70, DeviceTypeEnum.PACKET_ROUTER.value : 70, diff --git a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py index 73a741ae551c5179cb78268f6fb87040c8481c53..094baa1a674fab1a573a97c364a12386ca940cc9 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py @@ -22,6 +22,7 @@ NETWORK_DEVICE_TYPES = { PACKET_DEVICE_TYPES = { DeviceTypeEnum.TERAFLOWSDN_CONTROLLER, + DeviceTypeEnum.IP_SDN_CONTROLLER, DeviceTypeEnum.EMULATED_IP_SDN_CONTROLLER, DeviceTypeEnum.PACKET_ROUTER, DeviceTypeEnum.EMULATED_PACKET_ROUTER, DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH, } diff --git a/src/service/requirements.in b/src/service/requirements.in index 48fd76485d6bbaf53c3867147882614fc0cf1b04..a10f7da7aba3027a89bdd8e9b62bc4ab90fb5e02 100644 --- a/src/service/requirements.in +++ b/src/service/requirements.in @@ -15,6 +15,7 @@ anytree==2.8.0 geopy==2.3.0 +netaddr==0.9.0 networkx==2.6.3 pydot==1.4.2 redis==4.1.2 diff --git a/src/service/service/service_handlers/__init__.py b/src/service/service/service_handlers/__init__.py index 551d35c7ba8f5386831871fe70fdc962633e1a18..eaf8f715aeff435b67bce77928e726daeb4729e2 100644 --- a/src/service/service/service_handlers/__init__.py +++ b/src/service/service/service_handlers/__init__.py @@ -20,6 +20,7 @@ from .l2nm_openconfig.L2NMOpenConfigServiceHandler import L2NMOpenConfigServiceH from .l3nm_emulated.L3NMEmulatedServiceHandler import L3NMEmulatedServiceHandler from .l3nm_openconfig.L3NMOpenConfigServiceHandler import L3NMOpenConfigServiceHandler from .l3nm_gnmi_openconfig.L3NMGnmiOpenConfigServiceHandler import L3NMGnmiOpenConfigServiceHandler +from .l3nm_ietf_actn.L3NMIetfActnServiceHandler import L3NMIetfActnServiceHandler from .microwave.MicrowaveServiceHandler import MicrowaveServiceHandler from .p4.p4_service_handler import P4ServiceHandler from .tapi_tapi.TapiServiceHandler import TapiServiceHandler @@ -57,6 +58,12 @@ SERVICE_HANDLERS = [ FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_GNMI_OPENCONFIG, } ]), + (L3NMIetfActnServiceHandler, [ + { + FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_L3NM, + FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN, + } + ]), (TapiServiceHandler, [ { FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, diff --git a/src/service/service/service_handlers/l3nm_ietf_actn/Constants.py b/src/service/service/service_handlers/l3nm_ietf_actn/Constants.py new file mode 100644 index 0000000000000000000000000000000000000000..62babd7c20491a2673a6dc3d10db33d33bb0a47a --- /dev/null +++ b/src/service/service/service_handlers/l3nm_ietf_actn/Constants.py @@ -0,0 +1,52 @@ +# 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. + +# These hardcoded values will be updated with proper logic in second phase of the PoC + +VPN_VLAN_TAGS_TO_SERVICE_NAME = { + (21, 101): ('osu_tunnel_1', 'etht_service_1'), + (31, 201): ('osu_tunnel_2', 'etht_service_2'), +} + +OSU_TUNNEL_SETTINGS = { + 'osu_tunnel_1': { + 'odu_type': 'osuflex', + 'osuflex_number': 40, + 'bidirectional': True, + 'delay': 20, + 'ttp_channel_names': { + ('10.0.10.1', '200'): 'och:1-odu2:1-oduflex:1-osuflex:2', + ('10.0.30.1', '200'): 'och:1-odu2:1-oduflex:3-osuflex:1', + } + }, + 'osu_tunnel_2': { + 'odu_type': 'osuflex', + 'osuflex_number': 40, + 'bidirectional': True, + 'delay': 20, + 'ttp_channel_names': { + ('10.0.10.1', '200'): 'och:1-odu2:1-oduflex:1-osuflex:2', + ('10.0.30.1', '200'): 'och:1-odu2:1-oduflex:3-osuflex:1', + } + }, +} + +ETHT_SERVICE_SETTINGS = { + 'etht_service_1': { + 'service_type': 'op-mp2mp-svc', + }, + 'etht_service_2': { + 'service_type': 'op-mp2mp-svc', + }, +} diff --git a/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py b/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py new file mode 100644 index 0000000000000000000000000000000000000000..0c20fdf96d30d0c12ef3f8c0a29189befd791a15 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_ietf_actn/L3NMIetfActnServiceHandler.py @@ -0,0 +1,318 @@ +# 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, logging, netaddr +from typing import Any, Dict, List, Optional, Tuple, Union +from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method +from common.proto.context_pb2 import ConfigRule, Device, DeviceId, EndPoint, Service +from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set +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.task_scheduler.TaskExecutor import TaskExecutor +from .Constants import ETHT_SERVICE_SETTINGS, OSU_TUNNEL_SETTINGS, VPN_VLAN_TAGS_TO_SERVICE_NAME + +LOGGER = logging.getLogger(__name__) + +METRICS_POOL = MetricsPool('Service', 'Handler', labels={'handler': 'l3nm_ietf_actn'}) + +class L3NMIetfActnServiceHandler(_ServiceHandler): + def __init__( # pylint: disable=super-init-not-called + self, service : Service, task_executor : TaskExecutor, **settings + ) -> None: + self.__service = service + self.__task_executor = task_executor + self.__settings_handler = SettingsHandler(service.service_config, **settings) + + def _get_endpoint_details( + self, endpoint : Tuple[str, str, Optional[str]] + ) -> Tuple[Device, EndPoint, Dict]: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + endpoint_settings = self.__settings_handler.get_endpoint_settings(device_obj, endpoint_obj) + device_name = device_obj.name + endpoint_name = endpoint_obj.name + if endpoint_settings is None: + MSG = 'Settings not found for Endpoint(device=[uuid={:s}, name={:s}], endpoint=[uuid={:s}, name={:s}])' + raise Exception(MSG.format(device_uuid, device_name, endpoint_uuid, endpoint_name)) + endpoint_settings_dict : Dict = endpoint_settings.value + return device_obj, endpoint_obj, endpoint_settings_dict + + def _get_service_names( + self, + src_endpoint_details : Tuple[Device, EndPoint, Dict], + dst_endpoint_details : Tuple[Device, EndPoint, Dict] + ) -> Tuple[str, str]: + _, _, src_endpoint_settings_dict = src_endpoint_details + src_vlan_tag = src_endpoint_settings_dict['vlan_tag'] + + _, _, dst_endpoint_settings_dict = dst_endpoint_details + dst_vlan_tag = dst_endpoint_settings_dict['vlan_tag'] + + service_names = VPN_VLAN_TAGS_TO_SERVICE_NAME.get((src_vlan_tag, dst_vlan_tag)) + if service_names is None: + MSG = 'Unable to find service names from VLAN tags(src={:s}, dst={:s})' + raise Exception(MSG.format(str(src_vlan_tag), str(dst_vlan_tag))) + return service_names + + def _compose_osu_tunnel( + self, osu_tunnel_name : str, + src_endpoint_details : Tuple[Device, EndPoint, Dict], + dst_endpoint_details : Tuple[Device, EndPoint, Dict], + is_delete : bool = False + ) -> ConfigRule: + osu_tunnel_resource_key = '/osu_tunnels/osu_tunnel[{:s}]'.format(osu_tunnel_name) + osu_tunnel_resource_value = {'name' : osu_tunnel_name} + if is_delete: + osu_tunnel_config_rule = json_config_rule_delete(osu_tunnel_resource_key, osu_tunnel_resource_value) + else: + src_device_obj, src_endpoint_obj, _ = src_endpoint_details + dst_device_obj, dst_endpoint_obj, _ = dst_endpoint_details + + osu_tunnel_settings = OSU_TUNNEL_SETTINGS[osu_tunnel_name] + ttp_channel_names = osu_tunnel_settings['ttp_channel_names'] + src_ttp_channel_name = ttp_channel_names[(src_device_obj.name, src_endpoint_obj.name)] + dst_ttp_channel_name = ttp_channel_names[(dst_device_obj.name, dst_endpoint_obj.name)] + + osu_tunnel_resource_value.update({ + 'odu_type' : osu_tunnel_settings['odu_type'], + 'osuflex_number' : osu_tunnel_settings['osuflex_number'], + 'bidirectional' : osu_tunnel_settings['bidirectional'], + 'delay' : osu_tunnel_settings['delay'], + 'src_node_id' : src_device_obj.name, + 'src_tp_id' : src_endpoint_obj.name, + 'src_ttp_channel_name': src_ttp_channel_name, + 'dst_node_id' : dst_device_obj.name, + 'dst_tp_id' : dst_endpoint_obj.name, + 'dst_ttp_channel_name': dst_ttp_channel_name, + }) + osu_tunnel_config_rule = json_config_rule_set(osu_tunnel_resource_key, osu_tunnel_resource_value) + LOGGER.debug('osu_tunnel_config_rule = {:s}'.format(str(osu_tunnel_config_rule))) + return ConfigRule(**osu_tunnel_config_rule) + + def _compose_static_routing( + self, src_vlan_tag : int, dst_vlan_tag : int + ) -> Tuple[List[Dict], List[Dict]]: + static_routing = self.__settings_handler.get('/static_routing') + if static_routing is None: raise Exception('static_routing not found') + static_routing_dict : Dict = static_routing.value + src_static_routes = list() + dst_static_routes = list() + for _, static_route in static_routing_dict.items(): + vlan_id = static_route['vlan-id'] + ipn_cidr = netaddr.IPNetwork(static_route['ip-network']) + ipn_network = str(ipn_cidr.network) + ipn_preflen = int(ipn_cidr.prefixlen) + next_hop = static_route['next-hop'] + if vlan_id == src_vlan_tag: + src_static_routes.append([ipn_network, ipn_preflen, next_hop]) + elif vlan_id == dst_vlan_tag: + dst_static_routes.append([ipn_network, ipn_preflen, next_hop]) + return src_static_routes, dst_static_routes + + def _compose_etht_service( + self, etht_service_name : str, osu_tunnel_name : str, + src_endpoint_details : Tuple[Device, EndPoint, Dict], + dst_endpoint_details : Tuple[Device, EndPoint, Dict], + is_delete : bool = False + ) -> ConfigRule: + etht_service_resource_key = '/etht_services/etht_service[{:s}]'.format(etht_service_name) + etht_service_resource_value = {'name' : etht_service_name} + if is_delete: + etht_service_config_rule = json_config_rule_delete(etht_service_resource_key, etht_service_resource_value) + else: + src_device_obj, src_endpoint_obj, src_endpoint_details = src_endpoint_details + src_vlan_tag = src_endpoint_details['vlan_tag'] + dst_device_obj, dst_endpoint_obj, dst_endpoint_details = dst_endpoint_details + dst_vlan_tag = dst_endpoint_details['vlan_tag'] + src_static_routes, dst_static_routes = self._compose_static_routing(src_vlan_tag, dst_vlan_tag) + etht_service_resource_value.update({ + 'osu_tunnel_name' : osu_tunnel_name, + 'service_type' : ETHT_SERVICE_SETTINGS[etht_service_name]['service_type'], + 'src_node_id' : src_device_obj.name, + 'src_tp_id' : src_endpoint_obj.name, + 'src_vlan_tag' : src_vlan_tag, + 'src_static_routes': src_static_routes, + 'dst_node_id' : dst_device_obj.name, + 'dst_tp_id' : dst_endpoint_obj.name, + 'dst_vlan_tag' : dst_vlan_tag, + 'dst_static_routes': dst_static_routes, + }) + etht_service_config_rule = json_config_rule_set(etht_service_resource_key, etht_service_resource_value) + LOGGER.debug('etht_service_config_rule = {:s}'.format(str(etht_service_config_rule))) + return ConfigRule(**etht_service_config_rule) + + @metered_subclass_method(METRICS_POOL) + def SetEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + LOGGER.debug('endpoints = {:s}'.format(str(endpoints))) + chk_type('endpoints', endpoints, list) + if len(endpoints) < 2: + LOGGER.warning('nothing done: not enough endpoints') + return [] + service_uuid = self.__service.service_id.service_uuid.uuid + LOGGER.debug('service_uuid = {:s}'.format(str(service_uuid))) + LOGGER.debug('self.__settings_handler = {:s}'.format(str(self.__settings_handler.dump_config_rules()))) + + results = [] + try: + src_endpoint_details = self._get_endpoint_details(endpoints[0]) + src_device_obj, _, _ = src_endpoint_details + src_controller = self.__task_executor.get_device_controller(src_device_obj) + if src_controller is None: src_controller = src_device_obj + + dst_endpoint_details = self._get_endpoint_details(endpoints[-1]) + dst_device_obj, _, _ = dst_endpoint_details + dst_controller = self.__task_executor.get_device_controller(dst_device_obj) + if dst_controller is None: dst_controller = dst_device_obj + + if src_controller.device_id.device_uuid.uuid != dst_controller.device_id.device_uuid.uuid: + raise Exception('Different Src-Dst devices not supported by now') + controller = src_controller + + osu_tunnel_name, etht_service_name = self._get_service_names( + src_endpoint_details, dst_endpoint_details + ) + + osu_tunnel_config_rule = self._compose_osu_tunnel( + osu_tunnel_name, src_endpoint_details, dst_endpoint_details, + is_delete=False + ) + + etht_service_config_rule = self._compose_etht_service( + etht_service_name, osu_tunnel_name, src_endpoint_details, + dst_endpoint_details, is_delete=False + ) + + del controller.device_config.config_rules[:] + controller.device_config.config_rules.append(osu_tunnel_config_rule) + controller.device_config.config_rules.append(etht_service_config_rule) + self.__task_executor.configure_device(controller) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to SetEndpoint for Service({:s})'.format(str(service_uuid))) + results.append(e) + + LOGGER.debug('results = {:s}'.format(str(results))) + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + LOGGER.debug('endpoints = {:s}'.format(str(endpoints))) + chk_type('endpoints', endpoints, list) + if len(endpoints) < 2: + LOGGER.warning('nothing done: not enough endpoints') + return [] + service_uuid = self.__service.service_id.service_uuid.uuid + LOGGER.debug('service_uuid = {:s}'.format(str(service_uuid))) + LOGGER.debug('self.__settings_handler = {:s}'.format(str(self.__settings_handler.dump_config_rules()))) + + results = [] + try: + src_endpoint_details = self._get_endpoint_details(endpoints[0]) + src_device_obj, _, _ = src_endpoint_details + src_controller = self.__task_executor.get_device_controller(src_device_obj) + if src_controller is None: src_controller = src_device_obj + + dst_endpoint_details = self._get_endpoint_details(endpoints[-1]) + dst_device_obj, _, _ = dst_endpoint_details + dst_controller = self.__task_executor.get_device_controller(dst_device_obj) + if dst_controller is None: dst_controller = dst_device_obj + + if src_controller.device_id.device_uuid.uuid != dst_controller.device_id.device_uuid.uuid: + raise Exception('Different Src-Dst devices not supported by now') + controller = src_controller + + osu_tunnel_name, etht_service_name = self._get_service_names( + src_endpoint_details, dst_endpoint_details + ) + + osu_tunnel_config_rule = self._compose_osu_tunnel( + osu_tunnel_name, src_endpoint_details, dst_endpoint_details, + is_delete=True + ) + + etht_service_config_rule = self._compose_etht_service( + etht_service_name, osu_tunnel_name, src_endpoint_details, + dst_endpoint_details, is_delete=True + ) + + del controller.device_config.config_rules[:] + controller.device_config.config_rules.append(osu_tunnel_config_rule) + controller.device_config.config_rules.append(etht_service_config_rule) + self.__task_executor.configure_device(controller) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteEndpoint for Service({:s})'.format(str(service_uuid))) + results.append(e) + + LOGGER.debug('results = {:s}'.format(str(results))) + return results + + @metered_subclass_method(METRICS_POOL) + 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))] + + @metered_subclass_method(METRICS_POOL) + 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))] + + @metered_subclass_method(METRICS_POOL) + 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 + + @metered_subclass_method(METRICS_POOL) + 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/service/service_handlers/l3nm_ietf_actn/__init__.py b/src/service/service/service_handlers/l3nm_ietf_actn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1549d9811aa5d1c193a44ad45d0d7773236c0612 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_ietf_actn/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/tests/f5g-poc-camara/POC-CAMARA-Guide.md b/src/tests/f5g-poc-camara/POC-CAMARA-Guide.md new file mode 100644 index 0000000000000000000000000000000000000000..85ec44cb6df77089194b9c8d7a7f5309a8513677 --- /dev/null +++ b/src/tests/f5g-poc-camara/POC-CAMARA-Guide.md @@ -0,0 +1,162 @@ +# TeraFlowSDN - ETSI F5G PoC CAMARA Guide + +This guide describes how to: +1. Configure and Deploy TeraFlowSDN for the ETSI F5G PoC CAMARA +2. Start Mock IETF ACTN SDN Controller (for testing and debugging) +3. Onboard the network topology descriptor +4. Expose the topology through the RESTConf IETF Network endpoint +5. Create Services through RESTConf IETF L3VPN endpoint +6. Get State of Services through RESTConf IETF L3VPN endpoint +7. Check configurations done in the Mock IETF ACTN SDN Controller (for testing and debugging) +8. Destroy Services through RESTConf IETF L3VPN endpoint + + +## 1. Configure and Deploy TeraFlowSDN for the ETSI F5G PoC CAMARA + +This guide assumes the user pre-configured a physical/virtual machine based on the steps described in +the official +[ETSI TeraFlowSDN - Deployment Guide](https://labs.etsi.org/rep/tfs/controller/-/wikis/1.-Deployment-Guide). + +__NOTE__: When you perform step _1.3. Deploy TeraFlowSDN_, configure the `my_deploy.sh` script modifying +the following settings: +```bash +# ... +export TFS_COMPONENTS="context device pathcomp service slice nbi webui" +# ... +export CRDB_DROP_DATABASE_IF_EXISTS="YES" +# ... +export QDB_DROP_TABLES_IF_EXIST="YES" +# ... +``` + +After modifying the file, deploy the TeraFlowSDN using the regular script `./deploy/all.sh`. +The script might take a while to run, especially the first time, since it needs to build the TeraFlowSDN +microservices. + + +## 2. Start Mock IETF ACTN SDN Controller (for testing and debugging) + +__NOTE__: This step is not needed when using the real NCE-T controller. + +Start the Mock IETF ACTN SDN Controller. This controller is a simple Python script that accepts requests +based on agreed F5G PoC CAMARA and stores it in memory, mimicking the NCE-T controller. + +Run the Mock IETF ACTN SDN Controller as follows: +```bash +python src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py +``` + + +## 3. Onboard the network topology descriptor + +The network topology descriptor is a TeraFlowSDN configuration file describing the different elements to be +managed by the SDN controller, such as devices, links, networks, etc. A preliminary descriptor has been +prepared for the PoC CAMARA. The file is named as `topology-real.json`. + +**NOTE**: Before onboarding file `topology-real.json`, update settings of device `nce-t` to match the IP +address, port, username, password, HTTP scheme, etc. of the real NCE-T. + +To onboard the descriptor file, navigate to the [TeraFlowSDN WebUI](http://127.0.0.1/webui) > Home. +Browse the file through the _Descriptors_ field, and click the _Submit_ button that is next to the field. +The onboarding should take few seconds and the WebUI should report that 1 context, 1 topology, 1 controller, +10 devices, and 24 links were added. Also, it should report that 1 topology was updated. + +Next, select in the field _Ctx/Topo_ the entry named as `Context(admin):Topology(admin)`, and click the +_Submit_ button that is next to the field. The topology should be displayed just below. + +Then, navigate to the WebUI > Devices and WebUI > Links sections to familiarize with the details provided +for each entity. You can check the details of each entity by clicking the eye-shaped icon on the right +side of each row. + +The underlying devices are configured as EMULATED entities while the NCE-T controller is configured as an +IP SDN controller. Auto-discovery of devices is not implemented as this will fall in PoC phase two. + + +## 4. Expose the topology through the RESTConf IETF Network endpoint + +The TeraFlowSDN controller features an NBI component that exposes RESTConf-based endpoints. To retrieve the +topology following the IETF Network data model, use the following `curl` (or similar) command: + +```bash +curl -u admin:admin http://127.0.0.1/restconf/data/ietf-network:networks/ +``` + +__NOTE__: The command requires to interrogate the complete database and might take few seconds to complete. + + +## 5. Create Services through RESTConf IETF L3VPN endpoint + +The TeraFlowSDN controller's NBI component also exposes the IETF L3VPN endpoints to +create/check_status/delete services. To try them, use the following `curl` (or similar) commands: + +```bash +curl -u admin:admin -X POST -H "Content-Type: application/json" -d @src/nbi/tests/data/ietf_l3vpn_req_svc1.json http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services +curl -u admin:admin -X POST -H "Content-Type: application/json" -d @src/nbi/tests/data/ietf_l3vpn_req_svc2.json http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services +``` + +__NOTE 1__: This command uses the provided descriptors for creating the VPN services with some adaptations +to adjust to the official data model. + +__NOTE 2__: This command retrieves no data if everything succeeds, in case of error, it will be reported. + +This step will create the services in TeraFlowSDN and create the appropriate configuration rules in the +NCE-T controller through the appropriate service handlers and SBI drivers. + +When the services are created, navigate to the WebUI > Services section to familiarize with the details +provided for each service. You can check the details of the service by clicking the eye-shaped icon on +the right side of each row. + +Note that two services are created per requested VPN. The reason for that is because those services named +as "vpnX" (the name provided in the request) correspond to end-to-end services, while the others with a UUID +as a name are generated by TeraFlowSDN to represent the transport segment managed through the NCE-T. +TeraFlowSDN gathers the settings from the upper-layer end-to-end service and contructs the NCE-T-bound +services. + +Also, you can navigate to the WebUI > Devices section, click on the eye-shaped icon next to the `nce-t` +device and check the configuration rules (defined using an internal data model, not IETF ACTN) that are +later converted into the IETF ACTN configuration instructions sent to the NCE-T. + +You should see in configuration rules of the `nce-t` device rules with a resource key formatted as +`/osu_tunnels/osu_tunnel[{osu-tunnel-name}]` for each OSU tunnel, and others with resource key like +`/etht_services/etht_service[{etht-service-name}]` for each EthT service. + + +## 6. Get State of Services through RESTConf IETF L3VPN endpoint + +To get the status of the services, use the following `curl` (or similar) commands: + +```bash +curl -u admin:admin http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services/vpn-service=vpn1 +curl -u admin:admin http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services/vpn-service=vpn2 +``` + +__NOTE__: This command retrieves an empty dictionary with no error if the service is ready and ACTIVE. + + +## 7. Check configurations done in the Mock IETF ACTN SDN Controller (for testing and debugging) + +__NOTE__: This step is not needed when using the real NCE-T controller. + +While running the Mock IETF ACTN SDN Controller, you can interrogate the OSU tunnels and EthT Services +created using the following commands: + +```bash +curl --insecure https://127.0.0.1:8443/restconf/v2/data/ietf-te:te/tunnels +curl --insecure https://127.0.0.1:8443/restconf/v2/data/ietf-eth-tran-service:etht-svc +``` + + +## 8. Destroy Services through RESTConf IETF L3VPN endpoint + +To destroy the services, use the following `curl` (or similar) commands: + +```bash +curl -u admin:admin -X DELETE http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services/vpn-service=vpn1 +curl -u admin:admin -X DELETE http://127.0.0.1/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services/vpn-service=vpn2 +``` + +__NOTE__: This command retrieves no data when it succeeds. + +When the services are deleted, navigate to the WebUI > Services section verify that no service is present. +Besides, navigate to the WebUI > Devices section, and inspect the NCE-T device to verify that the OSU +tunnel and ETHT service configuration rules disapeared. diff --git a/src/tests/f5g-poc-camara/data/topology-real.json b/src/tests/f5g-poc-camara/data/topology-real.json new file mode 100644 index 0000000000000000000000000000000000000000..c8c146ce28101cb727ceb526ef8fcd3aeefb4e53 --- /dev/null +++ b/src/tests/f5g-poc-camara/data/topology-real.json @@ -0,0 +1,260 @@ +{ + "contexts": [ + {"context_id": {"context_uuid": {"uuid": "admin"}}} + ], + + "topologies": [ + {"topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "admin"}}} + ], + + "devices": [ + {"device_id": {"device_uuid": {"uuid": "nce-t"}}, "name": "nce-t", "device_type": "ip-sdn-controller", + "device_operational_status": 1, "device_drivers": [10], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "10.0.2.10"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "8443"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": { + "scheme": "https", "username": "admin", "password": "admin", "base_url": "/restconf/v2/data", + "timeout": 120, "verify": false + }}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "name": "10.0.10.1", "device_type": "emu-packet-router", + "controller_id": {"device_uuid": {"uuid": "nce-t"}}, + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "mgmt", "name": "mgmt", "type": "mgmt" }, + {"uuid": "200", "name": "200", "type": "copper" }, + {"uuid": "500", "name": "500", "type": "optical"}, + {"uuid": "501", "name": "501", "type": "optical"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "name": "10.0.20.1", "device_type": "emu-packet-router", + "controller_id": {"device_uuid": {"uuid": "nce-t"}}, + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "mgmt", "name": "mgmt", "type": "mgmt" }, + {"uuid": "500", "name": "500", "type": "optical"}, + {"uuid": "501", "name": "501", "type": "optical"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "name": "10.0.30.1", "device_type": "emu-packet-router", + "controller_id": {"device_uuid": {"uuid": "nce-t"}}, + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "mgmt", "name": "mgmt", "type": "mgmt" }, + {"uuid": "200", "name": "200", "type": "copper" }, + {"uuid": "500", "name": "500", "type": "optical"}, + {"uuid": "501", "name": "501", "type": "optical"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "name": "10.0.40.1", "device_type": "emu-packet-router", + "controller_id": {"device_uuid": {"uuid": "nce-t"}}, + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "mgmt", "name": "mgmt", "type": "mgmt" }, + {"uuid": "500", "name": "500", "type": "optical"}, + {"uuid": "501", "name": "501", "type": "optical"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.10.5"}}, "name": "128.32.10.5", "device_type": "emu-client", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "name": "eth1", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "name": "128.32.10.1", "device_type": "emu-packet-router", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "200", "name": "200", "type": "copper"}, + {"uuid": "500", "name": "500", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.20.5"}}, "name": "128.32.20.5", "device_type": "emu-client", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "name": "eth1", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "name": "128.32.20.1", "device_type": "emu-packet-router", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "200", "name": "200", "type": "copper"}, + {"uuid": "500", "name": "500", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "name": "128.32.33.5", "device_type": "emu-packet-router", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "200", "name": "200", "type": "copper"}, + {"uuid": "201", "name": "201", "type": "copper"}, + {"uuid": "500", "name": "500", "type": "copper"} + ]}}} + ]}}, + + {"device_id": {"device_uuid": {"uuid": "172.10.33.5"}}, "name": "172.10.33.5", "device_type": "emu-datacenter", + "device_operational_status": 1, "device_drivers": [0], "device_config": {"config_rules": [ + {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "200", "name": "200", "type": "copper"}, + {"uuid": "201", "name": "201", "type": "copper"}, + {"uuid": "500", "name": "500", "type": "copper"} + ]}}} + ]}} + ], + + "links": [ + {"link_id": {"link_uuid": {"uuid": "nce-t/mgmt==10.0.10.1/mgmt"}}, "name": "nce-t/mgmt==10.0.10.1/mgmt", "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "nce-t" }}, "endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "mgmt"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "nce-t/mgmt==10.0.20.1/mgmt"}}, "name": "nce-t/mgmt==10.0.20.1/mgmt", "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "nce-t" }}, "endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "mgmt"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "nce-t/mgmt==10.0.30.1/mgmt"}}, "name": "nce-t/mgmt==10.0.30.1/mgmt", "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "nce-t" }}, "endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "mgmt"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "nce-t/mgmt==10.0.40.1/mgmt"}}, "name": "nce-t/mgmt==10.0.40.1/mgmt", "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "nce-t" }}, "endpoint_uuid": {"uuid": "mgmt"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "mgmt"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "10.0.10.1-501"}}, "name": "10.0.10.1-501", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "501"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "501"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.20.1-501"}}, "name": "10.0.20.1-501", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "501"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "501"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "10.0.10.1-500"}}, "name": "10.0.10.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.40.1-500"}}, "name": "10.0.40.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "10.0.20.1-500"}}, "name": "10.0.20.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.30.1-500"}}, "name": "10.0.30.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.20.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "10.0.40.1-501"}}, "name": "10.0.40.1-501", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "501"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "501"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.30.1-501"}}, "name": "10.0.30.1-501", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "501"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.40.1"}}, "endpoint_uuid": {"uuid": "501"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.10.5-eth1"}}, "name": "128.32.10.5-eth1", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.10.5"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "endpoint_uuid": {"uuid": "200" }} + ]}, + {"link_id": {"link_uuid": {"uuid": "128.32.10.1-200"}}, "name": "128.32.10.1-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "endpoint_uuid": {"uuid": "200" }}, + {"device_id": {"device_uuid": {"uuid": "128.32.10.5"}}, "endpoint_uuid": {"uuid": "eth1"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.10.1-500"}}, "name": "128.32.10.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "200"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "128.32.33.5-200"}}, "name": "128.32.33.5-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "200"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.10.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.20.5-eth1"}}, "name": "128.32.20.5-eth1", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.20.5"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "endpoint_uuid": {"uuid": "200" }} + ]}, + {"link_id": {"link_uuid": {"uuid": "128.32.20.1-200"}}, "name": "128.32.20.1-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "endpoint_uuid": {"uuid": "200" }}, + {"device_id": {"device_uuid": {"uuid": "128.32.20.5"}}, "endpoint_uuid": {"uuid": "eth1"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.20.1-500"}}, "name": "128.32.20.1-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "201"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "128.32.33.5-201"}}, "name": "128.32.33.5-201", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "201"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.20.1"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "128.32.33.5-500"}}, "name": "128.32.33.5-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "200"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.10.1-200"}}, "name": "10.0.10.1-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.10.1"}}, "endpoint_uuid": {"uuid": "200"}}, + {"device_id": {"device_uuid": {"uuid": "128.32.33.5"}}, "endpoint_uuid": {"uuid": "500"}} + ]}, + + {"link_id": {"link_uuid": {"uuid": "172.10.33.5-500"}}, "name": "172.10.33.5-500", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "172.10.33.5"}}, "endpoint_uuid": {"uuid": "500"}}, + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "200"}} + ]}, + {"link_id": {"link_uuid": {"uuid": "10.0.30.1-200"}}, "name": "10.0.30.1-200", + "attributes": {"total_capacity_gbps": 10, "used_capacity_gbps": 0}, "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "10.0.30.1"}}, "endpoint_uuid": {"uuid": "200"}}, + {"device_id": {"device_uuid": {"uuid": "172.10.33.5"}}, "endpoint_uuid": {"uuid": "500"}} + ]} + ] +} diff --git a/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py index c459c294ccb1c457c258a4e80018a90244702b8b..26243e2b6bd8e8d8bd2b64e9552be7d9d36c853e 100644 --- a/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py +++ b/src/tests/tools/mock_ietf_actn_sdn_ctrl/MockIetfActnSdnCtrl.py @@ -29,7 +29,7 @@ from ResourceOsuTunnels import OsuTunnel, OsuTunnels BIND_ADDRESS = '0.0.0.0' BIND_PORT = 8443 -BASE_URL = '/restconf/data' +BASE_URL = '/restconf/v2/data' STR_ENDPOINT = 'https://{:s}:{:s}{:s}'.format(str(BIND_ADDRESS), str(BIND_PORT), str(BASE_URL)) LOG_LEVEL = logging.DEBUG diff --git a/src/webui/service/static/topology_icons/Acknowledgements.txt b/src/webui/service/static/topology_icons/Acknowledgements.txt index 43ecee79806682317bd5d2a8ebff16bac8767220..08e9ed27ce3e87e05d8d1d1176704b53b93c8ef3 100644 --- a/src/webui/service/static/topology_icons/Acknowledgements.txt +++ b/src/webui/service/static/topology_icons/Acknowledgements.txt @@ -31,5 +31,9 @@ https://symbols.getvecta.com/stencil_241/158_local-director.6b38eab9e4.png => em https://symbols.getvecta.com/stencil_240/197_radio-tower.b6138c8c29.png => radio-router.png https://symbols.getvecta.com/stencil_241/216_radio-tower.5159339bc0.png => emu-radio-router.png -https://symbols.getvecta.com/stencil_240/124_laptop.be264ceb77.png => laptop.png -https://symbols.getvecta.com/stencil_241/154_laptop.c01910b6c8.png => emu-laptop.png +https://symbols.getvecta.com/stencil_240/124_laptop.be264ceb77.png => client.png +https://symbols.getvecta.com/stencil_241/154_laptop.c01910b6c8.png => emu-client.png + +https://symbols.getvecta.com/stencil_240/16_atm-tag-switch-router.3149d7e933.png => ip-sdn-controller.png +https://symbols.getvecta.com/stencil_241/46_atm-tag-sw-rtr.776719c0b0.png => emu-ip-sdn-controller.png + diff --git a/src/webui/service/static/topology_icons/emu-ip-sdn-controller.png b/src/webui/service/static/topology_icons/emu-ip-sdn-controller.png new file mode 100644 index 0000000000000000000000000000000000000000..ff4c69120e28b434df3d5a8db46fe304dbdd03e7 Binary files /dev/null and b/src/webui/service/static/topology_icons/emu-ip-sdn-controller.png differ diff --git a/src/webui/service/static/topology_icons/ip-sdn-controller.png b/src/webui/service/static/topology_icons/ip-sdn-controller.png new file mode 100644 index 0000000000000000000000000000000000000000..d0b1abe8738b7e34faaa34d286157a778069fdbb Binary files /dev/null and b/src/webui/service/static/topology_icons/ip-sdn-controller.png differ