Commit 3ab688b5 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 c40e680c 3d502262
Loading
Loading
Loading
Loading
+18 −5
Original line number Diff line number Diff line
@@ -696,6 +696,10 @@ class DescriptorLoader:
            hints['remote_as'] = first_remote_as
        if first_session_name:
            hints['bgp_session_name'] = first_session_name
        # include human-friendly device name if available
        device_name = name_by_device.get(device_uuid, '')
        if device_name:
            hints['device_name'] = device_name
        if inferred_sessions:
            hints['bgp_sessions'] = inferred_sessions

@@ -763,6 +767,8 @@ class DescriptorLoader:
                LOGGER.info('SpineLeaf settings received for fabric %s: %s', fabric_id, settings)
            device_roles = fabric.get('device_roles', {})

                # First pass: set roles for all devices so inventory is complete for subsequent generation
            set_successful = []
            for device_identifier, device_role in device_roles.items():
                device_uuid = self._resolve_device_uuid(str(device_identifier), device_map)
                if not device_uuid:
@@ -784,14 +790,21 @@ class DescriptorLoader:
                        request_kwargs['role'] = json.dumps(hints)
                    config = method(SetDeviceRoleRequest(**request_kwargs))
                    if config is not None and getattr(config, 'device_uuid', ''):
                        set_successful.append((device_uuid, config))
                    num_ok += 1
                except Exception as e:  
                    error_list.append(f'{fabric_id}/{device_identifier}: {str(e)}')

            # Second pass: deploy configs for all devices that were set successfully
            for device_uuid, config in set_successful:
                try:
                    self.__spl_cli.DeployDeviceConfig(DeployDeviceConfigRequest(
                        device_uuid=device_uuid,
                        fabric_id=fabric_id,
                        config=config,
                    ))
                    num_ok += 1
                except Exception as e:
                    error_list.append(f'{fabric_id}/{device_identifier}: {str(e)}')
                    error_list.append(f'{fabric_id}/{device_uuid}: deploy failed: {str(e)}')

        self.__results.append(('spine_leaf', 'config', num_ok, error_list))
        
+25 −33
Original line number Diff line number Diff line
@@ -135,33 +135,25 @@ class FabricConfigBuilder:
                            ))
                    except Exception:
                        continue
            else:
                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 = self._strip_prefix(local_underlays[0])
                        remote_ip = self._strip_prefix(remote_underlays[0])
                    else:
                        local_ip = self._strip_prefix(
                            device.get('underlay_ip') or (local_underlays[0] if local_underlays else None) or lo
                        )
                        remote_ip = self._strip_prefix(
                            leaf.get('underlay_ip') or (remote_underlays[0] if remote_underlays else None) or leaf.get('loopback_ip')
                        )

        elif role in ("leaf", "gateway"):
            sessions = device.get('bgp_sessions', []) or []
            if sessions:
                for s in sessions:
                    try:
                        local_ip = self._strip_prefix(str(s.get('local.address') or s.get('local_address') or ''))
                        remote_ip = self._strip_prefix(str(s.get('remote.address') or s.get('remote_address') or ''))
                        remote_as = s.get('remote.as') or s.get('remote_as') or s.get('remote_asn')
                        name = s.get('name') or s.get('peer') or None
                        if local_ip and remote_ip and remote_as is not None:
                            rules.append(self._bgp_session(
                        name=leaf["device_id"],
                                name=name or f'peer-{remote_ip}',
                                local_ip=local_ip,
                                remote_ip=remote_ip,
                        remote_as=leaf["asn"]
                                remote_as=remote_as,
                            ))

        elif role in ("leaf", "gateway"):
                    except Exception:
                        continue
            else:
                spines = inventory.get("spines", [])
                for spine in spines:
                    local_underlays = device.get('underlay_ips', [])
@@ -178,13 +170,13 @@ class FabricConfigBuilder:
                        )

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

            if not spines:
            if not sessions and not inventory.get("spines", []):
                local_underlay = device.get('underlay_ip') or (device.get('underlay_ips', [None])[0] or '')
                local_ip = self._strip_prefix(local_underlay) or lo
                remote_ip = device.get('bgp_remote_address') or self._derive_p2p_peer(local_underlay)
