Commit 54cbd317 authored by Mohamad Rahhal's avatar Mohamad Rahhal
Browse files

SpineLeaf:

-Added methods + alignment to role of device
parent 550c71bf
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -20,6 +20,11 @@ RUN apt-get --yes --quiet --quiet update && \

ENV PYTHONUNBUFFERED=0


RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \
    wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \
    chmod +x /bin/grpc_health_probe

RUN python3 -m pip install --upgrade 'pip==25.2'
RUN python3 -m pip install --upgrade 'setuptools==79.0.0' 'wheel==0.45.1'
RUN python3 -m pip install --upgrade 'pip-tools==7.3.0'
@@ -53,5 +58,7 @@ COPY src/logical_resources/client/. logical_resources/client/
COPY src/device/__init__.py device/__init__.py
COPY src/device/client/. device/client/
COPY src/spine_leaf/. spine_leaf/
COPY src/service/__init__.py service/__init__.py
COPY src/service/service/. service/service/

ENTRYPOINT ["python", "-m", "spine_leaf.service"]
+46 −17
Original line number Diff line number Diff line
@@ -53,8 +53,11 @@ class FabricConfigBuilder:
        ))

        # ---- VXLAN (leaf only) ----
        vni = None
        if role in ("leaf", "gateway"):
            vni = device["vnis"][0]
            vnis = device.get("vnis", [])
            if vnis and len(vnis) > 0:
                vni = vnis[0]
                rules.append((
                    "/interfaces/vxlan",
                    json.dumps({
@@ -66,26 +69,52 @@ class FabricConfigBuilder:
                ))

        # ---- BGP sessions ----
        # Prefer per-link underlay IP lists when available. If both local and remote expose
        # underlay_ips lists, pick the first element of each as the session addresses. Fall
        # back to single 'underlay_ip' or loopback as necessary.
        if role == "spine":
            for leaf in inventory["leaves"]:
            leaves = inventory.get("leaves", [])
            for leaf in leaves:
                # prefer lists, then single value, then loopback
                local_ip = None
                remote_ip = None
                local_underlays = device.get('underlay_ips', [])
                remote_underlays = leaf.get('underlay_ips', [])
                if local_underlays and remote_underlays:
                    local_ip = local_underlays[0]
                    remote_ip = remote_underlays[0]
                else:
                    local_ip = device.get('underlay_ip') or (local_underlays[0] if local_underlays else None) or lo
                    remote_ip = leaf.get('underlay_ip') or (remote_underlays[0] if remote_underlays else None) or leaf.get('loopback_ip')

                rules.append(self._bgp_session(
                    name=leaf["device_id"],
                    local_ip=lo,
                    remote_ip=leaf["loopback_ip"],
                    local_ip=local_ip,
                    remote_ip=remote_ip,
                    remote_as=leaf["asn"]
                ))

        elif role in ("leaf", "gateway"):
            for spine in inventory["spines"]:
            spines = inventory.get("spines", [])
            for spine in spines:
                local_underlays = device.get('underlay_ips', [])
                remote_underlays = spine.get('underlay_ips', [])
                if local_underlays and remote_underlays:
                    local_ip = local_underlays[0]
                    remote_ip = remote_underlays[0]
                else:
                    local_ip = device.get('underlay_ip') or (local_underlays[0] if local_underlays else None) or lo
                    remote_ip = spine.get('underlay_ip') or (remote_underlays[0] if remote_underlays else None) or spine.get('loopback_ip')

                rules.append(self._bgp_session(
                    name=spine["device_id"],
                    local_ip=lo,
                    remote_ip=spine["loopback_ip"],
                    local_ip=local_ip,
                    remote_ip=remote_ip,
                    remote_as=spine["asn"]
                ))

        # ---- EVPN (leaf only) ----
        if role in ("leaf", "gateway"):
        if role in ("leaf", "gateway") and vni is not None:
            rules.append((
                "/network_instances/bgp_evpn",
                json.dumps({
+40 −7
Original line number Diff line number Diff line
@@ -73,10 +73,36 @@ class ResourceAllocator:
        reply = self.logical_resource_client.GetResourcesByType(ResourceTypeQuery(resource_type=resource_type))
        return self._pick_first_available(reply)

    def allocate_for_role(self, device_uuid: str, fabric_id: str, role: str):
        resource_types = ['asn', 'loopback', 'ip']
        if role in ('leaf', 'gateway'):
            resource_types.extend(['vlan', 'vni'])
    @staticmethod
    def _normalize_loopback(loopback_ip: str) -> str:
        return loopback_ip.split('/')[0] if loopback_ip 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()
        use_overlay = overlay_type in ('vxlan', 'bgp_evpn')

        profile = {
            'resource_types': ['asn', 'loopback', 'ip'],
            'routing_table': 'vrf-underlay' if role == 'spine' else 'vrf-dataplane',
            'local_role': 'ebgp',
            'multihop': True,
            'afi': 'ip',
            'include_overlay': False,
        }

        if role in ('leaf', 'gateway') and use_overlay:
            profile['resource_types'].extend(['vlan', 'vni'])
            profile['include_overlay'] = True
            profile['afi'] = 'ip,l2vpn'

        # Remove legacy IS-IS/OSPF automatic local_role mapping: use BGP underlay by default

        return profile

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

        selected: Dict[str, str] = {}
        reserved: List[TypingTuple[str, str]] = []
@@ -92,20 +118,27 @@ class ResourceAllocator:
                selected[resource_type] = value
                reserved.append((resource_type, value))

            loopback_ip = selected.get('loopback', '')
            router_id = self._normalize_loopback(loopback_ip)
            config = ConfigSetting(
                device_uuid=device_uuid,
                endpoint_uuid=device_uuid,
                local_role=role,
                asn=int(selected.get('asn', '0') or 0),
                ip_address=selected.get('loopback', ''),
                ip_address=loopback_ip,
                router_id=router_id,
                remote_address=selected.get('ip', ''),
                vlan_tag=int(selected.get('vlan', '0') or 0),
                vni=int(selected.get('vni', '0') or 0),
                routing_table=str(profile['routing_table']),
                multihop=bool(profile['multihop']),
                afi=str(profile['afi']),
                vlan_tag=int(selected.get('vlan', '0') or 0) if profile['include_overlay'] else 0,
                vni=int(selected.get('vni', '0') or 0) if profile['include_overlay'] else 0,
            )

            db_resources = {
                'asn': str(config.asn),
                'loopback_ip': config.ip_address,
                'underlay_ip': config.remote_address,  # Primary underlay IP for BGP sessions
                'underlay_ips': [config.remote_address] if config.remote_address else [],
                'vlans': [str(config.vlan_tag)] if config.vlan_tag else [],
                'vnis': [str(config.vni)] if config.vni else [],
+12 −14
Original line number Diff line number Diff line
@@ -49,15 +49,12 @@ class SpineLeafServicerImpl(SpineLeafServicer):


    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
        fabric = self.db.get_fabric(request.fabric_id) or {}
        config, resources = self.resource_allocator.allocate_for_role(
            request.device_uuid,
            request.fabric_id,
            role
            role,
            fabric,
        )
        if not config or not resources:
            context.set_code(grpc.StatusCode.FAILED_PRECONDITION)
@@ -75,8 +72,9 @@ class SpineLeafServicerImpl(SpineLeafServicer):
            'spine_leaf',
            request.underlay_type,
            request.overlay_type,
            '',
            '',
            str(request.spine_count),
            str(request.leaf_count),
            str(request.gateway_count),
        )
        return FabricDefinitionResponse(fabric_id=fabric_id)

@@ -98,6 +96,8 @@ class SpineLeafServicerImpl(SpineLeafServicer):
                raise RuntimeError(msg)

            self.db.store_config(request.fabric_id, request.device_uuid, config)
            config_rules = self.db.build_driver_config_rules(request.fabric_id, request.device_uuid)
            LOGGER.info('Generated spine config rules for fabric=%s device=%s: %s', request.fabric_id, request.device_uuid, config_rules)
            return config

        except Exception as e:
@@ -128,6 +128,8 @@ class SpineLeafServicerImpl(SpineLeafServicer):
                raise RuntimeError(msg)

            self.db.store_config(request.fabric_id, request.device_uuid, config)
            config_rules = self.db.build_driver_config_rules(request.fabric_id, request.device_uuid)
            LOGGER.info('Generated leaf config rules for fabric=%s device=%s: %s', request.fabric_id, request.device_uuid, config_rules)
            return config

        except Exception as e:
@@ -158,6 +160,8 @@ class SpineLeafServicerImpl(SpineLeafServicer):
                raise RuntimeError(msg)

            self.db.store_config(request.fabric_id, request.device_uuid, config)
            config_rules = self.db.build_driver_config_rules(request.fabric_id, request.device_uuid)
            LOGGER.info('Generated gateway config rules for fabric=%s device=%s: %s', request.fabric_id, request.device_uuid, config_rules)
            return config
        except Exception as e:
            for rtype, value in self.db.get_reserved_resources_for_device(
@@ -195,12 +199,6 @@ class SpineLeafServicerImpl(SpineLeafServicer):

    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def GetFabricInventory(self, request: FabricScope, context: grpc.ServicerContext) -> FabricInventory:
        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 FabricInventory(fabric_id=request.fabric_id)

        inventory = self.db.get_fabric_inventory(request.fabric_id)
        response = FabricInventory(fabric_id=request.fabric_id)

+16 −2
Original line number Diff line number Diff line
@@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import uuid
from typing import Dict, List, Optional, Tuple

from service.service.tools.object_uuid import get_uuid_from_string

class SpineLeafStateStore:
    #In-memory state for fabric/device roles and generated configs
@@ -23,6 +23,7 @@ class SpineLeafStateStore:
        self.fabrics: Dict[str, Dict] = {}
        self.device_roles: Dict[Tuple[str, str], Dict] = {}
        self.config_store: Dict[Tuple[str, str], Dict] = {}
        self.driver_config_rules: Dict[Tuple[str, str], List[Tuple[str, str]]] = {}
        self.reserved_resources: Dict[str, Dict[str, List[Tuple[str, str]]]] = {}

    def create_fabric(
@@ -34,7 +35,7 @@ class SpineLeafStateStore:
        asn_range: str,
        ip_range: str,
    ) -> Tuple[str, bool, str]:
        fabric_id = f"{fabric_name}-{uuid.uuid4().hex[:8]}"
        fabric_id = get_uuid_from_string(fabric_name)
        self.fabrics[fabric_id] = {
            'name': fabric_name,
            'topology_type': topology_type,
@@ -58,6 +59,7 @@ class SpineLeafStateStore:
            'role': role,
            'asn': resources.get('asn'),
            'loopback_ip': resources.get('loopback_ip'),
            'underlay_ip': resources.get('underlay_ip'),  # Primary underlay IP for BGP sessions
            'underlay_ips': resources.get('underlay_ips', []),
            'vlans': resources.get('vlans', []),
            'vnis': resources.get('vnis', []),
@@ -82,6 +84,16 @@ class SpineLeafStateStore:
        self.reserved_resources[fabric_id][device_id] = tracked
        return True, f'Device {device_id} set as {role} in fabric {fabric_id}'

    def build_driver_config_rules(self, fabric_id: str, device_id: str) -> List[Tuple[str, str]]:
        from spine_leaf.scripts.fabric_config_builder import FabricConfigBuilder

        rules = FabricConfigBuilder(self).build(fabric_id, device_id)
        self.driver_config_rules[(fabric_id, device_id)] = rules
        return rules

    def get_driver_config_rules(self, fabric_id: str, device_id: str) -> List[Tuple[str, str]]:
        return self.driver_config_rules.get((fabric_id, device_id), [])

    def get_device_role(self, fabric_id: str, device_id: str) -> Optional[Dict]:
        return self.device_roles.get((fabric_id, device_id))

@@ -95,6 +107,8 @@ class SpineLeafStateStore:
            del self.reserved_resources[fabric_id][device_id]
        if key in self.config_store:
            del self.config_store[key]
        if key in self.driver_config_rules:
            del self.driver_config_rules[key]

        return True, f'Device {device_id} role unset in fabric {fabric_id}'