Commit 9ca6c694 authored by Mohamad Rahhal's avatar Mohamad Rahhal
Browse files

Merge branch 'feat/389-cttc-spine-leaf-component' of...

Merge branch 'feat/389-cttc-spine-leaf-component' of https://labs.etsi.org/rep/tfs/controller into feat/390-cttc-integration-of-spine-leaf-fabric-management
parents 479e3333 efa62314
Loading
Loading
Loading
Loading
+121 −4
Original line number Diff line number Diff line
@@ -36,7 +36,7 @@
import concurrent.futures, copy, ipaddress, json, logging, operator
from typing import Any, Dict, List, Optional, Tuple, Union
from common.proto.context_pb2 import (
    Connection, Context, ContextId, Device, DeviceId, Empty,
    ConfigRule, Connection, Context, ContextId, Device, DeviceId, Empty,
    Link, LinkId, Service, ServiceId, Slice, SliceId,
    Topology, TopologyId , OpticalLink
)
@@ -46,8 +46,10 @@ from context.client.ContextClient import ContextClient
from device.client.DeviceClient import DeviceClient
from logical_resources.client.Logicalresourceclient import LogicalResourceClient
from common.proto.spineleaf_pb2 import SetDeviceRoleRequest
from common.proto.spineleaf_pb2 import DeployDeviceConfigRequest, SetDeviceRoleRequest
from common.proto.spineleaf_pb2 import DeployDeviceConfigRequest, FabricSettings, SetDeviceRoleRequest
from common.tools.context_queries.Device import get_device
from common.tools.object_factory.Context import json_context_id
from common.tools.object_factory.ConfigRule import json_config_rule_set
from context.client.ContextClient import ContextClient
from device.client.DeviceClient import DeviceClient
from spine_leaf.client.SpineLeafClient import SpineLeafClient
@@ -538,6 +540,52 @@ class DescriptorLoader:
                            device_map[device.name] = device_uuid
        return device_map.get(device_identifier)
    

    @staticmethod
    def _parse_spine_leaf_rule(entry: Any) -> Optional[Tuple[str, Dict[str, Any]]]:
        if not isinstance(entry, (list, tuple)) or len(entry) != 2:
            return None

        resource_key, resource_value = entry
        if isinstance(resource_value, str):
            try:
                resource_value = json.loads(resource_value)
            except Exception:
                return None

        if not isinstance(resource_value, dict):
            return None

        return str(resource_key), resource_value

    def _apply_spine_leaf_deploy_payload(self, device_uuid: str, payload_json: str) -> int:
        payload = json.loads(payload_json or '{}')
        config_rules = []
        for entry in payload.get('driver_config_rules', []):
            parsed = self._parse_spine_leaf_rule(entry)
            if parsed is None:
                continue
            resource_key, resource_value = parsed
            config_rules.append(ConfigRule(**json_config_rule_set(resource_key, resource_value)))

        if not config_rules:
            return 0

        device = get_device(
            self.__ctx_cli,
            device_uuid,
            rw_copy=True,
            include_endpoints=False,
            include_config_rules=False,
            include_components=False,
        )
        if device is None:
            raise Exception(f'Device not found: {device_uuid}')

        device.device_config.config_rules.extend(config_rules)
        self.__dev_cli.ConfigureDevice(device)
        return len(config_rules)
    
    @staticmethod
    def _derive_peer_ip(local_cidr: str) -> str:
        if not local_cidr:
@@ -595,9 +643,28 @@ class DescriptorLoader:
        for alloc_device, alloc in router_id_allocs.items():
            router_id_by_device[str(alloc_device)] = str(alloc.get('value', ''))

        loopback_allocs = logical_fabric.get('loopback', {}).get('allocations', {})
        loopback_by_device = {}
        for alloc_device, alloc in loopback_allocs.items():
            loopback_by_device[str(alloc_device)] = str(alloc.get('value', ''))

        vlan_allocs = logical_fabric.get('vlan', {}).get('allocations', {})
        vlans_by_device = {}
        for alloc_device, alloc in vlan_allocs.items():
            vlans_by_device.setdefault(str(alloc_device), []).append(str(alloc.get('value', '')))

        vni_allocs = logical_fabric.get('vni', {}).get('allocations', {})
        vnis_by_device = {}
        for alloc_device, alloc in vni_allocs.items():
            vnis_by_device.setdefault(str(alloc_device), []).append(str(alloc.get('value', '')))

        interfaces_alloc = logical_fabric.get('interface_ip_address', {}).get('allocations', {})
        device_interfaces = interfaces_alloc.get(device_uuid, {})
        interfaces = device_interfaces.get('interfaces', device_interfaces.get('endpoints', {}))
        bridge_ports_alloc = logical_fabric.get('bridge_ports', {})
        vrf_interfaces_alloc = logical_fabric.get('vrf_interfaces', {})
        tenant_networks = logical_fabric.get('tenant_networks', {})
        static_vteps_alloc = logical_fabric.get('static_vteps', {})

        hint_interfaces: List[Dict[str, str]] = []
        inferred_sessions: List[Dict[str, Any]] = []