+11 −1
Original line number Diff line number Diff line
@@ -104,6 +104,10 @@ class SpineLeafServicerImpl(SpineLeafServicer):
        if session_name:
            resources['bgp_session_name'] = session_name

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

        bgp_sessions = hints.get('bgp_sessions', [])
        if isinstance(bgp_sessions, list) and bgp_sessions:
            resources['bgp_sessions'] = bgp_sessions
@@ -229,7 +233,13 @@ class SpineLeafServicerImpl(SpineLeafServicer):
    @safe_and_metered_rpc_method(METRICS_POOL, LOGGER)
    def DeployDeviceConfig(self, request: DeployDeviceConfigRequest, context: grpc.ServicerContext) -> DeployResponse:
        config = self.db.get_config(request.fabric_id, request.device_uuid) or request.config
        driver_config_rules = self.db.get_driver_config_rules(request.fabric_id, request.device_uuid)
        stored_rules = self.db.get_driver_config_rules(request.fabric_id, request.device_uuid)
        fresh_rules = self.db.build_driver_config_rules(request.fabric_id, request.device_uuid)
        merged = list(stored_rules or [])
        for r in fresh_rules:
            if r not in merged:
                merged.append(r)
        driver_config_rules = merged
        payload_json = build_deploy_payload(
            request.device_uuid,
            request.fabric_id,
+2 −0
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ class SpineLeafStateStore:

        self.device_roles[key] = {
            'role': role,
            'device_name': resources.get('device_name'),
            'asn': resources.get('asn'),
            'loopback_ip': resources.get('loopback_ip'),
            'underlay_ip': resources.get('underlay_ip'),  # Primary underlay IP for BGP sessions
@@ -132,6 +133,7 @@ class SpineLeafStateStore:

            device_info = {
                'device_id': device_id,
                'device_name': role_cfg.get('device_name'),
                'role': role_cfg['role'],
                'asn': role_cfg['asn'],
                'loopback_ip': role_cfg['loopback_ip'],
+213 −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
import logging
import os
import copy

from common.proto.context_pb2 import ConfigRule
from common.proto.spineleaf_pb2 import DeployDeviceConfigRequest, UnsetDeviceRoleRequest
from common.tools.context_queries.Device import get_device
from common.tools.object_factory.ConfigRule import (
    json_config_rule_set,
    json_config_rule_delete,
)
from context.client.ContextClient import ContextClient
from device.client.DeviceClient import DeviceClient
from spine_leaf.client.SpineLeafClient import SpineLeafClient

logging.basicConfig(level=logging.INFO)
LOGGER = logging.getLogger("leaf_underlay_apply")


# -----------------------------
# ENV
# -----------------------------
CONTEXTSERVICE_HOST = os.getenv("CONTEXTSERVICE_SERVICE_HOST", "10.152.183.46")
CONTEXTSERVICE_PORT = int(os.getenv("CONTEXTSERVICE_SERVICE_PORT_GRPC", "1010"))

DEVICESERVICE_HOST = os.getenv("DEVICESERVICE_SERVICE_HOST", "10.152.183.102")
DEVICESERVICE_PORT = int(os.getenv("DEVICESERVICE_SERVICE_PORT_GRPC", "2020"))

SPINE_LEAFSERVICE_HOST = os.getenv("SPINE_LEAFSERVICE_SERVICE_HOST", "10.152.183.147")
SPINE_LEAFSERVICE_PORT = int(os.getenv("SPINE_LEAFSERVICE_SERVICE_PORT_GRPC", "10095"))

FABRIC_ID = os.getenv("FABRIC_ID", "354a08b7-3730-5dd6-9472-47554115818b")


# -----------------------------
# LEAF DEVICES ONLY
# -----------------------------
LEAF_DEVICE_UUIDS = [
    "1761a9f5-2672-5996-b3f3-3e91eaef20dc",
    "03d3c087-da28-539e-aae0-60da7b90bad6",
    "6ac9614b-6893-549d-b39c-d4af4320678a",
    "dc0d2a74-3415-5a4c-ac65-0f1ef1ca769b",
]


# -----------------------------
# FLAGS
# -----------------------------
def is_apply_enabled() -> bool:
    return os.getenv("APPLY_CONFIG", "false").lower() in {"1", "true", "yes"}


def is_deconfigure_enabled() -> bool:
    return os.getenv("DECONFIGURE", "false").lower() in {"1", "true", "yes"}


# -----------------------------
# PARSER
# -----------------------------
def parse_rule(entry):
    if not isinstance(entry, (list, tuple)) or len(entry) != 2:
        return None

    key, value = entry

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

    if not isinstance(value, dict):
        return None

    return key, value


# -----------------------------
# APPLY ENGINE
# -----------------------------
class LeafUnderlayApplier:

    def __init__(self):
        self.spine_leaf_client = SpineLeafClient(
            host=SPINE_LEAFSERVICE_HOST,
            port=SPINE_LEAFSERVICE_PORT,
        )

    def run(self):
        context_client = ContextClient(
            host=CONTEXTSERVICE_HOST,
            port=CONTEXTSERVICE_PORT,
        )

        device_client = DeviceClient(
            host=DEVICESERVICE_HOST,
            port=DEVICESERVICE_PORT,
        )

        try:
            for device_uuid in LEAF_DEVICE_UUIDS:
                self.process_device(context_client, device_client, device_uuid)

        finally:
            device_client.close()
            context_client.close()

    def process_device(self, context_client, device_client, device_uuid):

        LOGGER.info("Processing LEAF device: %s", device_uuid)

        request = DeployDeviceConfigRequest(
            fabric_id=FABRIC_ID,
            device_uuid=device_uuid,
        )

        response = self.spine_leaf_client.DeployDeviceConfig(request)

        payload = json.loads(getattr(response, "message", "") or "{}")

        raw_rules = payload.get("driver_config_rules", [])

        set_rules = []
        delete_rules = []

        deconfigure = is_deconfigure_enabled()
        apply = is_apply_enabled() or deconfigure

        for entry in raw_rules:
            parsed = parse_rule(entry)
            if not parsed:
                continue

            key, value = parsed

            set_rules.append(ConfigRule(**json_config_rule_set(key, value)))

            if deconfigure:
                delete_rules.append(ConfigRule(**json_config_rule_delete(key, value)))

        LOGGER.info(
            "Device %s -> rules generated: set=%d delete=%d",
            device_uuid,
            len(set_rules),
            len(delete_rules),
        )

        device = get_device(
            context_client,
            device_uuid,
            rw_copy=True,
            include_endpoints=False,
            include_config_rules=False,
            include_components=False,
        )

        if device is None:
            LOGGER.error("Device not found: %s", device_uuid)
            return

        working_device = copy.deepcopy(device)

        # IMPORTANT: prevent duplication on rerun (clear protobuf repeated field)
        del working_device.device_config.config_rules[:]

        if not apply:
            LOGGER.warning("DRY RUN - no changes applied for %s", device_uuid)
            return

        if deconfigure:
            LOGGER.info("DECONFIGURING LEAF %s", device_uuid)

            working_device.device_config.config_rules.extend(delete_rules)
            result = device_client.ConfigureDevice(working_device)

            LOGGER.info("Deconfig result %s => %s", device_uuid, result)

            self.spine_leaf_client.UnsetDeviceRole(
                UnsetDeviceRoleRequest(
                    fabric_id=FABRIC_ID,
                    device_uuid=device_uuid,
                )
            )

        else:
            LOGGER.info("APPLYING LEAF UNDERLAY %s", device_uuid)

            working_device.device_config.config_rules.extend(set_rules)
            result = device_client.ConfigureDevice(working_device)

            LOGGER.info("Apply result %s => %s", device_uuid, result)


# -----------------------------
# ENTRYPOINT
# -----------------------------
if __name__ == "__main__":
    LeafUnderlayApplier().run()
 No newline at end of file
Loading