# 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 copy, logging from typing import Dict, Optional from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME, INTERDOMAIN_TOPOLOGY_NAME from common.DeviceTypes import DeviceTypeEnum from common.proto.context_pb2 import ContextId, Device, DeviceDriverEnum, DeviceOperationalStatusEnum, EndPoint from common.tools.context_queries.CheckType import ( device_type_is_datacenter, device_type_is_network, endpoint_type_is_border) from common.tools.context_queries.Device import add_device_to_topology, get_device from common.tools.grpc.Tools import grpc_message_to_json from common.tools.object_factory.Context import json_context_id from common.tools.object_factory.Device import json_device from context.client.ContextClient import ContextClient from .Tools import replace_device_uuids_by_names # Remark on UUIDs: # TopologyAbstractor, AbstractDevice and AbstractLink are used # to compose network reporesentations to be forwarded to remote # instances. Constraining it to use UUIDs is pointless given # these UUIDs, to be unique, need to be bound to the local # context/topology UUIDs, which might be different than that for # the remote TeraFlowSDN instances. For this very reason, we use # the device/link/endpoint/topology/context names as UUIDs, to # prevent UUID-related issues. LOGGER = logging.getLogger(__name__) class AbstractDevice: def __init__(self, device_name : str, device_type : DeviceTypeEnum): self.__context_client = ContextClient() self.__device_name : str = device_name self.__device_type : DeviceTypeEnum = device_type self.__device : Optional[Device] = None # Dict[device_name, Dict[endpoint_name, abstract EndPoint]] self.__device_endpoint_to_abstract : Dict[str, Dict[str, EndPoint]] = dict() # Dict[endpoint_name, device_name] self.__abstract_endpoint_to_device : Dict[str, str] = dict() def to_json(self) -> Dict: return { 'device_name' : self.__device_name, 'device_type' : self.__device_type, 'device' : grpc_message_to_json(self.__device), 'device_endpoint_to_abstract' : { device_name : { endpoint_name : grpc_message_to_json(endpoint) for endpoint_name, endpoint in endpoints.items() } for device_name, endpoints in self.__device_endpoint_to_abstract.items() }, 'abstract_endpoint_to_device' : self.__abstract_endpoint_to_device, } @property def name(self) -> str: return self.__device_name @property def device(self) -> Optional[Device]: return self.__device def get_endpoint(self, device_name : str, endpoint_name : str) -> Optional[EndPoint]: return self.__device_endpoint_to_abstract.get(device_name, {}).get(endpoint_name) def initialize(self) -> bool: if self.__device is not None: return False local_interdomain_device = get_device( self.__context_client, self.__device_name, rw_copy=True, include_endpoints=True, include_config_rules=False, include_components=False) create_abstract_device = local_interdomain_device is None if create_abstract_device: self._create_empty() else: self._load_existing(local_interdomain_device) is_datacenter = device_type_is_datacenter(self.__device_type) is_network = device_type_is_network(self.__device_type) if is_datacenter or is_network: # Add abstract device to INTERDOMAIN_TOPOLOGY_NAME topology admin_context_id = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME)) topology_name = INTERDOMAIN_TOPOLOGY_NAME add_device_to_topology(self.__context_client, admin_context_id, topology_name, self.__device_name) # seems not needed; to be removed in future releases #if is_datacenter and create_abstract_device: # dc_device = self.__context_client.GetDevice(DeviceId(**json_device_id(self.__device_uuid))) # if device_type_is_datacenter(dc_device.device_type): # self.update_endpoints(dc_device) #elif is_network: # devices_in_admin_topology = get_devices_in_topology( # self.__context_client, context_id, DEFAULT_TOPOLOGY_NAME) # for device in devices_in_admin_topology: # if device_type_is_datacenter(device.device_type): continue # self.update_endpoints(device) return create_abstract_device def _create_empty(self) -> None: device_name = self.__device_name device = Device(**json_device( device_name, self.__device_type.value, DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED, name=device_name, endpoints=[], config_rules=[], drivers=[DeviceDriverEnum.DEVICEDRIVER_UNDEFINED] )) self.__context_client.SetDevice(device) self.__device = device # Store copy with names as UUIDs def _load_existing(self, local_interdomain_device : Device) -> None: self.__device_endpoint_to_abstract = dict() self.__abstract_endpoint_to_device = dict() replace_device_uuids_by_names(self.__context_client, local_interdomain_device) self.__device = local_interdomain_device self.__device_type = self.__device.device_type device_type = self.__device_type is_datacenter = device_type_is_datacenter(device_type) is_network = device_type_is_network(device_type) if not is_datacenter and not is_network: LOGGER.warning('Unsupported InterDomain Device Type: {:s}'.format(str(device_type))) return # for each endpoint in abstract device, populate internal data structures and mappings for interdomain_endpoint in self.__device.device_endpoints: endpoint_name : str = interdomain_endpoint.name if is_network: endpoint_name,device_name = endpoint_name.split('@', maxsplit=1) else: device_name = self.__device_name self.__device_endpoint_to_abstract\ .setdefault(device_name, {}).setdefault(endpoint_name, interdomain_endpoint) self.__abstract_endpoint_to_device\ .setdefault(endpoint_name, device_name) # This method becomes useless; the endpoint name is considered a primary key; cannot be updated #def _update_endpoint_name(self, device_uuid : str, endpoint_uuid : str, endpoint_name : str) -> bool: # device_endpoint_to_abstract = self.__device_endpoint_to_abstract.get(device_uuid, {}) # interdomain_endpoint = device_endpoint_to_abstract.get(endpoint_uuid) # interdomain_endpoint_name = interdomain_endpoint.name # if endpoint_name == interdomain_endpoint_name: return False # interdomain_endpoint.name = endpoint_name # return True def _update_endpoint_type(self, device_name : str, endpoint_name : str, endpoint_type : str) -> bool: device_endpoint_to_abstract = self.__device_endpoint_to_abstract.get(device_name, {}) interdomain_endpoint = device_endpoint_to_abstract.get(endpoint_name) interdomain_endpoint_type = interdomain_endpoint.endpoint_type if endpoint_type == interdomain_endpoint_type: return False interdomain_endpoint.endpoint_type = endpoint_type return True def _add_endpoint(self, device_name : str, endpoint_name : str, endpoint_type : str) -> EndPoint: interdomain_endpoint = self.__device.device_endpoints.add() interdomain_endpoint.endpoint_id.topology_id.topology_uuid.uuid = DEFAULT_TOPOLOGY_NAME interdomain_endpoint.endpoint_id.topology_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_NAME interdomain_endpoint.endpoint_id.device_id.device_uuid.uuid = self.__device.name interdomain_endpoint.endpoint_id.endpoint_uuid.uuid = endpoint_name interdomain_endpoint.name = endpoint_name interdomain_endpoint.endpoint_type = endpoint_type self.__device_endpoint_to_abstract\ .setdefault(device_name, {}).setdefault(endpoint_name, interdomain_endpoint) self.__abstract_endpoint_to_device\ .setdefault(endpoint_name, device_name) return interdomain_endpoint def _remove_endpoint(self, device_name : str, endpoint_name : str, interdomain_endpoint : EndPoint) -> None: self.__abstract_endpoint_to_device.pop(endpoint_name, None) device_endpoint_to_abstract = self.__device_endpoint_to_abstract.get(device_name, {}) device_endpoint_to_abstract.pop(endpoint_name, None) self.__device.device_endpoints.remove(interdomain_endpoint) def update_endpoints(self, device : Device) -> bool: if device_type_is_datacenter(self.__device.device_type): return False device_name = device.name device_border_endpoint_names = { endpoint.name : endpoint.endpoint_type for endpoint in device.device_endpoints if endpoint_type_is_border(endpoint.endpoint_type) } updated = False # for each border endpoint in abstract device that is not in device; remove from abstract device device_endpoint_to_abstract = self.__device_endpoint_to_abstract.get(device_name, {}) _device_endpoint_to_abstract = copy.deepcopy(device_endpoint_to_abstract) for endpoint_name, interdomain_endpoint in _device_endpoint_to_abstract.items(): if endpoint_name in device_border_endpoint_names: continue # remove interdomain endpoint that is not in device self._remove_endpoint(device_name, endpoint_name, interdomain_endpoint) updated = True # for each border endpoint in device that is not in abstract device; add to abstract device for endpoint_name,endpoint_type in device_border_endpoint_names.items(): abstract_endpoint = self.__device_endpoint_to_abstract.get(device_name, {}).get(endpoint_name) abstract_endpoint_name = None if abstract_endpoint is None else abstract_endpoint.name # if already added; just check endpoint type is not modified if abstract_endpoint_name in self.__abstract_endpoint_to_device: #updated = updated or self._update_endpoint_name(device_name, endpoint_name, endpoint_name) updated = updated or self._update_endpoint_type(device_name, endpoint_name, endpoint_type) continue # otherwise, add it to the abstract device self._add_endpoint(device_name, endpoint_name, endpoint_type) updated = True return updated