Commit 03f4ef1b authored by Mohamad Rahhal's avatar Mohamad Rahhal
Browse files

Spine-Leaf Component

- Added some features
- Did some Changes
parent d510022c
Loading
Loading
Loading
Loading
+114 −0
Original line number Diff line number Diff line
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import json
from typing import List, Tuple

class FabricConfigBuilder:
    def __init__(self, state_store):
        self.store = state_store

    def build(self, fabric_id: str, device_id: str) -> List[Tuple[str, str]]:
        rules = []

        inventory = self.store.get_fabric_inventory(fabric_id)
        device    = self.store.get_device_role(fabric_id, device_id)
        if not device:
            return rules

        role = device["role"]
        asn  = device["asn"]
        lo   = device["loopback_ip"]

        # ---- Loopback IP ----
        rules.append((
            "/interfaces/ip",
            json.dumps({
                "address": f"{lo}/32",
                "interface": "lo",
                "comment": f"{role} loopback"
            })
        ))

        # ---- BGP Instance ----
        rules.append((
            "/network_instances/bgp_instance",
            json.dumps({
                "name": "default",
                "as": asn,
                "router_id": lo
            })
        ))

        # ---- VXLAN (leaf only) ----
        if role in ("leaf", "gateway"):
            vni = device["vnis"][0]
            rules.append((
                "/interfaces/vxlan",
                json.dumps({
                    "name": f"vxlan{vni}",
                    "vni": int(vni),
                    "local-address": lo,
                    "port": 4789
                })
            ))

        # ---- BGP sessions ----
        if role == "spine":
            for leaf in inventory["leaves"]:
                rules.append(self._bgp_session(
                    name=leaf["device_id"],
                    local_ip=lo,
                    remote_ip=leaf["loopback_ip"],
                    remote_as=leaf["asn"]
                ))

        elif role in ("leaf", "gateway"):
            for spine in inventory["spines"]:
                rules.append(self._bgp_session(
                    name=spine["device_id"],
                    local_ip=lo,
                    remote_ip=spine["loopback_ip"],
                    remote_as=spine["asn"]
                ))

        # ---- EVPN (leaf only) ----
        if role in ("leaf", "gateway"):
            rules.append((
                "/network_instances/bgp_evpn",
                json.dumps({
                    "name": "evpn-vni10",
                    "instance": "default",
                    "vni": int(vni)
                })
            ))

        return rules

    def _bgp_session(self, name, local_ip, remote_ip, remote_as):
        return (
            "/network_instances/bgp_session",
            json.dumps({
                "name": name,
                "instance": "default",
                "local.address": local_ip,
                "remote.address": remote_ip,
                "remote.as": int(remote_as),
                "local.role": "ebgp",
                "routing-table": "vrf-dataplane",
                "multihop": "yes",
                "afi": "ip,l2vpn"
            })
        )
+13 −11
Original line number Diff line number Diff line
@@ -17,6 +17,8 @@
import logging
from typing import Dict, List, Optional, Tuple as TypingTuple

from networkx import config

from common.proto.logical_resources_pb2 import (
    FabricId,
    ResourceReservation,
@@ -24,15 +26,14 @@ from common.proto.logical_resources_pb2 import (
    Tuple as LRTuple,
)
from common.proto.spineleaf_pb2 import ConfigSetting
from logical_resources_client.client.logicalresources import logicalresourcesClient
from logical_resources_client.client.logicalresources import LogicalResourceClient

LOGGER = logging.getLogger(__name__)


class ResourceAllocator:
    def __init__(self, logical_resource_client=None) -> None:
    def __init__(self) -> None:
        super().__init__()
        self.logical_resource_client = logical_resource_client or logicalresourcesClient()
        self.logical_resource_client = LogicalResourceClient()

    def _pick_first_available(self, resource_reply) -> Optional[str]:
        for entry in resource_reply.resources:
@@ -51,15 +52,18 @@ class ResourceAllocator:
        reply = self.logical_resource_client.ReserveResource(request)
        return bool(getattr(reply, 'success', False)), str(getattr(reply, 'message', ''))


    def _release(self, resource_type: str, value: str, fabric_id: str) -> None:
        if self.logical_resource_client is None:
            return

        request = ResourceReservation(
            tuple=LRTuple(type=resource_type, value=value),
            fabric_id=FabricId(fabric_id=fabric_id),  
        )
        self.logical_resource_client.ReleaseResource(request)


    def _rollback_reservations(self, reservations: List[TypingTuple[str, str]], fabric_id: str) -> None:
        for resource_type, value in reservations:
            try:
@@ -74,7 +78,6 @@ class ResourceAllocator:
        return self._pick_first_available(reply)

    def allocate_for_role(self, device_uuid: str, fabric_id: str, role: str):
        # Base underlay resources for every role.
        resource_types = ['asn', 'loopback', 'ip']
        if role in ('leaf', 'gateway'):
            resource_types.extend(['vlan', 'vni'])
@@ -87,11 +90,9 @@ class ResourceAllocator:
                value = self._get_one_available(resource_type)
                if not value:
                    raise RuntimeError(f'No available resource for type={resource_type}')

                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))

