Loading src/common/tools/descriptor/Loader.py +121 −4 Original line number Diff line number Diff line Loading @@ -36,12 +36,14 @@ 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 ) 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 Loading Loading @@ -427,6 +429,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: Loading Loading @@ -484,9 +532,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]] = [] Loading Loading @@ -577,6 +644,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: Loading Loading @@ -623,6 +700,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: Loading Loading @@ -652,8 +746,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 Loading Loading @@ -687,11 +794,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)}') Loading src/spine_leaf/scripts/fabric_config_builder.py +359 −150 File changed.Preview size limit exceeded, changes collapsed. Show changes src/spine_leaf/scripts/resource_allocator.py +73 −10 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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: Loading @@ -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() Loading @@ -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'] Loading @@ -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) Loading src/spine_leaf/service/SpineLeafServicerImpl.py +31 −7 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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: Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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( Loading src/spine_leaf/state_store.py +33 −3 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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}' Loading @@ -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, Loading @@ -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', } Loading Loading @@ -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 Loading
src/common/tools/descriptor/Loader.py +121 −4 Original line number Diff line number Diff line Loading @@ -36,12 +36,14 @@ 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 ) 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 Loading Loading @@ -427,6 +429,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: Loading Loading @@ -484,9 +532,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]] = [] Loading Loading @@ -577,6 +644,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: Loading Loading @@ -623,6 +700,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: Loading Loading @@ -652,8 +746,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 Loading Loading @@ -687,11 +794,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)}') Loading
src/spine_leaf/scripts/fabric_config_builder.py +359 −150 File changed.Preview size limit exceeded, changes collapsed. Show changes
src/spine_leaf/scripts/resource_allocator.py +73 −10 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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: Loading @@ -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() Loading @@ -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'] Loading @@ -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) Loading
src/spine_leaf/service/SpineLeafServicerImpl.py +31 −7 Original line number Diff line number Diff line Loading @@ -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) Loading Loading @@ -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: Loading @@ -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( Loading Loading @@ -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( Loading Loading @@ -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( Loading
src/spine_leaf/state_store.py +33 −3 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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}' Loading @@ -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, Loading @@ -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', } Loading Loading @@ -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