@@ -688,6 +755,16 @@ class DescriptorLoader:
        hints: Dict[str, Any] = {}
        if hint_interfaces:
            hints['interfaces'] = hint_interfaces
        if asn_by_device.get(device_uuid):
            hints['asn'] = asn_by_device[device_uuid]
        loopback_ip = loopback_by_device.get(device_uuid) or router_id_by_device.get(device_uuid)
        if loopback_ip:
            hints['loopback_ip'] = loopback_ip
            hints['router_id'] = loopback_ip
        if vlans_by_device.get(device_uuid):
            hints['vlans'] = vlans_by_device[device_uuid]
        if vnis_by_device.get(device_uuid):
            hints['vnis'] = vnis_by_device[device_uuid]
        if first_underlay_ip:
            hints['underlay_ip'] = first_underlay_ip
        if first_remote_address:
@@ -734,6 +811,23 @@ class DescriptorLoader:

        if bgp_sessions:
            hints['bgp_sessions'] = bgp_sessions

        if isinstance(bridge_ports_alloc, dict):
            bridge_ports = bridge_ports_alloc.get(device_uuid, [])
            if isinstance(bridge_ports, list) and bridge_ports:
                hints['bridge_ports'] = bridge_ports
        if isinstance(vrf_interfaces_alloc, dict):
            vrf_interfaces = vrf_interfaces_alloc.get(device_uuid, [])
            if isinstance(vrf_interfaces, list) and vrf_interfaces:
                hints['vrf_interfaces'] = vrf_interfaces
        if isinstance(tenant_networks, dict):
            tenant = tenant_networks.get(device_uuid) or tenant_networks.get('default') or {}
            if isinstance(tenant, dict) and tenant:
                hints['tenant_network'] = tenant
        if isinstance(static_vteps_alloc, dict):
            static_vteps = static_vteps_alloc.get(device_uuid, [])
            if isinstance(static_vteps, list) and static_vteps:
                hints['static_vteps'] = static_vteps
        return hints

    def _process_spine_leaf(self) -> None:
@@ -763,8 +857,21 @@ class DescriptorLoader:
                error_list.append(f'{fabric_name}: fabric not registered in LogicalResources')
                continue
            settings = fabric.get('settings', {})
            apply_config = bool(settings.get('apply_config', False)) if isinstance(settings, dict) else False
            if settings:
                LOGGER.info('SpineLeaf settings received for fabric %s: %s', fabric_id, settings)
                try:
                    self.__spl_cli.DefineSpineLeafFabric(FabricSettings(
                        fabric_name=fabric_id,
                        underlay_type=str(settings.get('underlay_type', '')),
                        overlay_type=str(settings.get('overlay_type', '')),
                        spine_count=int(settings.get('spine_count', 0) or 0),
                        leaf_count=int(settings.get('leaf_count', 0) or 0),
                        gateway_count=int(settings.get('gateway_count', 0) or 0),
                    ))
                    num_ok += 1
                except Exception as e:
                    error_list.append(f'{fabric_id}: define fabric failed: {str(e)}')
                    continue
            device_roles = fabric.get('device_roles', {})

                # First pass: set roles for all devices so inventory is complete for subsequent generation
