Skip to content
Snippets Groups Projects
AbstractDevice.py 11.1 KiB
Newer Older
Lluis Gifre Renom's avatar
Lluis Gifre Renom committed
# 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(
Lluis Gifre Renom's avatar
Lluis Gifre Renom committed
        #        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)
        # 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