@@ -107,12 +108,13 @@ class ResourceAllocator:
            )

            db_resources = {
                'asn': config.asn,
                'asn': str(config.asn),
                'loopback_ip': config.ip_address,
                'underlay_ips': [config.remote_address] if config.remote_address else [],
                'vlans': [config.vlan_tag] if config.vlan_tag else [],
                'vnis': [config.vni] if config.vni else [],
                'vlans': [str(config.vlan_tag)] if config.vlan_tag else [],
                'vnis': [str(config.vni)] if config.vni else [],
            }

            return config, db_resources
        except Exception:  # pylint: disable=broad-except
            self._rollback_reservations(reserved, fabric_id)
+107 −40
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ from common.proto.spineleaf_pb2 import (
from common.proto.spineleaf_pb2_grpc import SpineLeafServicer
from ..state_store import SpineLeafStateStore as InternalDatabase
from ..scripts.config_payload import build_deploy_payload
from ..scripts.resource_allocator import ResourceAllocator

LOGGER = logging.getLogger(__name__)
METRICS_POOL = MetricsPool('SpineLeaf', 'RPC')
@@ -43,8 +44,30 @@ class SpineLeafServicerImpl(SpineLeafServicer):
    def __init__(self) -> None:
        LOGGER.debug('Creating Servicer...')
        self.db = InternalDatabase()
        self.resource_allocator = ResourceAllocator()
        LOGGER.debug('Servicer Created')


    def _allocate_for_role(self, request: SetDeviceRoleRequest, role: str, context: grpc.ServicerContext):
        fabric = self.db.get_fabric(request.fabric_id)
        if not fabric:
            context.set_code(grpc.StatusCode.NOT_FOUND)
            context.set_details(f'Fabric {request.fabric_id} not found')
            return None, None
        config, resources = self.resource_allocator.allocate_for_role(
            request.device_uuid,
            request.fabric_id,
            role
        )
        if not config or not resources:
            context.set_code(grpc.StatusCode.FAILED_PRECONDITION)
            context.set_details(
                f'Unable to allocate resources for role={role} device={request.device_uuid}'
            )
            return None, None
        return config, resources


    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def DefineSpineLeafFabric(self, request: FabricSettings, context: grpc.ServicerContext) -> FabricDefinitionResponse:
        fabric_id, _, _ = self.db.create_fabric(
@@ -60,59 +83,92 @@ class SpineLeafServicerImpl(SpineLeafServicer):
    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def SetDeviceAsSpine(self, request: SetDeviceRoleRequest, context: grpc.ServicerContext) -> ConfigSetting:
        role = 'spine'
        config = ConfigSetting(device_uuid=request.device_uuid, endpoint_uuid=request.device_uuid, local_role=role)
        ok, msg = self.db.set_device_role(request.fabric_id, request.device_uuid, role, {
            'asn': config.asn,
            'loopback_ip': config.ip_address,
            'underlay_ips': [config.remote_address] if config.remote_address else [],
            'vlans': [],
            'vnis': [],
        })
        if not ok:
            context.set_code(grpc.StatusCode.FAILED_PRECONDITION)
            context.set_details(msg)
        config, resources = self._allocate_for_role(request, role, context)
        if config is None:
            return ConfigSetting()

        try:
            ok, msg = self.db.set_device_role(
                request.fabric_id,
                request.device_uuid,
                role,
                resources
            )
            if not ok:
                raise RuntimeError(msg)

            self.db.store_config(request.fabric_id, request.device_uuid, config)
            return config

        except Exception as e:
            # rollback allocated LogicalResources
            for rtype, value in self.db.get_reserved_resources_for_device(
                request.fabric_id, request.device_uuid
            ):
                self.resource_allocator._release(rtype, value, request.fabric_id)

            context.set_code(grpc.StatusCode.INTERNAL)
            context.set_details(str(e))
            return ConfigSetting()

    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def SetDeviceAsLeaf(self, request: SetDeviceRoleRequest, context: grpc.ServicerContext) -> ConfigSetting:
        role = 'leaf'
        config = ConfigSetting(device_uuid=request.device_uuid, endpoint_uuid=request.device_uuid, local_role=role)
        ok, msg = self.db.set_device_role(request.fabric_id, request.device_uuid, role, {
            'asn': config.asn,
            'loopback_ip': config.ip_address,
            'underlay_ips': [config.remote_address] if config.remote_address else [],
            'vlans': [config.vlan_tag] if config.vlan_tag else [],
            'vnis': [config.vni] if config.vni else [],
        })
        if not ok:
            context.set_code(grpc.StatusCode.FAILED_PRECONDITION)
            context.set_details(msg)
        config, resources = self._allocate_for_role(request, role, context)
        if config is None:
            return ConfigSetting()
        try:
            ok, msg = self.db.set_device_role(
                request.fabric_id,
                request.device_uuid,
                role,
                resources
            )
            if not ok:
                raise RuntimeError(msg)

            self.db.store_config(request.fabric_id, request.device_uuid, config)
            return config

        except Exception as e:
            for rtype, value in self.db.get_reserved_resources_for_device(
                request.fabric_id, request.device_uuid
            ):
                self.resource_allocator._release(rtype, value, request.fabric_id)

            context.set_code(grpc.StatusCode.INTERNAL)
            context.set_details(str(e))
            return ConfigSetting()


    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def SetDeviceAsGateway(self, request: SetDeviceRoleRequest, context: grpc.ServicerContext) -> ConfigSetting:
        role = 'gateway'
        config = ConfigSetting(device_uuid=request.device_uuid, endpoint_uuid=request.device_uuid, local_role=role)
        ok, msg = self.db.set_device_role(request.fabric_id, request.device_uuid, role, {
            'asn': config.asn,
            'loopback_ip': config.ip_address,
            'underlay_ips': [config.remote_address] if config.remote_address else [],
            'vlans': [config.vlan_tag] if config.vlan_tag else [],
            'vnis': [config.vni] if config.vni else [],
        })
        if not ok:
            context.set_code(grpc.StatusCode.FAILED_PRECONDITION)
            context.set_details(msg)
        config, resources = self._allocate_for_role(request, role, context)
        if config is None:
            return ConfigSetting()
        try:
            ok, msg = self.db.set_device_role(
                request.fabric_id,
                request.device_uuid,
                role,
                resources
            )
            if not ok:
                raise RuntimeError(msg)

            self.db.store_config(request.fabric_id, request.device_uuid, config)
            return config
        except Exception as e:
            for rtype, value in self.db.get_reserved_resources_for_device(
                request.fabric_id, request.device_uuid
            ):
                self.resource_allocator._release(rtype, value, request.fabric_id)

            context.set_code(grpc.StatusCode.INTERNAL)
            context.set_details(str(e))
            return ConfigSetting()


    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def DeployDeviceConfig(self, request: DeployDeviceConfigRequest, context: grpc.ServicerContext) -> DeployResponse:
@@ -123,7 +179,18 @@ class SpineLeafServicerImpl(SpineLeafServicer):

    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def UnsetDeviceRole(self, request: UnsetDeviceRoleRequest, context: grpc.ServicerContext) -> OperationStatus:
        success, message = self.db.unset_device_role(request.fabric_id, request.device_uuid)
        fabric_id = request.fabric_id
        device_id = request.device_uuid
        reserved = self.db.get_reserved_resources_for_device(fabric_id, device_id)
        for resource_type, value in reserved:
            try:
                self.resource_allocator._release(resource_type, value, fabric_id)
            except Exception:
                LOGGER.exception(
                    'Failed to release resource %s=%s for device=%s fabric=%s',
                    resource_type, value, device_id, fabric_id
                )
        success, message = self.db.unset_device_role(fabric_id, device_id)
        return OperationStatus(success=success, message=message)

    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
+1 −1
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ class SpineLeafStateStore:

        tracked: List[Tuple[str, str]] = []
        if resources.get('asn'):
            tracked.append(('asn', resources['asn']))
            tracked.append(('asn', str(resources['asn'])))
        if resources.get('loopback_ip'):
            tracked.append(('loopback', resources['loopback_ip']))
        for ip in resources.get('underlay_ips', []):