@@ -798,11 +905,21 @@ class DescriptorLoader:
            # Second pass: deploy configs for all devices that were set successfully
            for device_uuid, config in set_successful:
                try:
                    self.__spl_cli.DeployDeviceConfig(DeployDeviceConfigRequest(
                    deploy_response = self.__spl_cli.DeployDeviceConfig(DeployDeviceConfigRequest(
                        device_uuid=device_uuid,
                        fabric_id=fabric_id,
                        config=config,
                    ))
                    if apply_config:
                        applied_rules = self._apply_spine_leaf_deploy_payload(
                            device_uuid,
                            getattr(deploy_response, 'message', '') or '',
                        )
                        LOGGER.info(
                            'Applied %d SpineLeaf config rules to device %s through Device-SBI',
                            applied_rules,
                            device_uuid,
                        )
                except Exception as e:
                    error_list.append(f'{fabric_id}/{device_uuid}: deploy failed: {str(e)}')

+359 −150

File changed.

Preview size limit exceeded, changes collapsed.

+73 −10
Original line number Diff line number Diff line
@@ -31,9 +31,14 @@ class ResourceAllocator:
        super().__init__()
        self.logical_resource_client = LogicalResourceClient()

    def _pick_first_available(self, resource_reply) -> Optional[str]:
    def _pick_first_available(
        self,
        resource_reply,
        fabric_id: Optional[str] = None,
        allow_same_fabric: bool = False,
    ) -> Optional[str]:
        for entry in resource_reply.resources:
            if not entry.allocated:
            if not entry.allocated or (allow_same_fabric and entry.fabric_id == fabric_id):
                return entry.value
        return None

@@ -67,11 +72,25 @@ class ResourceAllocator:
            except Exception:  # pylint: disable=broad-except
                LOGGER.exception('Failed rollback for resource %s=%s', resource_type, value)

    def _get_one_available(self, resource_type: str) -> Optional[str]:
    def _is_reserved_by_fabric(self, resource_type: str, value: str, fabric_id: str) -> bool:
        if self.logical_resource_client is None:
            return False
        reply = self.logical_resource_client.GetResourcesByType(ResourceTypeQuery(resource_type=resource_type))
        for entry in reply.resources:
            if entry.value == value and entry.allocated and entry.fabric_id == fabric_id:
                return True
        return False

    def _get_one_available(
        self,
        resource_type: str,
        fabric_id: Optional[str] = None,
        allow_same_fabric: bool = False,
    ) -> Optional[str]:
        if self.logical_resource_client is None:
            return None
        reply = self.logical_resource_client.GetResourcesByType(ResourceTypeQuery(resource_type=resource_type))
        return self._pick_first_available(reply)
        return self._pick_first_available(reply, fabric_id=fabric_id, allow_same_fabric=allow_same_fabric)

    def _infer_remote_as(self, local_asn: str, fabric_id: str) -> Optional[str]:
        if self.logical_resource_client is None:
@@ -97,6 +116,10 @@ class ResourceAllocator:
    def _normalize_loopback(loopback_ip: str) -> str:
        return loopback_ip.split('/')[0] if loopback_ip else ''

    @staticmethod
    def _strip_prefix(address: str) -> str:
        return address.split('/')[0] if address else ''

    def _resource_profile(self, role: str, fabric: Optional[Dict]) -> Dict[str, object]:
        overlay_type = str((fabric or {}).get('overlay_type', '')).lower()
        underlay_type = str((fabric or {}).get('underlay_type', '')).lower()
@@ -115,12 +138,45 @@ class ResourceAllocator:
            profile['resource_types'].extend(['vlan', 'vni'])
            profile['include_overlay'] = True
            profile['afi'] = 'ip,l2vpn'
            profile['multihop'] = True

        

        return profile

    def allocate_for_role(self, device_uuid: str, fabric_id: str, role: str, fabric: Optional[Dict] = None):
    @staticmethod
    def _first_hint_value(hints: Optional[Dict], *keys: str) -> Optional[str]:
        if not hints:
            return None
        for key in keys:
            value = hints.get(key)
            if isinstance(value, list) and value:
                return str(value[0])
            if value not in (None, '', []):
                return str(value)
        return None

    def _preferred_resource(self, resource_type: str, hints: Optional[Dict]) -> Optional[str]:
        if resource_type == 'asn':
            return self._first_hint_value(hints, 'asn')
        if resource_type == 'loopback':
            return self._strip_prefix(self._first_hint_value(hints, 'loopback_ip', 'router_id') or '')
        if resource_type == 'ip':
            return self._strip_prefix(self._first_hint_value(hints, 'underlay_ip') or '')
        if resource_type == 'vlan':
            return self._first_hint_value(hints, 'vlans', 'vlan')
        if resource_type == 'vni':
            return self._first_hint_value(hints, 'vnis', 'vni')
        return None

    def allocate_for_role(
        self,
        device_uuid: str,
        fabric_id: str,
        role: str,
        fabric: Optional[Dict] = None,
        hints: Optional[Dict] = None,
    ):
        profile = self._resource_profile(role, fabric)
        resource_types = profile['resource_types']

@@ -129,14 +185,21 @@ class ResourceAllocator:

        try:
            for resource_type in resource_types:
                value = self._get_one_available(resource_type)
                preferred_value = self._preferred_resource(resource_type, hints)
                allow_same_fabric = resource_type in ('vlan', 'vni')
                value = preferred_value or self._get_one_available(resource_type, fabric_id, allow_same_fabric)
                if not value:
                    raise RuntimeError(f'No available resource for type={resource_type}')
                already_reserved = (
                    (allow_same_fabric or preferred_value is not None)
                    and self._is_reserved_by_fabric(resource_type, value, fabric_id)
                )
                if not already_reserved:
                    ok, message = self._reserve(resource_type, value, fabric_id)
                    if not ok:
                        raise RuntimeError(f'Could not reserve {resource_type}={value}: {message}')
                selected[resource_type] = value
                    reserved.append((resource_type, value))
                selected[resource_type] = value

            loopback_ip = selected.get('loopback', '')
            router_id = self._normalize_loopback(loopback_ip)
+31 −7
Original line number Diff line number Diff line
@@ -49,13 +49,14 @@ class SpineLeafServicerImpl(SpineLeafServicer):
        LOGGER.debug('Servicer Created')


    def _allocate_for_role(self, request: SetDeviceRoleRequest, role: str, context: grpc.ServicerContext):
    def _allocate_for_role(self, request: SetDeviceRoleRequest, role: str, context: grpc.ServicerContext, hints=None):
        fabric = self.db.get_fabric(request.fabric_id) or {}
        config, resources = self.resource_allocator.allocate_for_role(
            request.device_uuid,
            request.fabric_id,
            role,
            fabric,
            hints=hints,
        )
        if not config or not resources:
            context.set_code(grpc.StatusCode.FAILED_PRECONDITION)
@@ -108,10 +109,30 @@ class SpineLeafServicerImpl(SpineLeafServicer):
        if device_name:
            resources['device_name'] = device_name

        bridge = str(hints.get('bridge', '') or '')
        if bridge:
            resources['bridge'] = bridge

        bridge_ports = hints.get('bridge_ports', [])
        if isinstance(bridge_ports, list) and bridge_ports:
            resources['bridge_ports'] = bridge_ports

        bgp_sessions = hints.get('bgp_sessions', [])
        if isinstance(bgp_sessions, list) and bgp_sessions:
            resources['bgp_sessions'] = bgp_sessions

        vrf_interfaces = hints.get('vrf_interfaces', [])
        if isinstance(vrf_interfaces, list) and vrf_interfaces:
            resources['vrf_interfaces'] = vrf_interfaces

        tenant_network = hints.get('tenant_network', {})
        if isinstance(tenant_network, dict) and tenant_network:
            resources['tenant_network'] = tenant_network

        static_vteps = hints.get('static_vteps', [])
        if isinstance(static_vteps, list) and static_vteps:
            resources['static_vteps'] = static_vteps


    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def DefineSpineLeafFabric(self, request: FabricSettings, context: grpc.ServicerContext) -> FabricDefinitionResponse:
@@ -129,11 +150,12 @@ class SpineLeafServicerImpl(SpineLeafServicer):
    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def SetDeviceAsSpine(self, request: SetDeviceRoleRequest, context: grpc.ServicerContext) -> ConfigSetting:
        role = 'spine'
        config, resources = self._allocate_for_role(request, role, context)
        hints = self._extract_request_hints(request)
        config, resources = self._allocate_for_role(request, role, context, hints=hints)
        if config is None:
            return ConfigSetting()

        self._apply_hints(resources, self._extract_request_hints(request))
        self._apply_hints(resources, hints)

        try:
            ok, msg = self.db.set_device_role(
@@ -164,11 +186,12 @@ class SpineLeafServicerImpl(SpineLeafServicer):
    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def SetDeviceAsLeaf(self, request: SetDeviceRoleRequest, context: grpc.ServicerContext) -> ConfigSetting:
        role = 'leaf'
        config, resources = self._allocate_for_role(request, role, context)
        hints = self._extract_request_hints(request)
        config, resources = self._allocate_for_role(request, role, context, hints=hints)
        if config is None:
            return ConfigSetting()

        self._apply_hints(resources, self._extract_request_hints(request))
        self._apply_hints(resources, hints)

        try:
            ok, msg = self.db.set_device_role(
@@ -199,11 +222,12 @@ class SpineLeafServicerImpl(SpineLeafServicer):
    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def SetDeviceAsGateway(self, request: SetDeviceRoleRequest, context: grpc.ServicerContext) -> ConfigSetting:
        role = 'gateway'
        config, resources = self._allocate_for_role(request, role, context)
        hints = self._extract_request_hints(request)
        config, resources = self._allocate_for_role(request, role, context, hints=hints)
        if config is None:
            return ConfigSetting()

        self._apply_hints(resources, self._extract_request_hints(request))
        self._apply_hints(resources, hints)

        try:
            ok, msg = self.db.set_device_role(
+33 −3
Original line number Diff line number Diff line
@@ -32,10 +32,22 @@ class SpineLeafStateStore:
        topology_type: str,
        underlay_type: str,
        overlay_type: str,
        asn_range: str,
        ip_range: str,
        asn_range_or_spine_count: str,
        ip_range_or_leaf_count: str,
        gateway_count: str = '',
    ) -> Tuple[str, bool, str]:
        fabric_id = get_uuid_from_string(fabric_name)
        if gateway_count:
            asn_range = ''
            ip_range = ''
            spine_count = asn_range_or_spine_count
            leaf_count = ip_range_or_leaf_count
        else:
            asn_range = asn_range_or_spine_count
            ip_range = ip_range_or_leaf_count
            spine_count = ''
            leaf_count = ''

        self.fabrics[fabric_id] = {
            'name': fabric_name,
            'topology_type': topology_type,
@@ -43,6 +55,9 @@ class SpineLeafStateStore:
            'overlay_type': overlay_type,
            'asn_range': asn_range,
            'ip_range': ip_range,
            'spine_count': spine_count,
            'leaf_count': leaf_count,
            'gateway_count': gateway_count,
        }
        self.reserved_resources[fabric_id] = {}
        return fabric_id, True, f'Fabric {fabric_name} created with ID {fabric_id}'
@@ -53,7 +68,12 @@ class SpineLeafStateStore:
    def set_device_role(self, fabric_id: str, device_id: str, role: str, resources: Dict) -> Tuple[bool, str]:
        key = (fabric_id, device_id)
        if key in self.device_roles:
            return False, f'Device {device_id} already has a role in fabric {fabric_id}'
            existing_role = self.device_roles[key].get('role')
            if existing_role != role:
                return False, (
                    f'Device {device_id} already has role {existing_role} in fabric {fabric_id}; '
                    f'cannot set role {role}'
                )

        self.device_roles[key] = {
            'role': role,
@@ -67,8 +87,13 @@ class SpineLeafStateStore:
            'bgp_session_name': resources.get('bgp_session_name'),
            'bgp_sessions': resources.get('bgp_sessions', []),
            'interface_ips': resources.get('interface_ips', []),
            'bridge_ports': resources.get('bridge_ports', []),
            'bridge': resources.get('bridge'),
            'vlans': resources.get('vlans', []),
            'vnis': resources.get('vnis', []),
            'vrf_interfaces': resources.get('vrf_interfaces', []),
            'tenant_network': resources.get('tenant_network', {}),
            'static_vteps': resources.get('static_vteps', []),
            'status': 'configured',
        }

@@ -143,6 +168,11 @@ class SpineLeafStateStore:
                'bgp_remote_address': role_cfg.get('bgp_remote_address'),
                'bgp_session_name': role_cfg.get('bgp_session_name'),
                'interface_ips': role_cfg.get('interface_ips', []),
                'bridge_ports': role_cfg.get('bridge_ports', []),
                'bridge': role_cfg.get('bridge'),
                'vrf_interfaces': role_cfg.get('vrf_interfaces', []),
                'tenant_network': role_cfg.get('tenant_network', {}),
                'static_vteps': role_cfg.get('static_vteps', []),
            }

            if role_cfg['role'] == 'spine':
Loading