From c6dd3898e0cf9746dd8defbd03789333c6638d82 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 22 Jan 2026 18:13:14 +0000 Subject: [PATCH 01/72] NBI component - IETF L2VPN connector: - Added prefix to pre-defined bearers --- src/nbi/service/ietf_l2vpn/Constants.py | 114 +++++++++++++----------- 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/src/nbi/service/ietf_l2vpn/Constants.py b/src/nbi/service/ietf_l2vpn/Constants.py index e7b30ab54..6fd5db9d8 100644 --- a/src/nbi/service/ietf_l2vpn/Constants.py +++ b/src/nbi/service/ietf_l2vpn/Constants.py @@ -17,75 +17,81 @@ DEFAULT_ADDRESS_FAMILIES = ['IPV4'] DEFAULT_BGP_AS = 65000 DEFAULT_BGP_ROUTE_TARGET = '{:d}:{:d}'.format(DEFAULT_BGP_AS, 333) -# TODO: improve definition of bearer mappings +# TODO: improve definition of bearer mappings ; should be configure through +# Logical Resources component whenever the component is available. # Bearer mappings: -# device_uuid:endpoint_uuid => ( +# venue:device_uuid:endpoint_uuid => ( # device_uuid, endpoint_uuid, router_id, route_dist, sub_if_index, -# address_ip, address_prefix, remote_router, circuit_id) +# address_ip, address_prefix, remote_router, circuit_id +# ) BEARER_MAPPINGS = { + # OSM End-to-End Test + 'OSM-E2E:r1:Ethernet10': ('r1', 'Ethernet10', None, None, 0, '172.16.1.1', 24, None, None), + 'OSM-E2E:r3:Ethernet10': ('r3', 'Ethernet10', None, None, 0, '172.16.3.1', 24, None, None), + # OFC'22 - 'R1-EMU:13/1/2': ('R1-EMU', '13/1/2', '10.10.10.1', '65000:100', 400, '3.3.2.1', 24, None, None), - 'R2-EMU:13/1/2': ('R2-EMU', '13/1/2', '12.12.12.1', '65000:120', 450, '3.4.2.1', 24, None, None), - 'R3-EMU:13/1/2': ('R3-EMU', '13/1/2', '20.20.20.1', '65000:200', 500, '3.3.1.1', 24, None, None), - 'R4-EMU:13/1/2': ('R4-EMU', '13/1/2', '22.22.22.1', '65000:220', 550, '3.4.1.1', 24, None, None), + 'OFC22:R1-EMU:13/1/2': ('R1-EMU', '13/1/2', '10.10.10.1', '65000:100', 400, '3.3.2.1', 24, None, None), + 'OFC22:R2-EMU:13/1/2': ('R2-EMU', '13/1/2', '12.12.12.1', '65000:120', 450, '3.4.2.1', 24, None, None), + 'OFC22:R3-EMU:13/1/2': ('R3-EMU', '13/1/2', '20.20.20.1', '65000:200', 500, '3.3.1.1', 24, None, None), + 'OFC22:R4-EMU:13/1/2': ('R4-EMU', '13/1/2', '22.22.22.1', '65000:220', 550, '3.4.1.1', 24, None, None), # OECC/PSC'22 - domain 1 - 'R1@D1:3/1' : ('R1@D1', '3/1', '10.0.1.1', '65001:101', 100, '1.1.3.1', 24, None, None), - 'R1@D1:3/2' : ('R1@D1', '3/2', '10.0.1.1', '65001:101', 100, '1.1.3.2', 24, None, None), - 'R1@D1:3/3' : ('R1@D1', '3/3', '10.0.1.1', '65001:101', 100, '1.1.3.3', 24, None, None), - 'R2@D1:3/1' : ('R2@D1', '3/1', '10.0.1.2', '65001:102', 100, '1.2.3.1', 24, None, None), - 'R2@D1:3/2' : ('R2@D1', '3/2', '10.0.1.2', '65001:102', 100, '1.2.3.2', 24, None, None), - 'R2@D1:3/3' : ('R2@D1', '3/3', '10.0.1.2', '65001:102', 100, '1.2.3.3', 24, None, None), - 'R3@D1:3/1' : ('R3@D1', '3/1', '10.0.1.3', '65001:103', 100, '1.3.3.1', 24, None, None), - 'R3@D1:3/2' : ('R3@D1', '3/2', '10.0.1.3', '65001:103', 100, '1.3.3.2', 24, None, None), - 'R3@D1:3/3' : ('R3@D1', '3/3', '10.0.1.3', '65001:103', 100, '1.3.3.3', 24, None, None), - 'R4@D1:3/1' : ('R4@D1', '3/1', '10.0.1.4', '65001:104', 100, '1.4.3.1', 24, None, None), - 'R4@D1:3/2' : ('R4@D1', '3/2', '10.0.1.4', '65001:104', 100, '1.4.3.2', 24, None, None), - 'R4@D1:3/3' : ('R4@D1', '3/3', '10.0.1.4', '65001:104', 100, '1.4.3.3', 24, None, None), + 'OECCPSC22D1:R1@D1:3/1' : ('R1@D1', '3/1', '10.0.1.1', '65001:101', 100, '1.1.3.1', 24, None, None), + 'OECCPSC22D1:R1@D1:3/2' : ('R1@D1', '3/2', '10.0.1.1', '65001:101', 100, '1.1.3.2', 24, None, None), + 'OECCPSC22D1:R1@D1:3/3' : ('R1@D1', '3/3', '10.0.1.1', '65001:101', 100, '1.1.3.3', 24, None, None), + 'OECCPSC22D1:R2@D1:3/1' : ('R2@D1', '3/1', '10.0.1.2', '65001:102', 100, '1.2.3.1', 24, None, None), + 'OECCPSC22D1:R2@D1:3/2' : ('R2@D1', '3/2', '10.0.1.2', '65001:102', 100, '1.2.3.2', 24, None, None), + 'OECCPSC22D1:R2@D1:3/3' : ('R2@D1', '3/3', '10.0.1.2', '65001:102', 100, '1.2.3.3', 24, None, None), + 'OECCPSC22D1:R3@D1:3/1' : ('R3@D1', '3/1', '10.0.1.3', '65001:103', 100, '1.3.3.1', 24, None, None), + 'OECCPSC22D1:R3@D1:3/2' : ('R3@D1', '3/2', '10.0.1.3', '65001:103', 100, '1.3.3.2', 24, None, None), + 'OECCPSC22D1:R3@D1:3/3' : ('R3@D1', '3/3', '10.0.1.3', '65001:103', 100, '1.3.3.3', 24, None, None), + 'OECCPSC22D1:R4@D1:3/1' : ('R4@D1', '3/1', '10.0.1.4', '65001:104', 100, '1.4.3.1', 24, None, None), + 'OECCPSC22D1:R4@D1:3/2' : ('R4@D1', '3/2', '10.0.1.4', '65001:104', 100, '1.4.3.2', 24, None, None), + 'OECCPSC22D1:R4@D1:3/3' : ('R4@D1', '3/3', '10.0.1.4', '65001:104', 100, '1.4.3.3', 24, None, None), # OECC/PSC'22 - domain 2 - 'R1@D2:3/1' : ('R1@D2', '3/1', '10.0.2.1', '65002:101', 100, '2.1.3.1', 24, None, None), - 'R1@D2:3/2' : ('R1@D2', '3/2', '10.0.2.1', '65002:101', 100, '2.1.3.2', 24, None, None), - 'R1@D2:3/3' : ('R1@D2', '3/3', '10.0.2.1', '65002:101', 100, '2.1.3.3', 24, None, None), - 'R2@D2:3/1' : ('R2@D2', '3/1', '10.0.2.2', '65002:102', 100, '2.2.3.1', 24, None, None), - 'R2@D2:3/2' : ('R2@D2', '3/2', '10.0.2.2', '65002:102', 100, '2.2.3.2', 24, None, None), - 'R2@D2:3/3' : ('R2@D2', '3/3', '10.0.2.2', '65002:102', 100, '2.2.3.3', 24, None, None), - 'R3@D2:3/1' : ('R3@D2', '3/1', '10.0.2.3', '65002:103', 100, '2.3.3.1', 24, None, None), - 'R3@D2:3/2' : ('R3@D2', '3/2', '10.0.2.3', '65002:103', 100, '2.3.3.2', 24, None, None), - 'R3@D2:3/3' : ('R3@D2', '3/3', '10.0.2.3', '65002:103', 100, '2.3.3.3', 24, None, None), - 'R4@D2:3/1' : ('R4@D2', '3/1', '10.0.2.4', '65002:104', 100, '2.4.3.1', 24, None, None), - 'R4@D2:3/2' : ('R4@D2', '3/2', '10.0.2.4', '65002:104', 100, '2.4.3.2', 24, None, None), - 'R4@D2:3/3' : ('R4@D2', '3/3', '10.0.2.4', '65002:104', 100, '2.4.3.3', 24, None, None), + 'OECCPSC22D1:R1@D2:3/1' : ('R1@D2', '3/1', '10.0.2.1', '65002:101', 100, '2.1.3.1', 24, None, None), + 'OECCPSC22D1:R1@D2:3/2' : ('R1@D2', '3/2', '10.0.2.1', '65002:101', 100, '2.1.3.2', 24, None, None), + 'OECCPSC22D1:R1@D2:3/3' : ('R1@D2', '3/3', '10.0.2.1', '65002:101', 100, '2.1.3.3', 24, None, None), + 'OECCPSC22D1:R2@D2:3/1' : ('R2@D2', '3/1', '10.0.2.2', '65002:102', 100, '2.2.3.1', 24, None, None), + 'OECCPSC22D1:R2@D2:3/2' : ('R2@D2', '3/2', '10.0.2.2', '65002:102', 100, '2.2.3.2', 24, None, None), + 'OECCPSC22D1:R2@D2:3/3' : ('R2@D2', '3/3', '10.0.2.2', '65002:102', 100, '2.2.3.3', 24, None, None), + 'OECCPSC22D1:R3@D2:3/1' : ('R3@D2', '3/1', '10.0.2.3', '65002:103', 100, '2.3.3.1', 24, None, None), + 'OECCPSC22D1:R3@D2:3/2' : ('R3@D2', '3/2', '10.0.2.3', '65002:103', 100, '2.3.3.2', 24, None, None), + 'OECCPSC22D1:R3@D2:3/3' : ('R3@D2', '3/3', '10.0.2.3', '65002:103', 100, '2.3.3.3', 24, None, None), + 'OECCPSC22D1:R4@D2:3/1' : ('R4@D2', '3/1', '10.0.2.4', '65002:104', 100, '2.4.3.1', 24, None, None), + 'OECCPSC22D1:R4@D2:3/2' : ('R4@D2', '3/2', '10.0.2.4', '65002:104', 100, '2.4.3.2', 24, None, None), + 'OECCPSC22D1:R4@D2:3/3' : ('R4@D2', '3/3', '10.0.2.4', '65002:104', 100, '2.4.3.3', 24, None, None), # ECOC'22 - 'DC1-GW:CS1-GW1': ('CS1-GW1', '10/1', '5.5.1.1', None, 0, None, None, '5.5.2.1', 111), - 'DC1-GW:CS1-GW2': ('CS1-GW2', '10/1', '5.5.1.2', None, 0, None, None, '5.5.2.2', 222), - 'DC2-GW:CS2-GW1': ('CS2-GW1', '10/1', '5.5.2.1', None, 0, None, None, '5.5.1.1', 111), - 'DC2-GW:CS2-GW2': ('CS2-GW2', '10/1', '5.5.2.2', None, 0, None, None, '5.5.1.2', 222), + 'ECOC22:DC1-GW:CS1-GW1': ('CS1-GW1', '10/1', '5.5.1.1', None, 0, None, None, '5.5.2.1', 111), + 'ECOC22:DC1-GW:CS1-GW2': ('CS1-GW2', '10/1', '5.5.1.2', None, 0, None, None, '5.5.2.2', 222), + 'ECOC22:DC2-GW:CS2-GW1': ('CS2-GW1', '10/1', '5.5.2.1', None, 0, None, None, '5.5.1.1', 111), + 'ECOC22:DC2-GW:CS2-GW2': ('CS2-GW2', '10/1', '5.5.2.2', None, 0, None, None, '5.5.1.2', 222), # NetworkX'22 - 'R1:1/2': ('R1', '1/2', '5.1.1.2', None, 0, None, None, None, None), - 'R1:1/3': ('R1', '1/3', '5.1.1.3', None, 0, None, None, None, None), - 'R2:1/2': ('R2', '1/2', '5.2.1.2', None, 0, None, None, None, None), - 'R2:1/3': ('R2', '1/3', '5.2.1.3', None, 0, None, None, None, None), - 'R3:1/2': ('R3', '1/2', '5.3.1.2', None, 0, None, None, None, None), - 'R3:1/3': ('R3', '1/3', '5.3.1.3', None, 0, None, None, None, None), - 'R4:1/2': ('R4', '1/2', '5.4.1.2', None, 0, None, None, None, None), - 'R4:1/3': ('R4', '1/3', '5.4.1.3', None, 0, None, None, None, None), + 'NETX22:R1:1/2': ('R1', '1/2', '5.1.1.2', None, 0, None, None, None, None), + 'NETX22:R1:1/3': ('R1', '1/3', '5.1.1.3', None, 0, None, None, None, None), + 'NETX22:R2:1/2': ('R2', '1/2', '5.2.1.2', None, 0, None, None, None, None), + 'NETX22:R2:1/3': ('R2', '1/3', '5.2.1.3', None, 0, None, None, None, None), + 'NETX22:R3:1/2': ('R3', '1/2', '5.3.1.2', None, 0, None, None, None, None), + 'NETX22:R3:1/3': ('R3', '1/3', '5.3.1.3', None, 0, None, None, None, None), + 'NETX22:R4:1/2': ('R4', '1/2', '5.4.1.2', None, 0, None, None, None, None), + 'NETX22:R4:1/3': ('R4', '1/3', '5.4.1.3', None, 0, None, None, None, None), # OFC'23 - 'PE1:1/1': ('PE1', '1/1', '10.1.1.1', None, 0, None, None, None, None), - 'PE1:1/2': ('PE1', '1/2', '10.1.1.2', None, 0, None, None, None, None), - 'PE2:1/1': ('PE2', '1/1', '10.2.1.1', None, 0, None, None, None, None), - 'PE2:1/2': ('PE2', '1/2', '10.2.1.2', None, 0, None, None, None, None), - 'PE3:1/1': ('PE3', '1/1', '10.3.1.1', None, 0, None, None, None, None), - 'PE3:1/2': ('PE3', '1/2', '10.3.1.2', None, 0, None, None, None, None), - 'PE4:1/1': ('PE4', '1/1', '10.4.1.1', None, 0, None, None, None, None), - 'PE4:1/2': ('PE4', '1/2', '10.4.1.2', None, 0, None, None, None, None), + 'OFC23:PE1:1/1': ('PE1', '1/1', '10.1.1.1', None, 0, None, None, None, None), + 'OFC23:PE1:1/2': ('PE1', '1/2', '10.1.1.2', None, 0, None, None, None, None), + 'OFC23:PE2:1/1': ('PE2', '1/1', '10.2.1.1', None, 0, None, None, None, None), + 'OFC23:PE2:1/2': ('PE2', '1/2', '10.2.1.2', None, 0, None, None, None, None), + 'OFC23:PE3:1/1': ('PE3', '1/1', '10.3.1.1', None, 0, None, None, None, None), + 'OFC23:PE3:1/2': ('PE3', '1/2', '10.3.1.2', None, 0, None, None, None, None), + 'OFC23:PE4:1/1': ('PE4', '1/1', '10.4.1.1', None, 0, None, None, None, None), + 'OFC23:PE4:1/2': ('PE4', '1/2', '10.4.1.2', None, 0, None, None, None, None), - 'R149:eth-1/0/22': ('R149', 'eth-1/0/22', '5.5.5.5', None, 0, None, None, '5.5.5.1', '100'), - 'R155:eth-1/0/22': ('R155', 'eth-1/0/22', '5.5.5.1', None, 0, None, None, '5.5.5.5', '100'), - 'R199:eth-1/0/21': ('R199', 'eth-1/0/21', '5.5.5.6', None, 0, None, None, '5.5.5.5', '100'), + 'OFC23:R149:eth-1/0/22': ('R149', 'eth-1/0/22', '5.5.5.5', None, 0, None, None, '5.5.5.1', '100'), + 'OFC23:R155:eth-1/0/22': ('R155', 'eth-1/0/22', '5.5.5.1', None, 0, None, None, '5.5.5.5', '100'), + 'OFC23:R199:eth-1/0/21': ('R199', 'eth-1/0/21', '5.5.5.6', None, 0, None, None, '5.5.5.5', '100'), } -- GitLab From 86a17a52c65b1b3d031734e9f206e91bc05bd8ea Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 22 Jan 2026 18:23:35 +0000 Subject: [PATCH 02/72] End-to-End Tests using IETF L2VPN (OFC'22, ECOC'22, OECC/PSC'22): - Fixed bearer prefix --- src/tests/ecoc22/tests/Objects.py | 20 ++++++++++++++++---- src/tests/oeccpsc22/tests/Objects_Service.py | 4 ++-- src/tests/oeccpsc22/tests/Tools.py | 4 ++-- src/tests/ofc22/tests/Objects.py | 4 ++-- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/tests/ecoc22/tests/Objects.py b/src/tests/ecoc22/tests/Objects.py index 8d4874ea7..77a4a3f60 100644 --- a/src/tests/ecoc22/tests/Objects.py +++ b/src/tests/ecoc22/tests/Objects.py @@ -33,10 +33,22 @@ EP_ID_DC2_BKP = json_endpoint_id(DEV_ID_DC2, 'eth2') DEV_ID_CS2GW1 = json_device_id('CS2-GW1') DEV_ID_CS2GW2 = json_device_id('CS2-GW2') -WIM_SEP_DC1_PRI, WIM_MAP_DC1_PRI = wim_mapping(SITE_ID_DC1, EP_ID_DC1_PRI, DEV_ID_CS1GW1, priority=10, redundant=['DC1:DC1-GW:eth2']) -WIM_SEP_DC1_BKP, WIM_MAP_DC1_BKP = wim_mapping(SITE_ID_DC1, EP_ID_DC1_BKP, DEV_ID_CS1GW2, priority=20, redundant=['DC1:DC1-GW:eth1']) -WIM_SEP_DC2_PRI, WIM_MAP_DC2_PRI = wim_mapping(SITE_ID_DC2, EP_ID_DC2_PRI, DEV_ID_CS2GW1, priority=10, redundant=['DC2:DC2-GW:eth2']) -WIM_SEP_DC2_BKP, WIM_MAP_DC2_BKP = wim_mapping(SITE_ID_DC2, EP_ID_DC2_BKP, DEV_ID_CS2GW2, priority=20, redundant=['DC2:DC2-GW:eth1']) +WIM_SEP_DC1_PRI, WIM_MAP_DC1_PRI = wim_mapping( + SITE_ID_DC1, EP_ID_DC1_PRI, pe_device_id=DEV_ID_CS1GW1, + bearer_prefix='ECOC22', priority=10, redundant=['DC1:DC1-GW:eth2'] +) +WIM_SEP_DC1_BKP, WIM_MAP_DC1_BKP = wim_mapping( + SITE_ID_DC1, EP_ID_DC1_BKP, pe_device_id=DEV_ID_CS1GW2, + bearer_prefix='ECOC22', priority=20, redundant=['DC1:DC1-GW:eth1'] +) +WIM_SEP_DC2_PRI, WIM_MAP_DC2_PRI = wim_mapping( + SITE_ID_DC2, EP_ID_DC2_PRI, pe_device_id=DEV_ID_CS2GW1, + bearer_prefix='ECOC22', priority=10, redundant=['DC2:DC2-GW:eth2'] +) +WIM_SEP_DC2_BKP, WIM_MAP_DC2_BKP = wim_mapping( + SITE_ID_DC2, EP_ID_DC2_BKP, pe_device_id=DEV_ID_CS2GW2, + bearer_prefix='ECOC22', priority=20, redundant=['DC2:DC2-GW:eth1'] +) WIM_MAPPING = [ WIM_MAP_DC1_PRI, WIM_MAP_DC1_BKP, diff --git a/src/tests/oeccpsc22/tests/Objects_Service.py b/src/tests/oeccpsc22/tests/Objects_Service.py index 3440c5515..ad76422c6 100644 --- a/src/tests/oeccpsc22/tests/Objects_Service.py +++ b/src/tests/oeccpsc22/tests/Objects_Service.py @@ -21,14 +21,14 @@ WIM_SEP_D1R1_ID = compose_service_endpoint_id(D1_ENDPOINT_IDS[D1_DEVICE WIM_SEP_D1R1_ROUTER_ID = '10.10.10.1' WIM_SEP_D1R1_ROUTER_DIST = '65000:111' WIM_SEP_D1R1_SITE_ID = '1' -WIM_SEP_D1R1_BEARER = compose_bearer(D1_ENDPOINT_IDS[D1_DEVICE_D1R1_UUID]['3/1']) +WIM_SEP_D1R1_BEARER = compose_bearer(D1_ENDPOINT_IDS[D1_DEVICE_D1R1_UUID]['3/1'], 'OECCPSC22D1') WIM_SRV_D1R1_VLAN_ID = 400 WIM_SEP_D2R4_ID = compose_service_endpoint_id(D2_ENDPOINT_IDS[D2_DEVICE_D2R4_UUID]['3/3']) WIM_SEP_D2R4_ROUTER_ID = '20.20.20.1' WIM_SEP_D2R4_ROUTER_DIST = '65000:222' WIM_SEP_D2R4_SITE_ID = '2' -WIM_SEP_D2R4_BEARER = compose_bearer(D2_ENDPOINT_IDS[D2_DEVICE_D2R4_UUID]['3/3']) +WIM_SEP_D2R4_BEARER = compose_bearer(D2_ENDPOINT_IDS[D2_DEVICE_D2R4_UUID]['3/3'], 'OECCPSC22D2') WIM_SRV_D2R4_VLAN_ID = 500 WIM_USERNAME = 'admin' diff --git a/src/tests/oeccpsc22/tests/Tools.py b/src/tests/oeccpsc22/tests/Tools.py index 8e97fa403..79ec44f9f 100644 --- a/src/tests/oeccpsc22/tests/Tools.py +++ b/src/tests/oeccpsc22/tests/Tools.py @@ -33,7 +33,7 @@ def compose_service_endpoint_id(endpoint_id): endpoint_uuid = endpoint_id['endpoint_uuid']['uuid'] return ':'.join([device_uuid, endpoint_uuid]) -def compose_bearer(endpoint_id): +def compose_bearer(endpoint_id : Dict, bearer_prefix : str): device_uuid = endpoint_id['device_id']['device_uuid']['uuid'] endpoint_uuid = endpoint_id['endpoint_uuid']['uuid'] - return ':'.join([device_uuid, endpoint_uuid]) + return ':'.join([bearer_prefix, device_uuid, endpoint_uuid]) diff --git a/src/tests/ofc22/tests/Objects.py b/src/tests/ofc22/tests/Objects.py index a2a68a174..53eb67b2a 100644 --- a/src/tests/ofc22/tests/Objects.py +++ b/src/tests/ofc22/tests/Objects.py @@ -26,8 +26,8 @@ SITE_ID_DC2 = '2' DEV_ID_DC2 = json_device_id('R3-EMU') EP_ID_DC2 = json_endpoint_id(DEV_ID_DC2, '13/1/2') -WIM_SEP_DC1, WIM_MAP_DC1 = wim_mapping(SITE_ID_DC1, EP_ID_DC1) -WIM_SEP_DC2, WIM_MAP_DC2 = wim_mapping(SITE_ID_DC2, EP_ID_DC2) +WIM_SEP_DC1, WIM_MAP_DC1 = wim_mapping(SITE_ID_DC1, EP_ID_DC1, bearer_prefix='OFC22') +WIM_SEP_DC2, WIM_MAP_DC2 = wim_mapping(SITE_ID_DC2, EP_ID_DC2, bearer_prefix='OFC22') WIM_MAPPING = [ WIM_MAP_DC1, -- GitLab From 72cf0dc02965de421d371265a79f6753778d8e55 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 22 Jan 2026 18:35:19 +0000 Subject: [PATCH 03/72] OSM End-to-End integration test: - Initial implementation --- src/tests/.gitlab-ci.yml | 1 + src/tests/osm_end2end/.gitignore | 19 + src/tests/osm_end2end/.gitlab-ci.yml | 300 ++++++++++ src/tests/osm_end2end/Dockerfile | 86 +++ src/tests/osm_end2end/README.md | 130 +++++ src/tests/osm_end2end/__init__.py | 14 + .../osm_end2end/clab/osm_end2end.clab.yml | 82 +++ src/tests/osm_end2end/clab/r1-startup.cfg | 48 ++ src/tests/osm_end2end/clab/r2-startup.cfg | 48 ++ src/tests/osm_end2end/clab/r3-startup.cfg | 48 ++ src/tests/osm_end2end/data/tfs-topology.json | 126 ++++ .../deploy-scripts/clab-cli-dc1.sh | 16 + .../deploy-scripts/clab-cli-dc2.sh | 16 + .../osm_end2end/deploy-scripts/clab-cli-r1.sh | 16 + .../osm_end2end/deploy-scripts/clab-cli-r2.sh | 16 + .../osm_end2end/deploy-scripts/clab-cli-r3.sh | 16 + .../osm_end2end/deploy-scripts/clab-deploy.sh | 17 + .../deploy-scripts/clab-destroy.sh | 18 + .../deploy-scripts/clab-inspect.sh | 17 + src/tests/osm_end2end/deploy_specs.sh | 208 +++++++ src/tests/osm_end2end/redeploy-tfs.sh | 17 + src/tests/osm_end2end/requirements.in | 15 + src/tests/osm_end2end/scripts/run-cleanup.sh | 20 + .../osm_end2end/scripts/run-onboarding.sh | 20 + .../scripts/run-osm-service-create.sh | 20 + .../scripts/run-osm-service-remove.sh | 20 + src/tests/osm_end2end/tests/Fixtures.py | 51 ++ src/tests/osm_end2end/tests/MockOSM.py | 62 ++ src/tests/osm_end2end/tests/OSM_Constants.py | 53 ++ .../tests/WimconnectorIETFL2VPN.py | 545 ++++++++++++++++++ src/tests/osm_end2end/tests/__init__.py | 14 + .../osm_end2end/tests/acknowledgements.txt | 3 + src/tests/osm_end2end/tests/sdnconn.py | 242 ++++++++ src/tests/osm_end2end/tests/test_cleanup.py | 44 ++ .../osm_end2end/tests/test_onboarding.py | 67 +++ .../tests/test_osm_service_create.py | 77 +++ .../tests/test_osm_service_remove.py | 84 +++ 37 files changed, 2596 insertions(+) create mode 100644 src/tests/osm_end2end/.gitignore create mode 100644 src/tests/osm_end2end/.gitlab-ci.yml create mode 100644 src/tests/osm_end2end/Dockerfile create mode 100644 src/tests/osm_end2end/README.md create mode 100644 src/tests/osm_end2end/__init__.py create mode 100644 src/tests/osm_end2end/clab/osm_end2end.clab.yml create mode 100644 src/tests/osm_end2end/clab/r1-startup.cfg create mode 100644 src/tests/osm_end2end/clab/r2-startup.cfg create mode 100644 src/tests/osm_end2end/clab/r3-startup.cfg create mode 100644 src/tests/osm_end2end/data/tfs-topology.json create mode 100755 src/tests/osm_end2end/deploy-scripts/clab-cli-dc1.sh create mode 100755 src/tests/osm_end2end/deploy-scripts/clab-cli-dc2.sh create mode 100755 src/tests/osm_end2end/deploy-scripts/clab-cli-r1.sh create mode 100755 src/tests/osm_end2end/deploy-scripts/clab-cli-r2.sh create mode 100755 src/tests/osm_end2end/deploy-scripts/clab-cli-r3.sh create mode 100755 src/tests/osm_end2end/deploy-scripts/clab-deploy.sh create mode 100755 src/tests/osm_end2end/deploy-scripts/clab-destroy.sh create mode 100755 src/tests/osm_end2end/deploy-scripts/clab-inspect.sh create mode 100755 src/tests/osm_end2end/deploy_specs.sh create mode 100755 src/tests/osm_end2end/redeploy-tfs.sh create mode 100644 src/tests/osm_end2end/requirements.in create mode 100755 src/tests/osm_end2end/scripts/run-cleanup.sh create mode 100755 src/tests/osm_end2end/scripts/run-onboarding.sh create mode 100755 src/tests/osm_end2end/scripts/run-osm-service-create.sh create mode 100755 src/tests/osm_end2end/scripts/run-osm-service-remove.sh create mode 100644 src/tests/osm_end2end/tests/Fixtures.py create mode 100644 src/tests/osm_end2end/tests/MockOSM.py create mode 100644 src/tests/osm_end2end/tests/OSM_Constants.py create mode 100644 src/tests/osm_end2end/tests/WimconnectorIETFL2VPN.py create mode 100644 src/tests/osm_end2end/tests/__init__.py create mode 100644 src/tests/osm_end2end/tests/acknowledgements.txt create mode 100644 src/tests/osm_end2end/tests/sdnconn.py create mode 100644 src/tests/osm_end2end/tests/test_cleanup.py create mode 100644 src/tests/osm_end2end/tests/test_onboarding.py create mode 100644 src/tests/osm_end2end/tests/test_osm_service_create.py create mode 100644 src/tests/osm_end2end/tests/test_osm_service_remove.py diff --git a/src/tests/.gitlab-ci.yml b/src/tests/.gitlab-ci.yml index 287d698c4..5e7201ca6 100644 --- a/src/tests/.gitlab-ci.yml +++ b/src/tests/.gitlab-ci.yml @@ -14,6 +14,7 @@ # include the individual .gitlab-ci.yml of each end-to-end integration test include: + - local: '/src/tests/osm_end2end/.gitlab-ci.yml' - local: '/src/tests/ofc22/.gitlab-ci.yml' #- local: '/src/tests/oeccpsc22/.gitlab-ci.yml' - local: '/src/tests/ecoc22/.gitlab-ci.yml' diff --git a/src/tests/osm_end2end/.gitignore b/src/tests/osm_end2end/.gitignore new file mode 100644 index 000000000..a47dc9eff --- /dev/null +++ b/src/tests/osm_end2end/.gitignore @@ -0,0 +1,19 @@ +# 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. + +clab-*/ +images/ +*.clab.yml.bak +*.tar +*.tar.gz diff --git a/src/tests/osm_end2end/.gitlab-ci.yml b/src/tests/osm_end2end/.gitlab-ci.yml new file mode 100644 index 000000000..43b6f2fcb --- /dev/null +++ b/src/tests/osm_end2end/.gitlab-ci.yml @@ -0,0 +1,300 @@ +# 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. + +# Build, tag, and push the Docker image to the GitLab Docker registry +build osm_end2end: + variables: + TEST_NAME: 'osm_end2end' + stage: build + before_script: + - docker image prune --force + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + script: + - docker buildx build -t "${TEST_NAME}:latest" -f ./src/tests/${TEST_NAME}/Dockerfile . + - docker tag "${TEST_NAME}:latest" "$CI_REGISTRY_IMAGE/${TEST_NAME}:latest" + - docker push "$CI_REGISTRY_IMAGE/${TEST_NAME}:latest" + after_script: + - docker image prune --force + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' + - changes: + - src/common/**/*.py + - proto/*.proto + - src/tests/${TEST_NAME}/**/*.{py,in,sh,yml} + - src/tests/${TEST_NAME}/Dockerfile + - .gitlab-ci.yml + +# Deploy TeraFlowSDN and Execute end-2-end test +end2end_test osm_end2end: + timeout: 45m + variables: + TEST_NAME: 'osm_end2end' + stage: end2end_test + # Disable to force running it after all other tasks + #needs: + # - build osm_end2end + before_script: + # Cleanup old ContainerLab scenarios + - containerlab destroy --all --cleanup || true + + # Do Docker cleanup + - docker ps --all --quiet | xargs --no-run-if-empty docker stop + - docker container prune --force + - docker ps --all --quiet | xargs --no-run-if-empty docker rm --force + - docker image prune --force + - docker network prune --force + - docker volume prune --all --force + - docker buildx prune --force + + # Check MicroK8s is ready + - microk8s status --wait-ready + - LOOP_MAX_ATTEMPTS=10 + - LOOP_COUNTER=0 + - > + while ! kubectl get pods --all-namespaces &> /dev/null; do + printf "%c" "." + sleep 1 + LOOP_COUNTER=$((LOOP_COUNTER + 1)) + if [ "$LOOP_COUNTER" -ge "$LOOP_MAX_ATTEMPTS" ]; then + echo "Max attempts reached, exiting the loop." + exit 1 + fi + done + - kubectl get pods --all-namespaces + + # Always delete Kubernetes namespaces + - export K8S_NAMESPACES=$(kubectl get namespace -o jsonpath='{.items[*].metadata.name}') + - echo "K8S_NAMESPACES=${K8S_NAMESPACES}" + + - export OLD_NATS_NAMESPACES=$(echo "${K8S_NAMESPACES}" | tr ' ' '\n' | grep -E '^nats') + - echo "OLD_NATS_NAMESPACES=${OLD_NATS_NAMESPACES}" + - > + for ns in ${OLD_NATS_NAMESPACES}; do + if [[ "$ns" == nats* ]]; then + if helm3 status "$ns" &>/dev/null; then + helm3 uninstall "$ns" -n "$ns" + else + echo "Release '$ns' not found, skipping..." + fi + fi + done + - export OLD_NAMESPACES=$(echo "${K8S_NAMESPACES}" | tr ' ' '\n' | grep -E '^(tfs|crdb|qdb|kafka|nats)') + - echo "OLD_NAMESPACES=${OLD_NAMESPACES}" + - kubectl delete namespace ${OLD_NAMESPACES} || true + + # Clean-up Kubernetes Failed pods + - > + kubectl get pods --all-namespaces --no-headers --field-selector=status.phase=Failed + -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name | + xargs --no-run-if-empty --max-args=2 kubectl delete pod --namespace + + # Login Docker repository + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + + script: + # Download Docker image to run the test + - docker pull "${CI_REGISTRY_IMAGE}/${TEST_NAME}:latest" + + # Check MicroK8s is ready + - microk8s status --wait-ready + - LOOP_MAX_ATTEMPTS=10 + - LOOP_COUNTER=0 + - > + while ! kubectl get pods --all-namespaces &> /dev/null; do + printf "%c" "." + sleep 1 + LOOP_COUNTER=$((LOOP_COUNTER + 1)) + if [ "$LOOP_COUNTER" -ge "$LOOP_MAX_ATTEMPTS" ]; then + echo "Max attempts reached, exiting the loop." + exit 1 + fi + done + - kubectl get pods --all-namespaces + + # Deploy ContainerLab Scenario + - RUNNER_PATH=`pwd` + #- cd $PWD/src/tests/${TEST_NAME} + - mkdir -p /tmp/clab/${TEST_NAME} + - cp -R src/tests/${TEST_NAME}/clab/* /tmp/clab/${TEST_NAME} + - tree -la /tmp/clab/${TEST_NAME} + - cd /tmp/clab/${TEST_NAME} + - containerlab deploy --reconfigure --topo ${TEST_NAME}.clab.yml + - cd $RUNNER_PATH + + # Wait for initialization of Device NOSes + - sleep 3 + - docker ps -a + + # Dump configuration of the routers (before any configuration) + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r1 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r2 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r3 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + + # Configure TeraFlowSDN deployment + # Uncomment if DEBUG log level is needed for the components + #- yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="server").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/contextservice.yaml + - yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="server").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/deviceservice.yaml + #- yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="frontend").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/pathcompservice.yaml + - yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="server").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/serviceservice.yaml + - yq -i '((select(.kind=="Deployment").spec.template.spec.containers.[] | select(.name=="server").env.[]) | select(.name=="LOG_LEVEL").value) |= "DEBUG"' manifests/nbiservice.yaml + + - source src/tests/${TEST_NAME}/deploy_specs.sh + #- export TFS_REGISTRY_IMAGES="${CI_REGISTRY_IMAGE}" + #- export TFS_SKIP_BUILD="YES" + #- export TFS_IMAGE_TAG="latest" + #- echo "TFS_REGISTRY_IMAGES=${CI_REGISTRY_IMAGE}" + + # Deploy TeraFlowSDN + - ./deploy/crdb.sh + - ./deploy/nats.sh + - ./deploy/kafka.sh + #- ./deploy/qdb.sh + - ./deploy/tfs.sh + - ./deploy/show.sh + + ## Wait for Context to be subscribed to NATS + ## WARNING: this loop is infinite if there is no subscriber (such as monitoring). + ## Investigate if we can use a counter to limit the number of iterations. + ## For now, keep it commented out. + #- LOOP_MAX_ATTEMPTS=180 + #- LOOP_COUNTER=0 + #- > + # while ! kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/contextservice -c server 2>&1 | grep -q 'Subscriber is Ready? True'; do + # echo "Attempt: $LOOP_COUNTER" + # kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/contextservice -c server 2>&1; + # sleep 1; + # LOOP_COUNTER=$((LOOP_COUNTER + 1)) + # if [ "$LOOP_COUNTER" -ge "$LOOP_MAX_ATTEMPTS" ]; then + # echo "Max attempts reached, exiting the loop." + # break + # fi + # done + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/contextservice -c server + + - | + ping_check() { + local SRC=$1 DST_IP=$2 PATTERN=$3 + local OUTPUT + OUTPUT=$(containerlab exec --name ${TEST_NAME} --label clab-node-name=${SRC} --cmd "ping -n -c3 ${DST_IP}" --format json) + echo "$OUTPUT" + if echo "$OUTPUT" | grep -E "$PATTERN" >/dev/null; then + echo "PASSED ${SRC}->${DST_IP} + else + echo "FAILED ${SRC}->${DST_IP} + fi + echo "$OUTPUT" | grep -E "$PATTERN" + } + + # Run end-to-end test: onboard scenario + - > + docker run -t --rm --name ${TEST_NAME} --network=host + --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh" + --volume "$PWD/src/tests/${TEST_NAME}:/opt/results" + $CI_REGISTRY_IMAGE/${TEST_NAME}:latest /var/teraflow/run-onboarding.sh + + # Dump configuration of the routers (after configure TFS service) + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r1 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r2 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r3 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + + # Run end-to-end test: test no connectivity with ping + - ping_check "dc1" "172.16.1.10" "3 packets transmitted, 3 received, 0% packet loss" + - ping_check "dc1" "172.16.1.20" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + - ping_check "dc1" "172.16.1.30" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + + # Run end-to-end test: configure OSM service + - > + docker run -t --rm --name ${TEST_NAME} --network=host + --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh" + --volume "$PWD/src/tests/${TEST_NAME}:/opt/results" + $CI_REGISTRY_IMAGE/${TEST_NAME}:latest /var/teraflow/run-osm-service-create.sh + + # Give time to routers for being configured and stabilized + - sleep 60 + + # Dump configuration of the routers (after configure OSM service) + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r1 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r2 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r3 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + + # Run end-to-end test: test connectivity with ping + - ping_check "dc1" "172.16.1.10" "3 packets transmitted, 3 received, 0% packet loss" + - ping_check "dc1" "172.16.1.20" "3 packets transmitted, 3 received, 0% packet loss" + - ping_check "dc1" "172.16.1.30" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + + # Run end-to-end test: deconfigure OSM service + - > + docker run -t --rm --name ${TEST_NAME} --network=host + --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh" + --volume "$PWD/src/tests/${TEST_NAME}:/opt/results" + $CI_REGISTRY_IMAGE/${TEST_NAME}:latest /var/teraflow/run-osm-service-remove.sh + + # Give time to routers for being configured and stabilized + - sleep 60 + + # Dump configuration of the routers (after deconfigure OSM service) + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r1 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r2 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r3 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + + # Run end-to-end test: test no connectivity with ping + - ping_check "dc1" "172.16.1.10" "3 packets transmitted, 3 received, 0% packet loss" + - ping_check "dc1" "172.16.1.20" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + - ping_check "dc1" "172.16.1.30" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + + # Run end-to-end test: cleanup scenario + - > + docker run -t --rm --name ${TEST_NAME} --network=host + --volume "$PWD/tfs_runtime_env_vars.sh:/var/teraflow/tfs_runtime_env_vars.sh" + --volume "$PWD/src/tests/${TEST_NAME}:/opt/results" + $CI_REGISTRY_IMAGE/${TEST_NAME}:latest /var/teraflow/run-cleanup.sh + + after_script: + # Dump configuration of the routers (on after_script) + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r1 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r2 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + - containerlab exec --name ${TEST_NAME} --label clab-node-name=r3 --cmd "Cli --command \"enable"$'\n'$"show running-config\"" + + # Dump TeraFlowSDN component logs + - source src/tests/${TEST_NAME}/deploy_specs.sh + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/contextservice -c server + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/deviceservice -c server + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/pathcompservice -c frontend + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/serviceservice -c server + - kubectl --namespace $TFS_K8S_NAMESPACE logs deployment/nbiservice -c server + + # Clean up + - RUNNER_PATH=`pwd` + #- cd $PWD/src/tests/${TEST_NAME} + - cd /tmp/clab/${TEST_NAME} + - containerlab destroy --topo ${TEST_NAME}.clab.yml --cleanup || true + - sudo rm -rf clab-${TEST_NAME}/ .${TEST_NAME}.clab.yml.bak || true + - cd $RUNNER_PATH + - kubectl delete namespaces tfs || true + - docker ps --all --quiet | xargs --no-run-if-empty docker stop + - docker container prune --force + - docker ps --all --quiet | xargs --no-run-if-empty docker rm --force + - docker network prune --force + - docker volume prune --all --force + - docker image prune --force + + #coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' + artifacts: + when: always + reports: + junit: ./src/tests/${TEST_NAME}/report_*.xml diff --git a/src/tests/osm_end2end/Dockerfile b/src/tests/osm_end2end/Dockerfile new file mode 100644 index 000000000..5b59ca0fe --- /dev/null +++ b/src/tests/osm_end2end/Dockerfile @@ -0,0 +1,86 @@ +# 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. + +FROM python:3.9-slim + +# Install dependencies +RUN apt-get --yes --quiet --quiet update && \ + apt-get --yes --quiet --quiet install wget g++ git && \ + rm -rf /var/lib/apt/lists/* + +# Set Python to show logs as they occur +ENV PYTHONUNBUFFERED=0 + +# Get generic Python packages +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' + +# Get common Python packages +# Note: this step enables sharing the previous Docker build steps among all the Python components +WORKDIR /var/teraflow +COPY common_requirements.in common_requirements.in +RUN pip-compile --quiet --output-file=common_requirements.txt common_requirements.in +RUN python3 -m pip install -r common_requirements.txt + +# Add common files into working directory +WORKDIR /var/teraflow/common +COPY src/common/. ./ +RUN rm -rf proto + +# Create proto sub-folder, copy .proto files, and generate Python code +RUN mkdir -p /var/teraflow/common/proto +WORKDIR /var/teraflow/common/proto +RUN touch __init__.py +COPY proto/*.proto ./ +RUN python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. *.proto +RUN rm *.proto +RUN find . -type f -exec sed -i -E 's/^(import\ .*)_pb2/from . \1_pb2/g' {} \; + +# Create component sub-folders, get specific Python packages +RUN mkdir -p /var/teraflow/tests/osm_end2end +WORKDIR /var/teraflow/tests/osm_end2end +COPY src/tests/osm_end2end/requirements.in requirements.in +RUN pip-compile --quiet --output-file=requirements.txt requirements.in +RUN python3 -m pip install -r requirements.txt + +# Add component files into working directory +WORKDIR /var/teraflow +COPY src/__init__.py ./__init__.py +COPY src/common/*.py ./common/ +COPY src/common/tests/. ./common/tests/ +COPY src/common/tools/. ./common/tools/ +COPY src/context/__init__.py context/__init__.py +COPY src/context/client/. context/client/ +COPY src/device/__init__.py device/__init__.py +COPY src/device/client/. device/client/ +COPY src/monitoring/__init__.py monitoring/__init__.py +COPY src/monitoring/client/. monitoring/client/ +COPY src/service/__init__.py service/__init__.py +COPY src/service/client/. service/client/ +COPY src/slice/__init__.py slice/__init__.py +COPY src/slice/client/. slice/client/ +COPY src/vnt_manager/__init__.py vnt_manager/__init__.py +COPY src/vnt_manager/client/. vnt_manager/client/ +COPY src/tests/*.py ./tests/ +COPY src/tests/osm_end2end/__init__.py ./tests/osm_end2end/__init__.py +COPY src/tests/osm_end2end/data/. ./tests/osm_end2end/data/ +COPY src/tests/osm_end2end/tests/. ./tests/osm_end2end/tests/ +COPY src/tests/osm_end2end/scripts/. ./ + +RUN apt-get --yes --quiet --quiet update && \ + apt-get --yes --quiet --quiet install tree && \ + rm -rf /var/lib/apt/lists/* + +RUN tree -la /var/teraflow diff --git a/src/tests/osm_end2end/README.md b/src/tests/osm_end2end/README.md new file mode 100644 index 000000000..f27e142a9 --- /dev/null +++ b/src/tests/osm_end2end/README.md @@ -0,0 +1,130 @@ +# OSM Service End-to-End integration test + +## Emulated DataPlane Deployment +- ContainerLab +- Scenario +- Descriptor + +## TeraFlowSDN Deployment +```bash +cd ~/tfs-ctrl +source ~/tfs-ctrl/src/tests/osm_end2end/deploy_specs.sh +./deploy/all.sh +``` + +# ContainerLab - Arista cEOS - Commands + +## Download and install ContainerLab +```bash +sudo bash -c "$(curl -sL https://get.containerlab.dev)" -- -v 0.59.0 +``` + +## Download Arista cEOS image and create Docker image +```bash +cd ~/tfs-ctrl/src/tests/osm_end2end/ +docker import arista/cEOS64-lab-4.33.5M.tar ceos:4.33.5M +``` + +## Deploy scenario +```bash +cd ~/tfs-ctrl/src/tests/osm_end2end/ +sudo containerlab deploy --topo osm_end2end.clab.yml +``` + +## Inspect scenario +```bash +cd ~/tfs-ctrl/src/tests/osm_end2end/ +sudo containerlab inspect --topo osm_end2end.clab.yml +``` + +## Destroy scenario +```bash +cd ~/tfs-ctrl/src/tests/osm_end2end/ +sudo containerlab destroy --topo osm_end2end.clab.yml +sudo rm -rf clab-osm_end2end/ .osm_end2end.clab.yml.bak +``` + +## Access cEOS Bash/CLI +```bash +docker exec -it clab-osm_end2end-r1 bash +docker exec -it clab-osm_end2end-r2 bash +docker exec -it clab-osm_end2end-r3 bash +docker exec -it clab-osm_end2end-r1 Cli +docker exec -it clab-osm_end2end-r2 Cli +docker exec -it clab-osm_end2end-r3 Cli +``` + +## Configure ContainerLab clients +```bash +docker exec -it clab-osm_end2end-dc1 bash + ip link set address 00:c1:ab:00:01:0a dev eth1 + ip link set eth1 up + ip link add link eth1 name eth1.125 type vlan id 125 + ip address add 172.16.1.10/24 dev eth1.125 + ip link set eth1.125 up + ip route add 172.16.2.0/24 via 172.16.1.1 + ping 172.16.2.10 + +docker exec -it clab-osm_end2end-dc2 bash + ip link set address 00:c1:ab:00:02:0a dev eth1 + ip link set eth1 up + ip link add link eth1 name eth1.125 type vlan id 125 + ip address add 172.16.2.10/24 dev eth1.125 + ip link set eth1.125 up + ip route add 172.16.1.0/24 via 172.16.2.1 + ping 172.16.1.10 +``` + +## Install gNMIc +```bash +sudo bash -c "$(curl -sL https://get-gnmic.kmrd.dev)" +``` + +## gNMI Capabilities request +```bash +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure capabilities +``` + +## gNMI Get request +```bash +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path / > r1.json +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path /interfaces/interface > r1-ifaces.json +``` + +## gNMI Set request +```bash +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf set --update-path /system/config/hostname --update-value srl11 +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path /system/config/hostname + +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf set \ +--update-path '/network-instances/network-instance[name=default]/vlans/vlan[vlan-id=200]/config/vlan-id' --update-value 200 \ +--update-path '/interfaces/interface[name=Ethernet10]/config/name' --update-value '"Ethernet10"' \ +--update-path '/interfaces/interface[name=Ethernet10]/ethernet/switched-vlan/config/interface-mode' --update-value '"ACCESS"' \ +--update-path '/interfaces/interface[name=Ethernet10]/ethernet/switched-vlan/config/access-vlan' --update-value 200 \ +--update-path '/interfaces/interface[name=Ethernet2]/config/name' --update-value '"Ethernet2"' \ +--update-path '/interfaces/interface[name=Ethernet2]/ethernet/switched-vlan/config/interface-mode' --update-value '"TRUNK"' +--update-path '/interfaces/interface[name=Ethernet2]/ethernet/switched-vlan/config/trunk-vlans' --update-value 200 + +``` + +## Subscribe request +```bash +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf subscribe --path /interfaces/interface[name=Management0]/state/ + +# In another terminal, you can generate traffic opening SSH connection +ssh admin@clab-osm_end2end-r1 +``` + +# Check configurations done: +```bash +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path '/' > r1-all.json +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path '/network-instances' > r1-nis.json +gnmic --address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf get --path '/interfaces' > r1-ifs.json +``` + +# Delete elements: +```bash +--address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf set --delete '/network-instances/network-instance[name=b19229e8]' +--address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf set --delete '/interfaces/interface[name=ethernet-1/1]/subinterfaces/subinterface[index=0]' +--address clab-osm_end2end-r1 --port 6030 --username admin --password admin --insecure --encoding json_ietf set --delete '/interfaces/interface[name=ethernet-1/2]/subinterfaces/subinterface[index=0]' +``` diff --git a/src/tests/osm_end2end/__init__.py b/src/tests/osm_end2end/__init__.py new file mode 100644 index 000000000..3ccc21c7d --- /dev/null +++ b/src/tests/osm_end2end/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/tests/osm_end2end/clab/osm_end2end.clab.yml b/src/tests/osm_end2end/clab/osm_end2end.clab.yml new file mode 100644 index 000000000..2560cd3e4 --- /dev/null +++ b/src/tests/osm_end2end/clab/osm_end2end.clab.yml @@ -0,0 +1,82 @@ +# 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. + +# TFS - Arista devices + Linux clients + +name: osm_end2end + +mgmt: + network: mgmt-net + ipv4-subnet: 172.20.20.0/24 + +topology: + kinds: + arista_ceos: + kind: arista_ceos + #image: ceos:4.30.4M + #image: ceos:4.31.2F + #image: ceos:4.31.5M # tested, works + #image: ceos:4.32.0F + #image: ceos:4.33.5M + #image: ceos:4.34.4M + image: ceos:4.32.2F + #image: ceos:4.32.2.1F + #image: ceos:4.33.1F # does not work, libyang.util.LibyangError: failed to parse data tree: No module named "openconfig-platform-healthz" in the context. + linux: + kind: linux + image: ghcr.io/hellt/network-multitool:latest + + nodes: + r1: + kind: arista_ceos + mgmt-ipv4: 172.20.20.101 + startup-config: r1-startup.cfg + + r2: + kind: arista_ceos + mgmt-ipv4: 172.20.20.102 + startup-config: r2-startup.cfg + + r3: + kind: arista_ceos + mgmt-ipv4: 172.20.20.103 + startup-config: r3-startup.cfg + + dc1: + kind: linux + mgmt-ipv4: 172.20.20.201 + exec: + - ip link set address 00:c1:ab:00:01:0a dev eth1 + - ip link set eth1 up + - ip link add link eth1 name eth1.125 type vlan id 125 + - ip address add 172.16.1.10/24 dev eth1.125 + - ip link set eth1.125 up + - ip route add 172.16.2.0/24 via 172.16.1.1 + + dc2: + kind: linux + mgmt-ipv4: 172.20.20.202 + exec: + - ip link set address 00:c1:ab:00:02:0a dev eth1 + - ip link set eth1 up + - ip link add link eth1 name eth1.125 type vlan id 125 + - ip address add 172.16.2.10/24 dev eth1.125 + - ip link set eth1.125 up + - ip route add 172.16.1.0/24 via 172.16.2.1 + + links: + - endpoints: ["r1:eth2", "r2:eth1"] + - endpoints: ["r2:eth3", "r3:eth2"] + - endpoints: ["r1:eth10", "dc1:eth1"] + - endpoints: ["r3:eth10", "dc2:eth1"] diff --git a/src/tests/osm_end2end/clab/r1-startup.cfg b/src/tests/osm_end2end/clab/r1-startup.cfg new file mode 100644 index 000000000..712797deb --- /dev/null +++ b/src/tests/osm_end2end/clab/r1-startup.cfg @@ -0,0 +1,48 @@ +! device: r1 (cEOSLab, EOS-4.34.4M) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$OmfaAwJRg/r44r5U$9Fca1O1G6Bgsd4NKwSyvdRJcHHk71jHAR3apDWAgSTN/t/j1iroEhz5J36HjWjOF/jEVC/R8Wa60VmbX6.cr70 +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname r1 +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet2 +! +interface Ethernet10 +! +interface Management0 + ip address 172.20.20.101/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end diff --git a/src/tests/osm_end2end/clab/r2-startup.cfg b/src/tests/osm_end2end/clab/r2-startup.cfg new file mode 100644 index 000000000..6a1133703 --- /dev/null +++ b/src/tests/osm_end2end/clab/r2-startup.cfg @@ -0,0 +1,48 @@ +! device: r2 (cEOSLab, EOS-4.34.4M) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$OmfaAwJRg/r44r5U$9Fca1O1G6Bgsd4NKwSyvdRJcHHk71jHAR3apDWAgSTN/t/j1iroEhz5J36HjWjOF/jEVC/R8Wa60VmbX6.cr70 +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname r2 +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet1 +! +interface Ethernet3 +! +interface Management0 + ip address 172.20.20.102/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end diff --git a/src/tests/osm_end2end/clab/r3-startup.cfg b/src/tests/osm_end2end/clab/r3-startup.cfg new file mode 100644 index 000000000..946de6f77 --- /dev/null +++ b/src/tests/osm_end2end/clab/r3-startup.cfg @@ -0,0 +1,48 @@ +! device: r3 (cEOSLab, EOS-4.34.4M) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$OmfaAwJRg/r44r5U$9Fca1O1G6Bgsd4NKwSyvdRJcHHk71jHAR3apDWAgSTN/t/j1iroEhz5J36HjWjOF/jEVC/R8Wa60VmbX6.cr70 +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname r3 +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet2 +! +interface Ethernet10 +! +interface Management0 + ip address 172.20.20.103/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end diff --git a/src/tests/osm_end2end/data/tfs-topology.json b/src/tests/osm_end2end/data/tfs-topology.json new file mode 100644 index 000000000..ac87af62d --- /dev/null +++ b/src/tests/osm_end2end/data/tfs-topology.json @@ -0,0 +1,126 @@ +{ + "contexts": [ + {"context_id": {"context_uuid": {"uuid": "admin"}}} + ], + "topologies": [ + {"topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "admin"}}} + ], + "devices": [ + { + "device_id": {"device_uuid": {"uuid": "dc1"}}, "device_type": "emu-datacenter", + "device_drivers": ["DEVICEDRIVER_UNDEFINED"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "type": "copper"}, {"uuid": "int", "type": "copper"} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "dc2"}}, "device_type": "emu-datacenter", + "device_drivers": ["DEVICEDRIVER_UNDEFINED"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "type": "copper"}, {"uuid": "int", "type": "copper"} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "r1"}}, "device_type": "packet-router", + "device_drivers": ["DEVICEDRIVER_GNMI_OPENCONFIG"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "172.20.20.101"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "6030"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": { + "username": "admin", "password": "admin", "use_tls": false + }}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "r2"}}, "device_type": "packet-router", + "device_drivers": ["DEVICEDRIVER_GNMI_OPENCONFIG"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "172.20.20.102"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "6030"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": { + "username": "admin", "password": "admin", "use_tls": false + }}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "r3"}}, "device_type": "packet-router", + "device_drivers": ["DEVICEDRIVER_GNMI_OPENCONFIG"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "172.20.20.103"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "6030"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": { + "username": "admin", "password": "admin", "use_tls": false + }}} + ]} + } + ], + "links": [ + { + "link_id": {"link_uuid": {"uuid": "r1/Ethernet2==r2/Ethernet1"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet2"}}, + {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "r2/Ethernet1==r1/Ethernet2"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet1"}}, + {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet2"}} + ] + }, + + { + "link_id": {"link_uuid": {"uuid": "r2/Ethernet3==r3/Ethernet2"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet3"}}, + {"device_id": {"device_uuid": {"uuid": "r3"}}, "endpoint_uuid": {"uuid": "Ethernet2"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "r3/Ethernet2==r2/Ethernet3"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r3"}}, "endpoint_uuid": {"uuid": "Ethernet2"}}, + {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet3"}} + ] + }, + + { + "link_id": {"link_uuid": {"uuid": "r1/Ethernet10==dc1/eth1"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, + {"device_id": {"device_uuid": {"uuid": "dc1"}}, "endpoint_uuid": {"uuid": "eth1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "dc1/eth1==r1/Ethernet10"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "dc1"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} + ] + }, + + { + "link_id": {"link_uuid": {"uuid": "r3/Ethernet10==dc2/eth1"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r3"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, + {"device_id": {"device_uuid": {"uuid": "dc2"}}, "endpoint_uuid": {"uuid": "eth1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "dc2/eth1==r3/Ethernet10"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "dc2"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "r3"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} + ] + } + ] +} diff --git a/src/tests/osm_end2end/deploy-scripts/clab-cli-dc1.sh b/src/tests/osm_end2end/deploy-scripts/clab-cli-dc1.sh new file mode 100755 index 000000000..6b63cd3c2 --- /dev/null +++ b/src/tests/osm_end2end/deploy-scripts/clab-cli-dc1.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 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. + +docker exec -it clab-osm_end2end-dc1 bash diff --git a/src/tests/osm_end2end/deploy-scripts/clab-cli-dc2.sh b/src/tests/osm_end2end/deploy-scripts/clab-cli-dc2.sh new file mode 100755 index 000000000..5aa2e90ab --- /dev/null +++ b/src/tests/osm_end2end/deploy-scripts/clab-cli-dc2.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 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. + +docker exec -it clab-osm_end2end-dc2 bash diff --git a/src/tests/osm_end2end/deploy-scripts/clab-cli-r1.sh b/src/tests/osm_end2end/deploy-scripts/clab-cli-r1.sh new file mode 100755 index 000000000..56ba2d327 --- /dev/null +++ b/src/tests/osm_end2end/deploy-scripts/clab-cli-r1.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 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. + +docker exec -it clab-osm_end2end-r1 Cli diff --git a/src/tests/osm_end2end/deploy-scripts/clab-cli-r2.sh b/src/tests/osm_end2end/deploy-scripts/clab-cli-r2.sh new file mode 100755 index 000000000..66d92d77f --- /dev/null +++ b/src/tests/osm_end2end/deploy-scripts/clab-cli-r2.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 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. + +docker exec -it clab-osm_end2end-r2 Cli diff --git a/src/tests/osm_end2end/deploy-scripts/clab-cli-r3.sh b/src/tests/osm_end2end/deploy-scripts/clab-cli-r3.sh new file mode 100755 index 000000000..38612f73d --- /dev/null +++ b/src/tests/osm_end2end/deploy-scripts/clab-cli-r3.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 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. + +docker exec -it clab-osm_end2end-r3 Cli diff --git a/src/tests/osm_end2end/deploy-scripts/clab-deploy.sh b/src/tests/osm_end2end/deploy-scripts/clab-deploy.sh new file mode 100755 index 000000000..fb50064c5 --- /dev/null +++ b/src/tests/osm_end2end/deploy-scripts/clab-deploy.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# 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. + +cd ~/tfs-ctrl/src/tests/osm_end2end +sudo containerlab deploy --topo clab/osm_end2end.clab.yml diff --git a/src/tests/osm_end2end/deploy-scripts/clab-destroy.sh b/src/tests/osm_end2end/deploy-scripts/clab-destroy.sh new file mode 100755 index 000000000..1718f3949 --- /dev/null +++ b/src/tests/osm_end2end/deploy-scripts/clab-destroy.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# 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. + +cd ~/tfs-ctrl/src/tests/osm_end2end +sudo containerlab destroy --topo clab/osm_end2end.clab.yml +sudo rm -rf clab/clab-osm_end2end/ clab/.osm_end2end.clab.yml.bak diff --git a/src/tests/osm_end2end/deploy-scripts/clab-inspect.sh b/src/tests/osm_end2end/deploy-scripts/clab-inspect.sh new file mode 100755 index 000000000..d27a044c0 --- /dev/null +++ b/src/tests/osm_end2end/deploy-scripts/clab-inspect.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# 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. + +cd ~/tfs-ctrl/src/tests/osm_end2end +sudo containerlab inspect --topo osm_end2end.clab.yml diff --git a/src/tests/osm_end2end/deploy_specs.sh b/src/tests/osm_end2end/deploy_specs.sh new file mode 100755 index 000000000..72cd25b58 --- /dev/null +++ b/src/tests/osm_end2end/deploy_specs.sh @@ -0,0 +1,208 @@ +#!/bin/bash +# 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. + + +# ----- TeraFlowSDN ------------------------------------------------------------ + +# Set the URL of the internal MicroK8s Docker registry where the images will be uploaded to. +export TFS_REGISTRY_IMAGES="http://localhost:32000/tfs/" + +# Set the list of components, separated by spaces, you want to build images for, and deploy. +#export TFS_COMPONENTS="context device pathcomp service slice nbi webui load_generator" +export TFS_COMPONENTS="context device pathcomp service nbi" + +# Uncomment to activate Monitoring (old) +#export TFS_COMPONENTS="${TFS_COMPONENTS} monitoring" + +# Uncomment to activate Monitoring Framework (new) +#export TFS_COMPONENTS="${TFS_COMPONENTS} kpi_manager kpi_value_writer kpi_value_api telemetry analytics automation" + +# Uncomment to activate QoS Profiles +#export TFS_COMPONENTS="${TFS_COMPONENTS} qos_profile" + +# Uncomment to activate BGP-LS Speaker +#export TFS_COMPONENTS="${TFS_COMPONENTS} bgpls_speaker" + +# Uncomment to activate Optical Controller +# To manage optical connections, "service" requires "opticalcontroller" to be deployed +# before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the +# "opticalcontroller" only if "service" is already in TFS_COMPONENTS, and re-export it. +#if [[ "$TFS_COMPONENTS" == *"service"* ]]; then +# BEFORE="${TFS_COMPONENTS% service*}" +# AFTER="${TFS_COMPONENTS#* service}" +# export TFS_COMPONENTS="${BEFORE} opticalcontroller service ${AFTER}" +#fi + +# Uncomment to activate ZTP +#export TFS_COMPONENTS="${TFS_COMPONENTS} ztp" + +# Uncomment to activate Policy Manager +#export TFS_COMPONENTS="${TFS_COMPONENTS} policy" + +# Uncomment to activate Optical CyberSecurity +#export TFS_COMPONENTS="${TFS_COMPONENTS} dbscanserving opticalattackmitigator opticalattackdetector opticalattackmanager" + +# Uncomment to activate L3 CyberSecurity +#export TFS_COMPONENTS="${TFS_COMPONENTS} l3_attackmitigator l3_centralizedattackdetector" + +# Uncomment to activate TE +#export TFS_COMPONENTS="${TFS_COMPONENTS} te" + +# Uncomment to activate Forecaster +#export TFS_COMPONENTS="${TFS_COMPONENTS} forecaster" + +# Uncomment to activate E2E Orchestrator +#export TFS_COMPONENTS="${TFS_COMPONENTS} e2e_orchestrator" + +# Uncomment to activate DLT and Interdomain +#export TFS_COMPONENTS="${TFS_COMPONENTS} interdomain dlt" +#if [[ "$TFS_COMPONENTS" == *"dlt"* ]]; then +# export KEY_DIRECTORY_PATH="src/dlt/gateway/keys/priv_sk" +# export CERT_DIRECTORY_PATH="src/dlt/gateway/keys/cert.pem" +# export TLS_CERT_PATH="src/dlt/gateway/keys/ca.crt" +#fi + +# Uncomment to activate QKD App +# To manage QKD Apps, "service" requires "qkd_app" to be deployed +# before "service", thus we "hack" the TFS_COMPONENTS environment variable prepending the +# "qkd_app" only if "service" is already in TFS_COMPONENTS, and re-export it. +#if [[ "$TFS_COMPONENTS" == *"service"* ]]; then +# BEFORE="${TFS_COMPONENTS% service*}" +# AFTER="${TFS_COMPONENTS#* service}" +# export TFS_COMPONENTS="${BEFORE} qkd_app service ${AFTER}" +#fi + + +# Set the tag you want to use for your images. +export TFS_IMAGE_TAG="dev" + +# Set the name of the Kubernetes namespace to deploy TFS to. +export TFS_K8S_NAMESPACE="tfs" + +# Set additional manifest files to be applied after the deployment +export TFS_EXTRA_MANIFESTS="manifests/nginx_ingress_http.yaml" + +# Uncomment to monitor performance of components +#export TFS_EXTRA_MANIFESTS="${TFS_EXTRA_MANIFESTS} manifests/servicemonitors.yaml" + +# Uncomment when deploying Optical CyberSecurity +#export TFS_EXTRA_MANIFESTS="${TFS_EXTRA_MANIFESTS} manifests/cachingservice.yaml" + +# Set the new Grafana admin password +export TFS_GRAFANA_PASSWORD="admin123+" + +# Disable skip-build flag to rebuild the Docker images. +export TFS_SKIP_BUILD="" + + +# ----- CockroachDB ------------------------------------------------------------ + +# Set the namespace where CockroackDB will be deployed. +export CRDB_NAMESPACE="crdb" + +# Set the external port CockroackDB Postgre SQL interface will be exposed to. +export CRDB_EXT_PORT_SQL="26257" + +# Set the external port CockroackDB HTTP Mgmt GUI interface will be exposed to. +export CRDB_EXT_PORT_HTTP="8081" + +# Set the database username to be used by Context. +export CRDB_USERNAME="tfs" + +# Set the database user's password to be used by Context. +export CRDB_PASSWORD="tfs123" + +# Set CockroachDB installation mode to 'single'. This option is convenient for development and testing. +# See ./deploy/all.sh or ./deploy/crdb.sh for additional details +export CRDB_DEPLOY_MODE="single" + +# Disable flag for dropping database, if it exists. +export CRDB_DROP_DATABASE_IF_EXISTS="YES" + +# Disable flag for re-deploying CockroachDB from scratch. +export CRDB_REDEPLOY="" + + +# ----- NATS ------------------------------------------------------------------- + +# Set the namespace where NATS will be deployed. +export NATS_NAMESPACE="nats" + +# Set the external port NATS Client interface will be exposed to. +export NATS_EXT_PORT_CLIENT="4222" + +# Set the external port NATS HTTP Mgmt GUI interface will be exposed to. +export NATS_EXT_PORT_HTTP="8222" + +# Set NATS installation mode to 'single'. This option is convenient for development and testing. +# See ./deploy/all.sh or ./deploy/nats.sh for additional details +export NATS_DEPLOY_MODE="single" + +# Disable flag for re-deploying NATS from scratch. +export NATS_REDEPLOY="" + + +# ----- QuestDB ---------------------------------------------------------------- + +# Set the namespace where QuestDB will be deployed. +export QDB_NAMESPACE="qdb" + +# Set the external port QuestDB Postgre SQL interface will be exposed to. +export QDB_EXT_PORT_SQL="8812" + +# Set the external port QuestDB Influx Line Protocol interface will be exposed to. +export QDB_EXT_PORT_ILP="9009" + +# Set the external port QuestDB HTTP Mgmt GUI interface will be exposed to. +export QDB_EXT_PORT_HTTP="9000" + +# Set the database username to be used for QuestDB. +export QDB_USERNAME="admin" + +# Set the database user's password to be used for QuestDB. +export QDB_PASSWORD="quest" + +# Set the table name to be used by Monitoring for KPIs. +export QDB_TABLE_MONITORING_KPIS="tfs_monitoring_kpis" + +# Set the table name to be used by Slice for plotting groups. +export QDB_TABLE_SLICE_GROUPS="tfs_slice_groups" + +# Disable flag for dropping tables if they exist. +export QDB_DROP_TABLES_IF_EXIST="YES" + +# Disable flag for re-deploying QuestDB from scratch. +export QDB_REDEPLOY="" + + +# ----- K8s Observability ------------------------------------------------------ + +# Set the external port Prometheus Mgmt HTTP GUI interface will be exposed to. +export PROM_EXT_PORT_HTTP="9090" + +# Set the external port Grafana HTTP Dashboards will be exposed to. +export GRAF_EXT_PORT_HTTP="3000" + + +# ----- Apache Kafka ----------------------------------------------------------- + +# Set the namespace where Apache Kafka will be deployed. +export KFK_NAMESPACE="kafka" + +# Set the port Apache Kafka server will be exposed to. +export KFK_SERVER_PORT="9092" + +# Set the flag to YES for redeploying of Apache Kafka +export KFK_REDEPLOY="" diff --git a/src/tests/osm_end2end/redeploy-tfs.sh b/src/tests/osm_end2end/redeploy-tfs.sh new file mode 100755 index 000000000..d1e00f3e5 --- /dev/null +++ b/src/tests/osm_end2end/redeploy-tfs.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# 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. + +source ~/tfs-ctrl/src/tests/osm_end2end/deploy_specs.sh +./deploy/all.sh diff --git a/src/tests/osm_end2end/requirements.in b/src/tests/osm_end2end/requirements.in new file mode 100644 index 000000000..5c92783a2 --- /dev/null +++ b/src/tests/osm_end2end/requirements.in @@ -0,0 +1,15 @@ +# 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. + +requests==2.27.* diff --git a/src/tests/osm_end2end/scripts/run-cleanup.sh b/src/tests/osm_end2end/scripts/run-cleanup.sh new file mode 100755 index 000000000..556495dd1 --- /dev/null +++ b/src/tests/osm_end2end/scripts/run-cleanup.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# 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. + +source /var/teraflow/tfs_runtime_env_vars.sh +export PYTHONPATH=/var/teraflow +pytest --verbose --log-level=INFO \ + --junitxml=/opt/results/report_cleanup.xml \ + /var/teraflow/tests/osm_end2end/tests/test_cleanup.py diff --git a/src/tests/osm_end2end/scripts/run-onboarding.sh b/src/tests/osm_end2end/scripts/run-onboarding.sh new file mode 100755 index 000000000..a68014e45 --- /dev/null +++ b/src/tests/osm_end2end/scripts/run-onboarding.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# 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. + +source /var/teraflow/tfs_runtime_env_vars.sh +export PYTHONPATH=/var/teraflow +pytest --verbose --log-level=INFO \ + --junitxml=/opt/results/report_onboarding.xml \ + /var/teraflow/tests/osm_end2end/tests/test_onboarding.py diff --git a/src/tests/osm_end2end/scripts/run-osm-service-create.sh b/src/tests/osm_end2end/scripts/run-osm-service-create.sh new file mode 100755 index 000000000..fee45d6fe --- /dev/null +++ b/src/tests/osm_end2end/scripts/run-osm-service-create.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# 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. + +source /var/teraflow/tfs_runtime_env_vars.sh +export PYTHONPATH=/var/teraflow +pytest --verbose --log-level=INFO \ + --junitxml=/opt/results/report_osm_service_create.xml \ + /var/teraflow/tests/osm_end2end/tests/test_osm_service_create.py diff --git a/src/tests/osm_end2end/scripts/run-osm-service-remove.sh b/src/tests/osm_end2end/scripts/run-osm-service-remove.sh new file mode 100755 index 000000000..be9e574c8 --- /dev/null +++ b/src/tests/osm_end2end/scripts/run-osm-service-remove.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# 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. + +source /var/teraflow/tfs_runtime_env_vars.sh +export PYTHONPATH=/var/teraflow +pytest --verbose --log-level=INFO \ + --junitxml=/opt/results/report_osm_service_remove.xml \ + /var/teraflow/tests/osm_end2end/tests/test_osm_service_remove.py diff --git a/src/tests/osm_end2end/tests/Fixtures.py b/src/tests/osm_end2end/tests/Fixtures.py new file mode 100644 index 000000000..fae8401bb --- /dev/null +++ b/src/tests/osm_end2end/tests/Fixtures.py @@ -0,0 +1,51 @@ +# 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 pytest +from common.Constants import ServiceNameEnum +from common.Settings import get_service_host, get_service_port_http +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from service.client.ServiceClient import ServiceClient +from .MockOSM import MockOSM +from .OSM_Constants import WIM_MAPPING + +NBI_ADDRESS = get_service_host(ServiceNameEnum.NBI) +NBI_PORT = get_service_port_http(ServiceNameEnum.NBI) +NBI_USERNAME = 'admin' +NBI_PASSWORD = 'admin' +NBI_BASE_URL = '' + +@pytest.fixture(scope='session') +def osm_wim() -> MockOSM: + wim_url = 'http://{:s}:{:d}'.format(NBI_ADDRESS, NBI_PORT) + return MockOSM(wim_url, WIM_MAPPING, NBI_USERNAME, NBI_PASSWORD) + +@pytest.fixture(scope='session') +def context_client() -> ContextClient: + _client = ContextClient() + yield _client + _client.close() + +@pytest.fixture(scope='session') +def device_client() -> DeviceClient: + _client = DeviceClient() + yield _client + _client.close() + +@pytest.fixture(scope='session') +def service_client() -> ServiceClient: + _client = ServiceClient() + yield _client + _client.close() diff --git a/src/tests/osm_end2end/tests/MockOSM.py b/src/tests/osm_end2end/tests/MockOSM.py new file mode 100644 index 000000000..2361b44b6 --- /dev/null +++ b/src/tests/osm_end2end/tests/MockOSM.py @@ -0,0 +1,62 @@ +# 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 logging +from .WimconnectorIETFL2VPN import WimconnectorIETFL2VPN + +LOGGER = logging.getLogger(__name__) + +class MockOSM: + def __init__(self, url, mapping, username, password): + wim = {'wim_url': url} + wim_account = {'user': username, 'password': password} + config = {'mapping_not_needed': False, 'service_endpoint_mapping': mapping} + self.wim = WimconnectorIETFL2VPN(wim, wim_account, config=config) + self.conn_info = {} # internal database emulating OSM storage provided to WIM Connectors + + def create_connectivity_service(self, service_type, connection_points): + LOGGER.info('[create_connectivity_service] service_type={:s}'.format(str(service_type))) + LOGGER.info('[create_connectivity_service] connection_points={:s}'.format(str(connection_points))) + self.wim.check_credentials() + result = self.wim.create_connectivity_service(service_type, connection_points) + LOGGER.info('[create_connectivity_service] result={:s}'.format(str(result))) + service_uuid, conn_info = result + self.conn_info[service_uuid] = conn_info + return service_uuid + + def get_connectivity_service_status(self, service_uuid): + LOGGER.info('[get_connectivity_service] service_uuid={:s}'.format(str(service_uuid))) + conn_info = self.conn_info.get(service_uuid) + if conn_info is None: raise Exception('ServiceId({:s}) not found'.format(str(service_uuid))) + LOGGER.info('[get_connectivity_service] conn_info={:s}'.format(str(conn_info))) + self.wim.check_credentials() + result = self.wim.get_connectivity_service_status(service_uuid, conn_info=conn_info) + LOGGER.info('[get_connectivity_service] result={:s}'.format(str(result))) + return result + + def edit_connectivity_service(self, service_uuid, connection_points): + LOGGER.info('[edit_connectivity_service] service_uuid={:s}'.format(str(service_uuid))) + LOGGER.info('[edit_connectivity_service] connection_points={:s}'.format(str(connection_points))) + conn_info = self.conn_info.get(service_uuid) + if conn_info is None: raise Exception('ServiceId({:s}) not found'.format(str(service_uuid))) + LOGGER.info('[edit_connectivity_service] conn_info={:s}'.format(str(conn_info))) + self.wim.edit_connectivity_service(service_uuid, conn_info=conn_info, connection_points=connection_points) + + def delete_connectivity_service(self, service_uuid): + LOGGER.info('[delete_connectivity_service] service_uuid={:s}'.format(str(service_uuid))) + conn_info = self.conn_info.get(service_uuid) + if conn_info is None: raise Exception('ServiceId({:s}) not found'.format(str(service_uuid))) + LOGGER.info('[delete_connectivity_service] conn_info={:s}'.format(str(conn_info))) + self.wim.check_credentials() + self.wim.delete_connectivity_service(service_uuid, conn_info=conn_info) diff --git a/src/tests/osm_end2end/tests/OSM_Constants.py b/src/tests/osm_end2end/tests/OSM_Constants.py new file mode 100644 index 000000000..54a9b3693 --- /dev/null +++ b/src/tests/osm_end2end/tests/OSM_Constants.py @@ -0,0 +1,53 @@ +# 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. + + +# Ref: https://osm.etsi.org/wikipub/index.php/WIM +WIM_MAPPING = [ + { + 'device-id' : 'dc1', # pop_switch_dpid + #'device_interface_id' : ??, # pop_switch_port + 'service_endpoint_id' : 'ep-1', # wan_service_endpoint_id + 'service_mapping_info': { # wan_service_mapping_info, other extra info + 'bearer': {'bearer-reference': 'OSM-E2E:r1:Ethernet10'}, + 'site-id': '1', + }, + #'switch_dpid' : ??, # wan_switch_dpid + #'switch_port' : ??, # wan_switch_port + #'datacenter_id' : ??, # vim_account + }, + { + 'device-id' : 'dc2', # pop_switch_dpid + #'device_interface_id' : ??, # pop_switch_port + 'service_endpoint_id' : 'ep-2', # wan_service_endpoint_id + 'service_mapping_info': { # wan_service_mapping_info, other extra info + 'bearer': {'bearer-reference': 'OSM-E2E:r3:Ethernet10'}, + 'site-id': '2', + }, + #'switch_dpid' : ??, # wan_switch_dpid + #'switch_port' : ??, # wan_switch_port + #'datacenter_id' : ??, # vim_account + }, +] + +SERVICE_TYPE = 'ELINE' + +SERVICE_CONNECTION_POINTS = [ + {'service_endpoint_id': 'ep-1', + 'service_endpoint_encapsulation_type': 'dot1q', + 'service_endpoint_encapsulation_info': {'vlan': 125}}, + {'service_endpoint_id': 'ep-2', + 'service_endpoint_encapsulation_type': 'dot1q', + 'service_endpoint_encapsulation_info': {'vlan': 125}}, +] diff --git a/src/tests/osm_end2end/tests/WimconnectorIETFL2VPN.py b/src/tests/osm_end2end/tests/WimconnectorIETFL2VPN.py new file mode 100644 index 000000000..de940a7d2 --- /dev/null +++ b/src/tests/osm_end2end/tests/WimconnectorIETFL2VPN.py @@ -0,0 +1,545 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2018 Telefonica +# All Rights Reserved. +# +# Contributors: Oscar Gonzalez de Dios, Manuel Lopez Bravo, Guillermo Pajares Martin +# 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. +# +# This work has been performed in the context of the Metro-Haul project - +# funded by the European Commission under Grant number 761727 through the +# Horizon 2020 program. +## +"""The SDN/WIM connector is responsible for establishing wide area network +connectivity. + +This SDN/WIM connector implements the standard IETF RFC 8466 "A YANG Data + Model for Layer 2 Virtual Private Network (L2VPN) Service Delivery" + +It receives the endpoints and the necessary details to request +the Layer 2 service. +""" +import requests +import uuid +import logging +import copy +#from osm_ro_plugin.sdnconn import SdnConnectorBase, SdnConnectorError +from .sdnconn import SdnConnectorBase, SdnConnectorError + +"""Check layer where we move it""" + + +class WimconnectorIETFL2VPN(SdnConnectorBase): + def __init__(self, wim, wim_account, config=None, logger=None): + """IETF L2VPN WIM connector + + Arguments: (To be completed) + wim (dict): WIM record, as stored in the database + wim_account (dict): WIM account record, as stored in the database + """ + self.logger = logging.getLogger("ro.sdn.ietfl2vpn") + super().__init__(wim, wim_account, config, logger) + self.headers = {"Content-Type": "application/json"} + self.mappings = { + m["service_endpoint_id"]: m for m in self.service_endpoint_mapping + } + self.user = wim_account.get("user") + self.passwd = wim_account.get("password") # replace "passwordd" -> "password" + + if self.user and self.passwd is not None: + self.auth = (self.user, self.passwd) + else: + self.auth = None + + self.logger.info("IETFL2VPN Connector Initialized.") + + def check_credentials(self): + endpoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( + self.wim["wim_url"] + ) + + try: + response = requests.get(endpoint, auth=self.auth) + http_code = response.status_code + except requests.exceptions.RequestException as e: + raise SdnConnectorError(e.response, http_code=503) + + if http_code != 200: + raise SdnConnectorError("Failed while authenticating", http_code=http_code) + + self.logger.info("Credentials checked") + + def get_connectivity_service_status(self, service_uuid, conn_info=None): + """Monitor the status of the connectivity service stablished + + Arguments: + service_uuid: Connectivity service unique identifier + + Returns: + Examples:: + {'sdn_status': 'ACTIVE'} + {'sdn_status': 'INACTIVE'} + {'sdn_status': 'DOWN'} + {'sdn_status': 'ERROR'} + """ + try: + self.logger.info("Sending get connectivity service stuatus") + servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services/vpn-service={}/".format( + self.wim["wim_url"], service_uuid + ) + response = requests.get(servicepoint, auth=self.auth) + self.logger.warning('response.status_code={:s}'.format(str(response.status_code))) + if response.status_code != requests.codes.ok: + raise SdnConnectorError( + "Unable to obtain connectivity servcice status", + http_code=response.status_code, + ) + + service_status = {"sdn_status": "ACTIVE"} + + return service_status + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + def search_mapp(self, connection_point): + id = connection_point["service_endpoint_id"] + if id not in self.mappings: + raise SdnConnectorError("Endpoint {} not located".format(str(id))) + else: + return self.mappings[id] + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + """Stablish WAN connectivity between the endpoints + + Arguments: + service_type (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), + ``L3``. + connection_points (list): each point corresponds to + an entry point from the DC to the transport network. One + connection point serves to identify the specific access and + some other service parameters, such as encapsulation type. + Represented by a dict as follows:: + + { + "service_endpoint_id": ..., (str[uuid]) + "service_endpoint_encapsulation_type": ..., + (enum: none, dot1q, ...) + "service_endpoint_encapsulation_info": { + ... (dict) + "vlan": ..., (int, present if encapsulation is dot1q) + "vni": ... (int, present if encapsulation is vxlan), + "peers": [(ipv4_1), (ipv4_2)] + (present if encapsulation is vxlan) + } + } + + The service endpoint ID should be previously informed to the WIM + engine in the RO when the WIM port mapping is registered. + + Keyword Arguments: + bandwidth (int): value in kilobytes + latency (int): value in milliseconds + + Other QoS might be passed as keyword arguments. + + Returns: + tuple: ``(service_id, conn_info)`` containing: + - *service_uuid* (str): UUID of the established connectivity + service + - *conn_info* (dict or None): Information to be stored at the + database (or ``None``). This information will be provided to + the :meth:`~.edit_connectivity_service` and :obj:`~.delete`. + **MUST** be JSON/YAML-serializable (plain data structures). + + Raises: + SdnConnectorException: In case of error. + """ + SETTINGS = { # min_endpoints, max_endpoints, vpn_service_type + 'ELINE': (2, 2, 'vpws'), # Virtual Private Wire Service + 'ELAN' : (2, None, 'vpls'), # Virtual Private LAN Service + } + settings = SETTINGS.get(service_type) + if settings is None: raise NotImplementedError('Unsupported service_type({:s})'.format(str(service_type))) + min_endpoints, max_endpoints, vpn_service_type = settings + + if max_endpoints is not None and len(connection_points) > max_endpoints: + msg = "Connections between more than {:d} endpoints are not supported for service_type {:s}" + raise SdnConnectorError(msg.format(max_endpoints, service_type)) + + if min_endpoints is not None and len(connection_points) < min_endpoints: + msg = "Connections must be of at least {:d} endpoints for service_type {:s}" + raise SdnConnectorError(msg.format(min_endpoints, service_type)) + + """First step, create the vpn service""" + uuid_l2vpn = str(uuid.uuid4()) + vpn_service = {} + vpn_service["vpn-id"] = uuid_l2vpn + vpn_service["vpn-svc-type"] = vpn_service_type + vpn_service["svc-topo"] = "any-to-any" + vpn_service["customer-name"] = "osm" + vpn_service_list = [] + vpn_service_list.append(vpn_service) + vpn_service_l = {"ietf-l2vpn-svc:vpn-service": vpn_service_list} + response_service_creation = None + conn_info = [] + self.logger.info("Sending vpn-service : {:s}".format(str(vpn_service_l))) + + try: + endpoint_service_creation = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( + self.wim["wim_url"] + ) + ) + response_service_creation = requests.post( + endpoint_service_creation, + headers=self.headers, + json=vpn_service_l, + auth=self.auth, + ) + except requests.exceptions.ConnectionError: + raise SdnConnectorError( + "Request to create service Timeout", http_code=408 + ) + + if response_service_creation.status_code == 409: + raise SdnConnectorError( + "Service already exists", + http_code=response_service_creation.status_code, + ) + elif response_service_creation.status_code != requests.codes.created: + raise SdnConnectorError( + "Request to create service not accepted", + http_code=response_service_creation.status_code, + ) + + self.logger.info('connection_points = {:s}'.format(str(connection_points))) + + # Check if protected paths are requested + extended_connection_points = [] + for connection_point in connection_points: + extended_connection_points.append(connection_point) + + connection_point_wan_info = self.search_mapp(connection_point) + service_mapping_info = connection_point_wan_info.get('service_mapping_info', {}) + redundant_service_endpoint_ids = service_mapping_info.get('redundant') + + if redundant_service_endpoint_ids is None: continue + if len(redundant_service_endpoint_ids) == 0: continue + + for redundant_service_endpoint_id in redundant_service_endpoint_ids: + redundant_connection_point = copy.deepcopy(connection_point) + redundant_connection_point['service_endpoint_id'] = redundant_service_endpoint_id + extended_connection_points.append(redundant_connection_point) + + self.logger.info('extended_connection_points = {:s}'.format(str(extended_connection_points))) + + """Second step, create the connections and vpn attachments""" + for connection_point in extended_connection_points: + connection_point_wan_info = self.search_mapp(connection_point) + site_network_access = {} + connection = {} + + if connection_point["service_endpoint_encapsulation_type"] != "none": + if ( + connection_point["service_endpoint_encapsulation_type"] + == "dot1q" + ): + """The connection is a VLAN""" + connection["encapsulation-type"] = "dot1q-vlan-tagged" + tagged = {} + tagged_interf = {} + service_endpoint_encapsulation_info = connection_point[ + "service_endpoint_encapsulation_info" + ] + + if service_endpoint_encapsulation_info["vlan"] is None: + raise SdnConnectorError("VLAN must be provided") + + tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info[ + "vlan" + ] + tagged["dot1q-vlan-tagged"] = tagged_interf + connection["tagged-interface"] = tagged + else: + raise NotImplementedError("Encapsulation type not implemented") + + site_network_access["connection"] = connection + self.logger.info("Sending connection:{}".format(connection)) + vpn_attach = {} + vpn_attach["vpn-id"] = uuid_l2vpn + vpn_attach["site-role"] = vpn_service["svc-topo"] + "-role" + site_network_access["vpn-attachment"] = vpn_attach + self.logger.info("Sending vpn-attachement :{}".format(vpn_attach)) + uuid_sna = str(uuid.uuid4()) + site_network_access["network-access-id"] = uuid_sna + site_network_access["bearer"] = connection_point_wan_info[ + "service_mapping_info" + ]["bearer"] + + access_priority = connection_point_wan_info["service_mapping_info"].get("priority") + if access_priority is not None: + availability = {} + availability["access-priority"] = access_priority + availability["single-active"] = [None] + site_network_access["availability"] = availability + + constraint = {} + constraint['constraint-type'] = 'end-to-end-diverse' + constraint['target'] = {'all-other-accesses': [None]} + + access_diversity = {} + access_diversity['constraints'] = {'constraint': []} + access_diversity['constraints']['constraint'].append(constraint) + site_network_access["access-diversity"] = access_diversity + + site_network_accesses = {} + site_network_access_list = [] + site_network_access_list.append(site_network_access) + site_network_accesses[ + "ietf-l2vpn-svc:site-network-access" + ] = site_network_access_list + conn_info_d = {} + conn_info_d["site"] = connection_point_wan_info["service_mapping_info"][ + "site-id" + ] + conn_info_d["site-network-access-id"] = site_network_access[ + "network-access-id" + ] + conn_info_d["mapping"] = None + conn_info.append(conn_info_d) + + self.logger.info("Sending site_network_accesses : {:s}".format(str(site_network_accesses))) + + try: + endpoint_site_network_access_creation = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/" + "sites/site={}/site-network-accesses/".format( + self.wim["wim_url"], + connection_point_wan_info["service_mapping_info"][ + "site-id" + ], + ) + ) + response_endpoint_site_network_access_creation = requests.post( + endpoint_site_network_access_creation, + headers=self.headers, + json=site_network_accesses, + auth=self.auth, + ) + + if ( + response_endpoint_site_network_access_creation.status_code + == 409 + ): + self.delete_connectivity_service(vpn_service["vpn-id"]) + + raise SdnConnectorError( + "Site_Network_Access with ID '{}' already exists".format( + site_network_access["network-access-id"] + ), + http_code=response_endpoint_site_network_access_creation.status_code, + ) + elif ( + response_endpoint_site_network_access_creation.status_code + == 400 + ): + self.delete_connectivity_service(vpn_service["vpn-id"]) + + raise SdnConnectorError( + "Site {} does not exist".format( + connection_point_wan_info["service_mapping_info"][ + "site-id" + ] + ), + http_code=response_endpoint_site_network_access_creation.status_code, + ) + elif ( + response_endpoint_site_network_access_creation.status_code + != requests.codes.created + and response_endpoint_site_network_access_creation.status_code + != requests.codes.no_content + ): + self.delete_connectivity_service(vpn_service["vpn-id"]) + + raise SdnConnectorError( + "Request not accepted", + http_code=response_endpoint_site_network_access_creation.status_code, + ) + except requests.exceptions.ConnectionError: + self.delete_connectivity_service(vpn_service["vpn-id"]) + + raise SdnConnectorError("Request Timeout", http_code=408) + + return uuid_l2vpn, conn_info + + def delete_connectivity_service(self, service_uuid, conn_info=None): + """Disconnect multi-site endpoints previously connected + + This method should receive as the first argument the UUID generated by + the ``create_connectivity_service`` + """ + try: + self.logger.info("Sending delete") + servicepoint = "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services/vpn-service={}/".format( + self.wim["wim_url"], service_uuid + ) + response = requests.delete(servicepoint, auth=self.auth) + + if response.status_code != requests.codes.no_content: + raise SdnConnectorError( + "Error in the request", http_code=response.status_code + ) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + def edit_connectivity_service( + self, service_uuid, conn_info=None, connection_points=None, **kwargs + ): + """Change an existing connectivity service, see + ``create_connectivity_service``""" + # sites = {"sites": {}} + # site_list = [] + vpn_service = {} + vpn_service["svc-topo"] = "any-to-any" + counter = 0 + + for connection_point in connection_points: + site_network_access = {} + connection_point_wan_info = self.search_mapp(connection_point) + params_site = {} + params_site["site-id"] = connection_point_wan_info["service_mapping_info"][ + "site-id" + ] + params_site["site-vpn-flavor"] = "site-vpn-flavor-single" + device_site = {} + device_site["device-id"] = connection_point_wan_info["device-id"] + params_site["devices"] = device_site + # network_access = {} + connection = {} + + if connection_point["service_endpoint_encapsulation_type"] != "none": + if connection_point["service_endpoint_encapsulation_type"] == "dot1q": + """The connection is a VLAN""" + connection["encapsulation-type"] = "dot1q-vlan-tagged" + tagged = {} + tagged_interf = {} + service_endpoint_encapsulation_info = connection_point[ + "service_endpoint_encapsulation_info" + ] + + if service_endpoint_encapsulation_info["vlan"] is None: + raise SdnConnectorError("VLAN must be provided") + + tagged_interf["cvlan-id"] = service_endpoint_encapsulation_info[ + "vlan" + ] + tagged["dot1q-vlan-tagged"] = tagged_interf + connection["tagged-interface"] = tagged + else: + raise NotImplementedError("Encapsulation type not implemented") + + site_network_access["connection"] = connection + vpn_attach = {} + vpn_attach["vpn-id"] = service_uuid + vpn_attach["site-role"] = vpn_service["svc-topo"] + "-role" + site_network_access["vpn-attachment"] = vpn_attach + uuid_sna = conn_info[counter]["site-network-access-id"] + site_network_access["network-access-id"] = uuid_sna + site_network_access["bearer"] = connection_point_wan_info[ + "service_mapping_info" + ]["bearer"] + site_network_accesses = {} + site_network_access_list = [] + site_network_access_list.append(site_network_access) + site_network_accesses[ + "ietf-l2vpn-svc:site-network-access" + ] = site_network_access_list + + try: + endpoint_site_network_access_edit = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/" + "sites/site={}/site-network-accesses/".format( + self.wim["wim_url"], + connection_point_wan_info["service_mapping_info"]["site-id"], + ) + ) + response_endpoint_site_network_access_creation = requests.put( + endpoint_site_network_access_edit, + headers=self.headers, + json=site_network_accesses, + auth=self.auth, + ) + + if response_endpoint_site_network_access_creation.status_code == 400: + raise SdnConnectorError( + "Service does not exist", + http_code=response_endpoint_site_network_access_creation.status_code, + ) + elif ( + response_endpoint_site_network_access_creation.status_code != 201 + and response_endpoint_site_network_access_creation.status_code + != 204 + ): + raise SdnConnectorError( + "Request no accepted", + http_code=response_endpoint_site_network_access_creation.status_code, + ) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + counter += 1 + + return None + + def clear_all_connectivity_services(self): + """Delete all WAN Links corresponding to a WIM""" + try: + self.logger.info("Sending clear all connectivity services") + servicepoint = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( + self.wim["wim_url"] + ) + ) + response = requests.delete(servicepoint, auth=self.auth) + + if response.status_code != requests.codes.no_content: + raise SdnConnectorError( + "Unable to clear all connectivity services", + http_code=response.status_code, + ) + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) + + def get_all_active_connectivity_services(self): + """Provide information about all active connections provisioned by a + WIM + """ + try: + self.logger.info("Sending get all connectivity services") + servicepoint = ( + "{}/restconf/data/ietf-l2vpn-svc:l2vpn-svc/vpn-services".format( + self.wim["wim_url"] + ) + ) + response = requests.get(servicepoint, auth=self.auth) + + if response.status_code != requests.codes.ok: + raise SdnConnectorError( + "Unable to get all connectivity services", + http_code=response.status_code, + ) + + return response + except requests.exceptions.ConnectionError: + raise SdnConnectorError("Request Timeout", http_code=408) diff --git a/src/tests/osm_end2end/tests/__init__.py b/src/tests/osm_end2end/tests/__init__.py new file mode 100644 index 000000000..3ccc21c7d --- /dev/null +++ b/src/tests/osm_end2end/tests/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/tests/osm_end2end/tests/acknowledgements.txt b/src/tests/osm_end2end/tests/acknowledgements.txt new file mode 100644 index 000000000..b7ce926dd --- /dev/null +++ b/src/tests/osm_end2end/tests/acknowledgements.txt @@ -0,0 +1,3 @@ +MockOSM is based on source code taken from: +https://osm.etsi.org/gitlab/osm/ro/-/blob/master/RO-plugin/osm_ro_plugin/sdnconn.py +https://osm.etsi.org/gitlab/osm/ro/-/blob/master/RO-SDN-ietfl2vpn/osm_rosdn_ietfl2vpn/wimconn_ietfl2vpn.py diff --git a/src/tests/osm_end2end/tests/sdnconn.py b/src/tests/osm_end2end/tests/sdnconn.py new file mode 100644 index 000000000..a1849c9ef --- /dev/null +++ b/src/tests/osm_end2end/tests/sdnconn.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +## +# Copyright 2018 University of Bristol - High Performance Networks Research +# Group +# All Rights Reserved. +# +# Contributors: Anderson Bravalheri, Dimitrios Gkounis, Abubakar Siddique +# Muqaddas, Navdeep Uniyal, Reza Nejabati and Dimitra Simeonidou +# +# 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact with: +# +# Neither the name of the University of Bristol nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# This work has been performed in the context of DCMS UK 5G Testbeds +# & Trials Programme and in the framework of the Metro-Haul project - +# funded by the European Commission under Grant number 761727 through the +# Horizon 2020 and 5G-PPP programmes. +## +"""The SDN connector is responsible for establishing both wide area network connectivity (WIM) +and intranet SDN connectivity. + +It receives information from ports to be connected . +""" + +import logging +from http import HTTPStatus + + +class SdnConnectorError(Exception): + """Base Exception for all connector related errors + provide the parameter 'http_code' (int) with the error code: + Bad_Request = 400 + Unauthorized = 401 (e.g. credentials are not valid) + Not_Found = 404 (e.g. try to edit or delete a non existing connectivity service) + Forbidden = 403 + Method_Not_Allowed = 405 + Not_Acceptable = 406 + Request_Timeout = 408 (e.g timeout reaching server, or cannot reach the server) + Conflict = 409 + Service_Unavailable = 503 + Internal_Server_Error = 500 + """ + + def __init__(self, message, http_code=HTTPStatus.INTERNAL_SERVER_ERROR.value): + Exception.__init__(self, message) + self.http_code = http_code + + +class SdnConnectorBase(object): + """Abstract base class for all the SDN connectors + + Arguments: + wim (dict): WIM record, as stored in the database + wim_account (dict): WIM account record, as stored in the database + config + The arguments of the constructor are converted to object attributes. + An extra property, ``service_endpoint_mapping`` is created from ``config``. + """ + + def __init__(self, wim, wim_account, config=None, logger=None): + """ + :param wim: (dict). Contains among others 'wim_url' + :param wim_account: (dict). Contains among others 'uuid' (internal id), 'name', + 'sdn' (True if is intended for SDN-assist or False if intended for WIM), 'user', 'password'. + :param config: (dict or None): Particular information of plugin. These keys if present have a common meaning: + 'mapping_not_needed': (bool) False by default or if missing, indicates that mapping is not needed. + 'service_endpoint_mapping': (list) provides the internal endpoint mapping. The meaning is: + KEY meaning for WIM meaning for SDN assist + -------- -------- -------- + device_id pop_switch_dpid compute_id + device_interface_id pop_switch_port compute_pci_address + service_endpoint_id wan_service_endpoint_id SDN_service_endpoint_id + service_mapping_info wan_service_mapping_info SDN_service_mapping_info + contains extra information if needed. Text in Yaml format + switch_dpid wan_switch_dpid SDN_switch_dpid + switch_port wan_switch_port SDN_switch_port + datacenter_id vim_account vim_account + id: (internal, do not use) + wim_id: (internal, do not use) + :param logger (logging.Logger): optional logger object. If none is passed 'openmano.sdn.sdnconn' is used. + """ + self.logger = logger or logging.getLogger("ro.sdn") + self.wim = wim + self.wim_account = wim_account + self.config = config or {} + self.service_endpoint_mapping = self.config.get("service_endpoint_mapping", []) + + def check_credentials(self): + """Check if the connector itself can access the SDN/WIM with the provided url (wim.wim_url), + user (wim_account.user), and password (wim_account.password) + + Raises: + SdnConnectorError: Issues regarding authorization, access to + external URLs, etc are detected. + """ + raise NotImplementedError + + def get_connectivity_service_status(self, service_uuid, conn_info=None): + """Monitor the status of the connectivity service established + + Arguments: + service_uuid (str): UUID of the connectivity service + conn_info (dict or None): Information returned by the connector + during the service creation/edition and subsequently stored in + the database. + + Returns: + dict: JSON/YAML-serializable dict that contains a mandatory key + ``sdn_status`` associated with one of the following values:: + + {'sdn_status': 'ACTIVE'} + # The service is up and running. + + {'sdn_status': 'INACTIVE'} + # The service was created, but the connector + # cannot determine yet if connectivity exists + # (ideally, the caller needs to wait and check again). + + {'sdn_status': 'DOWN'} + # Connection was previously established, + # but an error/failure was detected. + + {'sdn_status': 'ERROR'} + # An error occurred when trying to create the service/ + # establish the connectivity. + + {'sdn_status': 'BUILD'} + # Still trying to create the service, the caller + # needs to wait and check again. + + Additionally ``error_msg``(**str**) and ``sdn_info``(**dict**) + keys can be used to provide additional status explanation or + new information available for the connectivity service. + """ + raise NotImplementedError + + def create_connectivity_service(self, service_type, connection_points, **kwargs): + """ + Establish SDN/WAN connectivity between the endpoints + :param service_type: (str): ``ELINE`` (L2), ``ELAN`` (L2), ``ETREE`` (L2), ``L3``. + :param connection_points: (list): each point corresponds to + an entry point to be connected. For WIM: from the DC to the transport network. + For SDN: Compute/PCI to the transport network. One + connection point serves to identify the specific access and + some other service parameters, such as encapsulation type. + Each item of the list is a dict with: + "service_endpoint_id": (str)(uuid) Same meaning that for 'service_endpoint_mapping' (see __init__) + In case the config attribute mapping_not_needed is True, this value is not relevant. In this case + it will contain the string "device_id:device_interface_id" + "service_endpoint_encapsulation_type": None, "dot1q", ... + "service_endpoint_encapsulation_info": (dict) with: + "vlan": ..., (int, present if encapsulation is dot1q) + "vni": ... (int, present if encapsulation is vxlan), + "peers": [(ipv4_1), (ipv4_2)] (present if encapsulation is vxlan) + "mac": ... + "device_id": ..., same meaning that for 'service_endpoint_mapping' (see __init__) + "device_interface_id": same meaning that for 'service_endpoint_mapping' (see __init__) + "switch_dpid": ..., present if mapping has been found for this device_id,device_interface_id + "swith_port": ... present if mapping has been found for this device_id,device_interface_id + "service_mapping_info": present if mapping has been found for this device_id,device_interface_id + :param kwargs: For future versions: + bandwidth (int): value in kilobytes + latency (int): value in milliseconds + Other QoS might be passed as keyword arguments. + :return: tuple: ``(service_id, conn_info)`` containing: + - *service_uuid* (str): UUID of the established connectivity service + - *conn_info* (dict or None): Information to be stored at the database (or ``None``). + This information will be provided to the :meth:`~.edit_connectivity_service` and :obj:`~.delete`. + **MUST** be JSON/YAML-serializable (plain data structures). + :raises: SdnConnectorException: In case of error. Nothing should be created in this case. + Provide the parameter http_code + """ + raise NotImplementedError + + def delete_connectivity_service(self, service_uuid, conn_info=None): + """ + Disconnect multi-site endpoints previously connected + + :param service_uuid: The one returned by create_connectivity_service + :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service' + if they do not return None + :return: None + :raises: SdnConnectorException: In case of error. The parameter http_code must be filled + """ + raise NotImplementedError + + def edit_connectivity_service( + self, service_uuid, conn_info=None, connection_points=None, **kwargs + ): + """Change an existing connectivity service. + + This method's arguments and return value follow the same convention as + :meth:`~.create_connectivity_service`. + + :param service_uuid: UUID of the connectivity service. + :param conn_info: (dict or None): Information previously returned by last call to create_connectivity_service + or edit_connectivity_service + :param connection_points: (list): If provided, the old list of connection points will be replaced. + :param kwargs: Same meaning that create_connectivity_service + :return: dict or None: Information to be updated and stored at the database. + When ``None`` is returned, no information should be changed. + When an empty dict is returned, the database record will be deleted. + **MUST** be JSON/YAML-serializable (plain data structures). + Raises: + SdnConnectorException: In case of error. + """ + + def clear_all_connectivity_services(self): + """Delete all WAN Links in a WIM. + + This method is intended for debugging only, and should delete all the + connections controlled by the WIM/SDN, not only the connections that + a specific RO is aware of. + + Raises: + SdnConnectorException: In case of error. + """ + raise NotImplementedError + + def get_all_active_connectivity_services(self): + """Provide information about all active connections provisioned by a + WIM. + + Raises: + SdnConnectorException: In case of error. + """ + raise NotImplementedError diff --git a/src/tests/osm_end2end/tests/test_cleanup.py b/src/tests/osm_end2end/tests/test_cleanup.py new file mode 100644 index 000000000..20afb5fe0 --- /dev/null +++ b/src/tests/osm_end2end/tests/test_cleanup.py @@ -0,0 +1,44 @@ +# 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 logging, os +from common.Constants import DEFAULT_CONTEXT_NAME +from common.proto.context_pb2 import ContextId +from common.tools.descriptor.Loader import DescriptorLoader, validate_empty_scenario +from common.tools.object_factory.Context import json_context_id +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from .Fixtures import context_client, device_client # pylint: disable=unused-import + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'tfs-topology.json') +ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME)) + +def test_scenario_cleanup( + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name +) -> None: + # Verify the scenario has no services/slices + response = context_client.GetContext(ADMIN_CONTEXT_ID) + assert len(response.service_ids) == 0 + assert len(response.slice_ids) == 0 + + # Load descriptors and validate the base scenario + descriptor_loader = DescriptorLoader( + descriptors_file=DESCRIPTOR_FILE, context_client=context_client, device_client=device_client) + descriptor_loader.validate() + descriptor_loader.unload() + validate_empty_scenario(context_client) diff --git a/src/tests/osm_end2end/tests/test_onboarding.py b/src/tests/osm_end2end/tests/test_onboarding.py new file mode 100644 index 000000000..763d7da17 --- /dev/null +++ b/src/tests/osm_end2end/tests/test_onboarding.py @@ -0,0 +1,67 @@ +# 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 logging, os, time +from common.Constants import DEFAULT_CONTEXT_NAME +from common.proto.context_pb2 import ContextId, DeviceOperationalStatusEnum, Empty +from common.tools.descriptor.Loader import DescriptorLoader, check_descriptor_load_results, validate_empty_scenario +from common.tools.object_factory.Context import json_context_id +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient +from .Fixtures import context_client, device_client # pylint: disable=unused-import + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +DESCRIPTOR_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'tfs-topology.json') +ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME)) + +def test_scenario_onboarding( + context_client : ContextClient, # pylint: disable=redefined-outer-name + device_client : DeviceClient, # pylint: disable=redefined-outer-name +) -> None: + validate_empty_scenario(context_client) + + descriptor_loader = DescriptorLoader( + descriptors_file=DESCRIPTOR_FILE, context_client=context_client, device_client=device_client) + results = descriptor_loader.process() + check_descriptor_load_results(results, descriptor_loader) + descriptor_loader.validate() + + # Verify the scenario has no services/slices + response = context_client.GetContext(ADMIN_CONTEXT_ID) + assert len(response.service_ids) == 0 + assert len(response.slice_ids) == 0 + +def test_scenario_devices_enabled( + context_client : ContextClient, # pylint: disable=redefined-outer-name +) -> None: + """ + This test validates that the devices are enabled. + """ + DEVICE_OP_STATUS_ENABLED = DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_ENABLED + + num_devices = -1 + num_devices_enabled, num_retry = 0, 0 + while (num_devices != num_devices_enabled) and (num_retry < 10): + time.sleep(1.0) + response = context_client.ListDevices(Empty()) + num_devices = len(response.devices) + num_devices_enabled = 0 + for device in response.devices: + if device.device_operational_status != DEVICE_OP_STATUS_ENABLED: continue + num_devices_enabled += 1 + LOGGER.info('Num Devices enabled: {:d}/{:d}'.format(num_devices_enabled, num_devices)) + num_retry += 1 + assert num_devices_enabled == num_devices diff --git a/src/tests/osm_end2end/tests/test_osm_service_create.py b/src/tests/osm_end2end/tests/test_osm_service_create.py new file mode 100644 index 000000000..1646f9a0c --- /dev/null +++ b/src/tests/osm_end2end/tests/test_osm_service_create.py @@ -0,0 +1,77 @@ +# 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 logging +from common.Constants import DEFAULT_CONTEXT_NAME +from common.proto.context_pb2 import ContextId, ServiceStatusEnum, ServiceTypeEnum +from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.Context import json_context_id +from context.client.ContextClient import ContextClient +from .Fixtures import ( # pylint: disable=unused-import + # be careful, order of symbols is important here! + osm_wim, context_client +) +from .MockOSM import MockOSM +from .OSM_Constants import SERVICE_CONNECTION_POINTS, SERVICE_TYPE + + +logging.getLogger('ro.sdn.ietfl2vpn').setLevel(logging.DEBUG) +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME)) + + +# pylint: disable=redefined-outer-name, unused-argument +def test_osm_service_create( + osm_wim : MockOSM, context_client : ContextClient +): + osm_wim.create_connectivity_service(SERVICE_TYPE, SERVICE_CONNECTION_POINTS) + service_uuid = list(osm_wim.conn_info.keys())[0] # this test adds a single service + + result = osm_wim.get_connectivity_service_status(service_uuid) + assert 'sdn_status' in result + assert result['sdn_status'] == 'ACTIVE' + + # Verify the scenario has 1 service and 0 slices + response = context_client.GetContext(ADMIN_CONTEXT_ID) + assert len(response.service_ids) == 1 + assert len(response.slice_ids) == 0 + + # Check there are no slices + response = context_client.ListSlices(ADMIN_CONTEXT_ID) + LOGGER.warning('Slices[{:d}] = {:s}'.format( + len(response.slices), grpc_message_to_json_string(response) + )) + assert len(response.slices) == 0 + + # Check there is 1 service + response = context_client.ListServices(ADMIN_CONTEXT_ID) + LOGGER.warning('Services[{:d}] = {:s}'.format( + len(response.services), grpc_message_to_json_string(response) + )) + assert len(response.services) == 1 + + for service in response.services: + service_id = service.service_id + assert service_id.service_uuid.uuid == service_uuid + assert service.service_status.service_status == ServiceStatusEnum.SERVICESTATUS_ACTIVE + assert service.service_type == ServiceTypeEnum.SERVICETYPE_L3NM + + response = context_client.ListConnections(service_id) + LOGGER.warning(' ServiceId[{:s}] => Connections[{:d}] = {:s}'.format( + grpc_message_to_json_string(service_id), len(response.connections), + grpc_message_to_json_string(response) + )) + assert len(response.connections) == 1 diff --git a/src/tests/osm_end2end/tests/test_osm_service_remove.py b/src/tests/osm_end2end/tests/test_osm_service_remove.py new file mode 100644 index 000000000..d893a9c1c --- /dev/null +++ b/src/tests/osm_end2end/tests/test_osm_service_remove.py @@ -0,0 +1,84 @@ +# 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 logging +from typing import Set +from common.Constants import DEFAULT_CONTEXT_NAME +from common.proto.context_pb2 import ContextId, ServiceStatusEnum, ServiceTypeEnum +from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.Context import json_context_id +from context.client.ContextClient import ContextClient +from .Fixtures import ( # pylint: disable=unused-import + # be careful, order of symbols is important here! + osm_wim, context_client +) +from .MockOSM import MockOSM + + +logging.getLogger('ro.sdn.ietfl2vpn').setLevel(logging.DEBUG) +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME)) + + +# pylint: disable=redefined-outer-name, unused-argument +def test_osm_service_remove( + osm_wim : MockOSM, context_client : ContextClient +): + # Verify the scenario has 1 service and 0 slices + response = context_client.GetContext(ADMIN_CONTEXT_ID) + assert len(response.service_ids) == 1 + assert len(response.slice_ids) == 0 + + # Check there are no slices + response = context_client.ListSlices(ADMIN_CONTEXT_ID) + LOGGER.warning('Slices[{:d}] = {:s}'.format( + len(response.slices), grpc_message_to_json_string(response) + )) + assert len(response.slices) == 0 + + # Check there is 1 service + response = context_client.ListServices(ADMIN_CONTEXT_ID) + LOGGER.warning('Services[{:d}] = {:s}'.format( + len(response.services), grpc_message_to_json_string(response) + )) + assert len(response.services) == 1 + + service_uuids : Set[str] = set() + for service in response.services: + service_id = service.service_id + assert service.service_status.service_status == ServiceStatusEnum.SERVICESTATUS_ACTIVE + assert service.service_type == ServiceTypeEnum.SERVICETYPE_L3NM + + response = context_client.ListConnections(service_id) + LOGGER.warning(' ServiceId[{:s}] => Connections[{:d}] = {:s}'.format( + grpc_message_to_json_string(service_id), len(response.connections), + grpc_message_to_json_string(response) + )) + assert len(response.connections) == 1 + + service_uuids.add(service_id.service_uuid.uuid) + + # Identify service to delete + assert len(service_uuids) == 1 + service_uuid = service_uuids.pop() + + osm_wim.conn_info[service_uuid] = dict() # delete just needs the placeholder to be populated + osm_wim.delete_connectivity_service(service_uuid) + + # Verify the scenario has no services/slices + response = context_client.GetContext(ADMIN_CONTEXT_ID) + assert len(response.service_ids) == 0 + assert len(response.slice_ids) == 0 -- GitLab From 16ed378862c6773786cb560f912c3658a030a444 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 22 Jan 2026 19:28:31 +0000 Subject: [PATCH 04/72] Service component - L3NM gNMI/OpenConfig service handler: - Minor code formatting --- .../L3NMGnmiOpenConfigServiceHandler.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py index 4517327e1..9a0480adc 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py @@ -65,9 +65,13 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): self.__endpoint_map[(device_uuid, endpoint_uuid)] = (device_obj.name, endpoint_obj.name) - LOGGER.debug('[pre] config_rule_composer = {:s}'.format(json.dumps(self.__config_rule_composer.dump()))) + MSG = '[pre] config_rule_composer = {:s}' + LOGGER.debug(MSG.format(json.dumps(self.__config_rule_composer.dump()))) + self.__static_route_generator.compose(endpoints) - LOGGER.debug('[post] config_rule_composer = {:s}'.format(json.dumps(self.__config_rule_composer.dump()))) + + MSG = '[post] config_rule_composer = {:s}' + LOGGER.debug(MSG.format(json.dumps(self.__config_rule_composer.dump()))) def _do_configurations( self, config_rules_per_device : Dict[str, List[Dict]], endpoints : List[Tuple[str, str, Optional[str]]], -- GitLab From 4fe67bb9a301da44cb22f7559589ff8cc6a1996f Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 22 Jan 2026 19:28:51 +0000 Subject: [PATCH 05/72] OSM End-to-End integration test: - Fixed ContainerLab scenario --- src/tests/osm_end2end/clab/osm_end2end.clab.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tests/osm_end2end/clab/osm_end2end.clab.yml b/src/tests/osm_end2end/clab/osm_end2end.clab.yml index 2560cd3e4..af98be9e1 100644 --- a/src/tests/osm_end2end/clab/osm_end2end.clab.yml +++ b/src/tests/osm_end2end/clab/osm_end2end.clab.yml @@ -62,7 +62,7 @@ topology: - ip link add link eth1 name eth1.125 type vlan id 125 - ip address add 172.16.1.10/24 dev eth1.125 - ip link set eth1.125 up - - ip route add 172.16.2.0/24 via 172.16.1.1 + - ip route add 172.16.3.0/24 via 172.16.1.1 dc2: kind: linux @@ -71,9 +71,9 @@ topology: - ip link set address 00:c1:ab:00:02:0a dev eth1 - ip link set eth1 up - ip link add link eth1 name eth1.125 type vlan id 125 - - ip address add 172.16.2.10/24 dev eth1.125 + - ip address add 172.16.3.10/24 dev eth1.125 - ip link set eth1.125 up - - ip route add 172.16.1.0/24 via 172.16.2.1 + - ip route add 172.16.1.0/24 via 172.16.3.1 links: - endpoints: ["r1:eth2", "r2:eth1"] -- GitLab From e2cb9a565ce6e21e2d9ba02e5bb07810fff9addb Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 22 Jan 2026 19:50:07 +0000 Subject: [PATCH 06/72] Service component - L3NM gNMI OpenConfig: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added support for VLAN tags and their propagation in L2/L· mixed environments --- .../ConfigRuleComposer.py | 111 ++++++++++++++++-- .../L3NMGnmiOpenConfigServiceHandler.py | 4 + .../l3nm_gnmi_openconfig/VlanIdPropagator.py | 87 ++++++++++++++ .../MockServiceHandler.py | 6 +- 4 files changed, 197 insertions(+), 11 deletions(-) create mode 100644 src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index 6857bce61..5e0ef4823 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -30,6 +30,27 @@ RE_IF = re.compile(r'^\/interface\[([^\]]+)\]$') RE_SUBIF = re.compile(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$') RE_SR = re.compile(r'^\/network_instance\[([^\]]+)\]\/protocols\[STATIC\]/route\[([^\:]+)\:([^\]]+)\]$') +def _safe_int(value: Optional[object]) -> Optional[int]: + try: + return int(value) if value is not None else None + except (TypeError, ValueError): + return None + +def _safe_bool(value: Optional[object]) -> Optional[bool]: + if value is None: + return None + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return bool(value) + if isinstance(value, str): + lowered = value.strip().lower() + if lowered in {'true', '1', 'yes', 'y', 'on', 'tagged'}: + return True + if lowered in {'false', '0', 'no', 'n', 'off', 'untagged'}: + return False + return None + def _interface( interface : str, if_type : Optional[str] = 'l3ipvlan', index : int = 0, vlan_id : Optional[int] = None, address_ip : Optional[str] = None, address_prefix : Optional[int] = None, mtu : Optional[int] = None, @@ -82,12 +103,28 @@ class EndpointComposer: self.sub_interface_index = 0 self.ipv4_address = None self.ipv4_prefix_len = None + self.explicit_vlan_ids : Set[int] = set() + self.force_trunk = False + + def _add_vlan_id(self, vlan_id : Optional[int]) -> None: + if vlan_id is not None: + self.explicit_vlan_ids.add(vlan_id) + + def _configure_from_settings(self, json_settings : Dict) -> None: + if not isinstance(json_settings, dict): + return + vlan_id = _safe_int(json_settings.get('vlan_id', json_settings.get('vlan-id'))) + self._add_vlan_id(vlan_id) def configure(self, endpoint_obj : Optional[EndPoint], settings : Optional[TreeNode]) -> None: if endpoint_obj is not None: self.objekt = endpoint_obj if settings is None: return - json_settings : Dict = settings.value + json_settings : Dict = settings.value or dict() + self._configure_from_settings(json_settings) + for child in settings.children: + if isinstance(child.value, dict): + self._configure_from_settings(child.value) if 'address_ip' in json_settings: self.ipv4_address = json_settings['address_ip'] @@ -107,7 +144,20 @@ class EndpointComposer: self.sub_interface_index = json_settings.get('index', 0) - def get_config_rules(self, network_instance_name : str, delete : bool = False) -> List[Dict]: + def set_force_trunk(self, enable : bool = True) -> None: + self.force_trunk = enable + + def _select_vlan_id(self, service_vlan_id : Optional[int]) -> Optional[int]: + if service_vlan_id is not None and service_vlan_id in self.explicit_vlan_ids: + return service_vlan_id + if len(self.explicit_vlan_ids) > 0: + return sorted(self.explicit_vlan_ids)[0] + return service_vlan_id + + def get_config_rules( + self, network_instance_name : str, service_vlan_id : Optional[int] = None, + access_vlan_tagged : bool = False, delete : bool = False + ) -> List[Dict]: if self.ipv4_address is None: return [] if self.ipv4_prefix_len is None: return [] json_config_rule = json_config_rule_delete if delete else json_config_rule_set @@ -118,18 +168,24 @@ class EndpointComposer: network_instance_name, self.objekt.name, self.sub_interface_index ))) + vlan_id = None + if self.force_trunk or access_vlan_tagged or len(self.explicit_vlan_ids) > 0: + vlan_id = self._select_vlan_id(service_vlan_id) + if vlan_id is None: + LOGGER.warning('VLAN tagging requested but no vlan_id provided for endpoint={:s}'.format(self.uuid)) + if delete: config_rules.extend([ json_config_rule(*_interface( self.objekt.name, index=self.sub_interface_index, address_ip=None, - address_prefix=None, enabled=False + address_prefix=None, enabled=False, vlan_id=vlan_id )), ]) else: config_rules.extend([ json_config_rule(*_interface( self.objekt.name, index=self.sub_interface_index, address_ip=self.ipv4_address, - address_prefix=self.ipv4_prefix_len, enabled=True + address_prefix=self.ipv4_prefix_len, enabled=True, vlan_id=vlan_id )), ]) return config_rules @@ -139,6 +195,8 @@ class EndpointComposer: 'index' : self.sub_interface_index, 'address_ip' : self.ipv4_address, 'address_prefix': self.ipv4_prefix_len, + 'explicit_vlan_ids': list(self.explicit_vlan_ids), + 'force_trunk' : self.force_trunk, } def __str__(self): @@ -155,6 +213,8 @@ class DeviceComposer: self.endpoints : Dict[str, EndpointComposer] = dict() # endpoint_uuid => EndpointComposer self.connected : Set[str] = set() self.static_routes : Dict[str, Dict[int, str]] = dict() # {prefix => {metric => next_hop}} + self.service_vlan_id : Optional[int] = None + self.access_vlan_tagged = False def set_endpoint_alias(self, endpoint_name : str, endpoint_uuid : str) -> None: self.aliases[endpoint_name] = endpoint_uuid @@ -225,7 +285,12 @@ class DeviceComposer: metric = static_route.get('metric', 0) self.static_routes.setdefault(prefix, dict())[metric] = next_hop - def get_config_rules(self, network_instance_name : str, delete : bool = False) -> List[Dict]: + def get_config_rules( + self, network_instance_name : str, service_vlan_id : Optional[int] = None, + access_vlan_tagged : bool = False, delete : bool = False + ) -> List[Dict]: + self.service_vlan_id = service_vlan_id + self.access_vlan_tagged = access_vlan_tagged SELECTED_DEVICES = { DeviceTypeEnum.PACKET_POP.value, DeviceTypeEnum.PACKET_ROUTER.value, @@ -238,7 +303,10 @@ class DeviceComposer: if network_instance_name != DEFAULT_NETWORK_INSTANCE: json_config_rule(*_network_instance(network_instance_name, 'L3VRF')) for endpoint in self.endpoints.values(): - config_rules.extend(endpoint.get_config_rules(network_instance_name, delete=delete)) + config_rules.extend(endpoint.get_config_rules( + network_instance_name, self.service_vlan_id, + access_vlan_tagged=self.access_vlan_tagged, delete=delete + )) if len(self.static_routes) > 0: config_rules.append( json_config_rule(*_network_instance_protocol_static(network_instance_name)) @@ -274,6 +342,8 @@ class ConfigRuleComposer: self.objekt : Optional[Service] = None self.aliases : Dict[str, str] = dict() # device_name => device_uuid self.devices : Dict[str, DeviceComposer] = dict() # device_uuid => DeviceComposer + self.vlan_id : Optional[int] = None + self.access_vlan_tagged = False def set_device_alias(self, device_name : str, device_uuid : str) -> None: self.aliases[device_name] = device_uuid @@ -286,15 +356,34 @@ class ConfigRuleComposer: def configure(self, service_obj : Service, settings : Optional[TreeNode]) -> None: self.objekt = service_obj + self.vlan_id = None + self.access_vlan_tagged = False if settings is None: return - #json_settings : Dict = settings.value - # For future use + json_settings : Dict = settings.value or dict() + + if 'vlan_id' in json_settings: + self.vlan_id = _safe_int(json_settings['vlan_id']) + elif 'vlan-id' in json_settings: + self.vlan_id = _safe_int(json_settings['vlan-id']) + + if 'access_vlan_tagged' in json_settings or 'access-vlan-tagged' in json_settings: + access_vlan_tagged = json_settings.get('access_vlan_tagged', json_settings.get('access-vlan-tagged')) + parsed = _safe_bool(access_vlan_tagged) + if parsed is None: + MSG = 'Invalid access_vlan_tagged value in service settings: {:s}' + LOGGER.warning(MSG.format(str(access_vlan_tagged))) + self.access_vlan_tagged = False + else: + self.access_vlan_tagged = parsed def get_config_rules( self, network_instance_name : str = NETWORK_INSTANCE, delete : bool = False ) -> Dict[str, List[Dict]]: return { - device_uuid : device.get_config_rules(network_instance_name, delete=delete) + device_uuid : device.get_config_rules( + network_instance_name, self.vlan_id, + access_vlan_tagged=self.access_vlan_tagged, delete=delete + ) for device_uuid, device in self.devices.items() } @@ -303,5 +392,7 @@ class ConfigRuleComposer: 'devices' : { device_uuid : device.dump() for device_uuid, device in self.devices.items() - } + }, + 'vlan_id': self.vlan_id, + 'access_vlan_tagged': self.access_vlan_tagged, } diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py index 4517327e1..e877c6c8d 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py @@ -26,6 +26,7 @@ from service.service.task_scheduler.TaskExecutor import TaskExecutor from service.service.tools.EndpointIdFormatters import endpointids_to_raw from .ConfigRuleComposer import ConfigRuleComposer from .StaticRouteGenerator import StaticRouteGenerator +from .VlanIdPropagator import VlanIdPropagator LOGGER = logging.getLogger(__name__) @@ -39,6 +40,7 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): self.__task_executor = task_executor self.__settings_handler = SettingsHandler(service.service_config, **settings) self.__config_rule_composer = ConfigRuleComposer() + self.__vlan_id_propagator = VlanIdPropagator(self.__config_rule_composer) self.__static_route_generator = StaticRouteGenerator(self.__config_rule_composer) self.__endpoint_map : Dict[Tuple[str, str], Tuple[str, str]] = dict() @@ -66,6 +68,8 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): self.__endpoint_map[(device_uuid, endpoint_uuid)] = (device_obj.name, endpoint_obj.name) LOGGER.debug('[pre] config_rule_composer = {:s}'.format(json.dumps(self.__config_rule_composer.dump()))) + self.__vlan_id_propagator.compose(endpoints) + LOGGER.debug('[post-vlan] config_rule_composer = {:s}'.format(json.dumps(self.__config_rule_composer.dump()))) self.__static_route_generator.compose(endpoints) LOGGER.debug('[post] config_rule_composer = {:s}'.format(json.dumps(self.__config_rule_composer.dump()))) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py new file mode 100644 index 000000000..69e2afb62 --- /dev/null +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py @@ -0,0 +1,87 @@ +# 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, logging +from typing import List, Optional, Tuple +from common.DeviceTypes import DeviceTypeEnum +from .ConfigRuleComposer import ConfigRuleComposer + +LOGGER = logging.getLogger(__name__) + +class VlanIdPropagator: + def __init__(self, config_rule_composer : ConfigRuleComposer) -> None: + self._config_rule_composer = config_rule_composer + self._router_types = { + DeviceTypeEnum.PACKET_ROUTER.value, + DeviceTypeEnum.EMULATED_PACKET_ROUTER.value, + DeviceTypeEnum.PACKET_POP.value, + DeviceTypeEnum.PACKET_RADIO_ROUTER.value, + DeviceTypeEnum.EMULATED_PACKET_RADIO_ROUTER.value, + } + + def _is_router_device(self, device) -> bool: + return device.objekt is not None and device.objekt.device_type in self._router_types + + def compose(self, connection_hop_list : List[Tuple[str, str, Optional[str]]]) -> None: + link_endpoints = self._compute_link_endpoints(connection_hop_list) + LOGGER.debug('link_endpoints = {:s}'.format(str(link_endpoints))) + + self._propagate_vlan_id(link_endpoints) + LOGGER.debug('config_rule_composer = {:s}'.format(json.dumps(self._config_rule_composer.dump()))) + + def _compute_link_endpoints( + self, connection_hop_list : List[Tuple[str, str, Optional[str]]] + ) -> List[Tuple[Tuple[str, str, Optional[str]], Tuple[str, str, Optional[str]]]]: + # In some cases connection_hop_list might contain repeated endpoints, remove them here. + added_connection_hops = set() + filtered_connection_hop_list = list() + for connection_hop in connection_hop_list: + if connection_hop in added_connection_hops: continue + filtered_connection_hop_list.append(connection_hop) + added_connection_hops.add(connection_hop) + connection_hop_list = filtered_connection_hop_list + + # In some cases connection_hop_list first and last items might be internal endpoints of + # devices instead of link endpoints. Filter those endpoints not reaching a new device. + if len(connection_hop_list) > 2 and connection_hop_list[0][0] == connection_hop_list[1][0]: + # same device on first 2 endpoints + connection_hop_list = connection_hop_list[1:] + if len(connection_hop_list) > 2 and connection_hop_list[-1][0] == connection_hop_list[-2][0]: + # same device on last 2 endpoints + connection_hop_list = connection_hop_list[:-1] + + num_connection_hops = len(connection_hop_list) + if num_connection_hops % 2 != 0: raise Exception('Number of connection hops must be even') + if num_connection_hops < 4: raise Exception('Number of connection hops must be >= 4') + + it_connection_hops = iter(connection_hop_list) + return list(zip(it_connection_hops, it_connection_hops)) + + def _propagate_vlan_id( + self, link_endpoints_list : List[Tuple[Tuple[str, str, Optional[str]], Tuple[str, str, Optional[str]]]] + ) -> None: + for link_endpoints in link_endpoints_list: + device_endpoint_a, device_endpoint_b = link_endpoints + + device_uuid_a, endpoint_uuid_a = device_endpoint_a[0:2] + device_a = self._config_rule_composer.get_device(device_uuid_a) + endpoint_a = device_a.get_endpoint(endpoint_uuid_a) + + device_uuid_b, endpoint_uuid_b = device_endpoint_b[0:2] + device_b = self._config_rule_composer.get_device(device_uuid_b) + endpoint_b = device_b.get_endpoint(endpoint_uuid_b) + + if self._is_router_device(device_a) and self._is_router_device(device_b): + endpoint_a.set_force_trunk() + endpoint_b.set_force_trunk() diff --git a/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockServiceHandler.py b/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockServiceHandler.py index 7abe201e0..2d95a5ede 100644 --- a/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockServiceHandler.py +++ b/src/service/tests/test_l3nm_gnmi_static_rule_gen/MockServiceHandler.py @@ -26,6 +26,7 @@ from .MockTaskExecutor import MockTaskExecutor from service.service.tools.EndpointIdFormatters import endpointids_to_raw from service.service.service_handlers.l3nm_gnmi_openconfig.ConfigRuleComposer import ConfigRuleComposer from service.service.service_handlers.l3nm_gnmi_openconfig.StaticRouteGenerator import StaticRouteGenerator +from service.service.service_handlers.l3nm_gnmi_openconfig.VlanIdPropagator import VlanIdPropagator LOGGER = logging.getLogger(__name__) @@ -37,6 +38,7 @@ class MockServiceHandler(_ServiceHandler): self.__task_executor = task_executor self.__settings_handler = SettingsHandler(service.service_config, **settings) self.__config_rule_composer = ConfigRuleComposer() + self.__vlan_id_propagator = VlanIdPropagator(self.__config_rule_composer) self.__static_route_generator = StaticRouteGenerator(self.__config_rule_composer) self.__endpoint_map : Dict[Tuple[str, str], Tuple[str, str]] = dict() @@ -94,8 +96,10 @@ class MockServiceHandler(_ServiceHandler): #prev_endpoint = _endpoint #prev_endpoint_obj = endpoint_obj + self.__vlan_id_propagator.compose(endpoints) + LOGGER.debug('[post-vlan] config_rule_composer = {:s}'.format(json.dumps(self.__config_rule_composer.dump()))) self.__static_route_generator.compose(endpoints) - LOGGER.debug('config_rule_composer = {:s}'.format(json.dumps(self.__config_rule_composer.dump()))) + LOGGER.debug('[post] config_rule_composer = {:s}'.format(json.dumps(self.__config_rule_composer.dump()))) def _do_configurations( self, config_rules_per_device : Dict[str, List[Dict]], endpoints : List[Tuple[str, str, Optional[str]]], -- GitLab From 9898e88447393593dddd7d7a217aa65eebcace30 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 22 Jan 2026 20:09:02 +0000 Subject: [PATCH 07/72] Service component - L3NM gNMI OpenConfig: - Fixed selection of subinterface index --- .../l3nm_gnmi_openconfig/ConfigRuleComposer.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index 5e0ef4823..a126df95a 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -163,28 +163,29 @@ class EndpointComposer: json_config_rule = json_config_rule_delete if delete else json_config_rule_set config_rules : List[Dict] = list() - if network_instance_name != DEFAULT_NETWORK_INSTANCE: - config_rules.append(json_config_rule(*_network_instance_interface( - network_instance_name, self.objekt.name, self.sub_interface_index - ))) - vlan_id = None if self.force_trunk or access_vlan_tagged or len(self.explicit_vlan_ids) > 0: vlan_id = self._select_vlan_id(service_vlan_id) if vlan_id is None: LOGGER.warning('VLAN tagging requested but no vlan_id provided for endpoint={:s}'.format(self.uuid)) + sub_interface_index = vlan_id if vlan_id is not None else self.sub_interface_index + + if network_instance_name != DEFAULT_NETWORK_INSTANCE: + config_rules.append(json_config_rule(*_network_instance_interface( + network_instance_name, self.objekt.name, sub_interface_index + ))) if delete: config_rules.extend([ json_config_rule(*_interface( - self.objekt.name, index=self.sub_interface_index, address_ip=None, + self.objekt.name, index=sub_interface_index, address_ip=None, address_prefix=None, enabled=False, vlan_id=vlan_id )), ]) else: config_rules.extend([ json_config_rule(*_interface( - self.objekt.name, index=self.sub_interface_index, address_ip=self.ipv4_address, + self.objekt.name, index=sub_interface_index, address_ip=self.ipv4_address, address_prefix=self.ipv4_prefix_len, enabled=True, vlan_id=vlan_id )), ]) -- GitLab From d8f035b567bc6e63d6aa2631591bfc757f1b2e2e Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 22 Jan 2026 22:23:25 +0000 Subject: [PATCH 08/72] Service component - L3NM gNMI OpenConfig: - Fixed configuration of VLAN --- .../l3nm_gnmi_openconfig/ConfigRuleComposer.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index a126df95a..b3273f9ba 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -70,6 +70,11 @@ def _network_instance(ni_name : str, ni_type : str) -> Tuple[str, Dict]: data = {'name': ni_name, 'type': ni_type} return path, data +def _network_instance_vlan(ni_name : str, vlan_id : int, vlan_name : str = None) -> Tuple[str, Dict]: + path = '/network_instance[{:s}]/vlan[{:s}]'.format(ni_name, str(vlan_id)) + data = {'name': ni_name, 'vlan_id': vlan_id, 'vlan_name': vlan_name} + return path, data + def _network_instance_protocol(ni_name : str, protocol : str) -> Tuple[str, Dict]: path = '/network_instance[{:s}]/protocols[{:s}]'.format(ni_name, protocol) data = {'name': ni_name, 'identifier': protocol, 'protocol_name': protocol} @@ -216,6 +221,7 @@ class DeviceComposer: self.static_routes : Dict[str, Dict[int, str]] = dict() # {prefix => {metric => next_hop}} self.service_vlan_id : Optional[int] = None self.access_vlan_tagged = False + self.vlan_ids : Set[int] = set() def set_endpoint_alias(self, endpoint_name : str, endpoint_uuid : str) -> None: self.aliases[endpoint_name] = endpoint_uuid @@ -292,6 +298,9 @@ class DeviceComposer: ) -> List[Dict]: self.service_vlan_id = service_vlan_id self.access_vlan_tagged = access_vlan_tagged + self.vlan_ids = set() + if self.service_vlan_id is not None: + self.vlan_ids.add(self.service_vlan_id) SELECTED_DEVICES = { DeviceTypeEnum.PACKET_POP.value, DeviceTypeEnum.PACKET_ROUTER.value, @@ -308,6 +317,13 @@ class DeviceComposer: network_instance_name, self.service_vlan_id, access_vlan_tagged=self.access_vlan_tagged, delete=delete )) + self.vlan_ids.update(endpoint.explicit_vlan_ids) + + for vlan_id in sorted(self.vlan_ids): + vlan_name = 'tfs-vlan-{:s}'.format(str(vlan_id)) + config_rules.append(json_config_rule(*_network_instance_vlan( + network_instance_name, vlan_id, vlan_name=vlan_name + ))) if len(self.static_routes) > 0: config_rules.append( json_config_rule(*_network_instance_protocol_static(network_instance_name)) -- GitLab From 4f1610ae63e146383a055fadc6c1bb09d6fed956 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 07:42:51 +0000 Subject: [PATCH 09/72] NBI component - IETF L2VPN connector: - Fixed management of locations --- .../ietf_l2vpn/L2VPN_SiteNetworkAccesses.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py b/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py index bd9706f31..e11beb62c 100644 --- a/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py +++ b/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py @@ -88,12 +88,25 @@ class L2VPN_SiteNetworkAccesses(Resource): location_refs = set() location_refs.add('fake-location') + device_refs = dict() + device_refs['fake-device'] = 'fake-location' + # Add mandatory fields OSM RO driver skips and fix wrong ones for site_network_access in site_network_accesses: + location = 'fake-location' if 'location-reference' in site_network_access: - location_refs.add(site_network_access['location-reference']) + location = site_network_access['location-reference'] + site_network_access.pop('location-reference') + #else: + # site_network_access['location-reference'] = location + location_refs.add(location) + + if 'device-reference' in site_network_access: + device = site_network_access['device-reference'] else: - site_network_access['location-reference'] = 'fake-location' + device = 'fake-device' + site_network_access['device-reference'] = device + device_refs[device] = location if 'connection' in site_network_access: connection = site_network_access['connection'] @@ -151,11 +164,15 @@ class L2VPN_SiteNetworkAccesses(Resource): 'sites': {'site': [{ 'site-id': site_id, 'default-ce-vlan-id': 1, - 'management': {'type': 'customer-managed'}, + 'management': {'type': 'provider-managed'}, 'locations': {'location': [ {'location-id': location_ref} for location_ref in location_refs ]}, + 'devices': {'device': [ + {'device-id': device_ref, 'location': location_ref} + for device_ref, location_ref in device_refs.items() + ]}, 'site-network-accesses': { 'site-network-access': site_network_accesses } @@ -163,7 +180,7 @@ class L2VPN_SiteNetworkAccesses(Resource): }} MSG = '[_prepare_request_payload] request_data={:s}' - LOGGER.debug(MSG.format(str(request_data))) + LOGGER.warning(MSG.format(str(request_data))) return request_data errors.append('Unexpected request: {:s}'.format(str(request_data))) -- GitLab From 560abc86527369b557bfcd3869b4df8b663c5018 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 09:57:11 +0000 Subject: [PATCH 10/72] Service component - L3NM gNMI OpenConfig: - Fixed activation of parent interfaces as routed --- .../l3nm_gnmi_openconfig/ConfigRuleComposer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index b3273f9ba..cfe4d1d62 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -312,7 +312,15 @@ class DeviceComposer: config_rules : List[Dict] = list() if network_instance_name != DEFAULT_NETWORK_INSTANCE: json_config_rule(*_network_instance(network_instance_name, 'L3VRF')) + + parent_interfaces : Set[str] = set() for endpoint in self.endpoints.values(): + if not delete and endpoint.objekt is not None: + if endpoint.objekt.name not in parent_interfaces: + config_rules.append(json_config_rule_set(*_interface( + endpoint.objekt.name, index=0, address_ip=None, address_prefix=None, enabled=True + ))) + parent_interfaces.add(endpoint.objekt.name) config_rules.extend(endpoint.get_config_rules( network_instance_name, self.service_vlan_id, access_vlan_tagged=self.access_vlan_tagged, delete=delete -- GitLab From c03b57bf773d9769775907b991480fe4551f2730 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 14:02:09 +0000 Subject: [PATCH 11/72] End-to-end test - OSM End-to-End: - Fixed ContainerLab scenario and startup files - Added target configs per router as reference --- .../osm_end2end/clab/osm_end2end.clab.yml | 6 +- src/tests/osm_end2end/clab/r1-startup.cfg | 2 +- src/tests/osm_end2end/clab/r2-startup.cfg | 2 +- src/tests/osm_end2end/clab/r3-startup.cfg | 2 +- .../clab/target_config/r1-startup.cfg | 62 ++++++++++++++++++ .../clab/target_config/r2-startup.cfg | 63 +++++++++++++++++++ .../clab/target_config/r3-startup.cfg | 62 ++++++++++++++++++ 7 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 src/tests/osm_end2end/clab/target_config/r1-startup.cfg create mode 100644 src/tests/osm_end2end/clab/target_config/r2-startup.cfg create mode 100644 src/tests/osm_end2end/clab/target_config/r3-startup.cfg diff --git a/src/tests/osm_end2end/clab/osm_end2end.clab.yml b/src/tests/osm_end2end/clab/osm_end2end.clab.yml index 2560cd3e4..af98be9e1 100644 --- a/src/tests/osm_end2end/clab/osm_end2end.clab.yml +++ b/src/tests/osm_end2end/clab/osm_end2end.clab.yml @@ -62,7 +62,7 @@ topology: - ip link add link eth1 name eth1.125 type vlan id 125 - ip address add 172.16.1.10/24 dev eth1.125 - ip link set eth1.125 up - - ip route add 172.16.2.0/24 via 172.16.1.1 + - ip route add 172.16.3.0/24 via 172.16.1.1 dc2: kind: linux @@ -71,9 +71,9 @@ topology: - ip link set address 00:c1:ab:00:02:0a dev eth1 - ip link set eth1 up - ip link add link eth1 name eth1.125 type vlan id 125 - - ip address add 172.16.2.10/24 dev eth1.125 + - ip address add 172.16.3.10/24 dev eth1.125 - ip link set eth1.125 up - - ip route add 172.16.1.0/24 via 172.16.2.1 + - ip route add 172.16.1.0/24 via 172.16.3.1 links: - endpoints: ["r1:eth2", "r2:eth1"] diff --git a/src/tests/osm_end2end/clab/r1-startup.cfg b/src/tests/osm_end2end/clab/r1-startup.cfg index 712797deb..b7feebe06 100644 --- a/src/tests/osm_end2end/clab/r1-startup.cfg +++ b/src/tests/osm_end2end/clab/r1-startup.cfg @@ -1,4 +1,4 @@ -! device: r1 (cEOSLab, EOS-4.34.4M) +! device: r1 (cEOSLab, EOS-4.32.2F-38195967.4322F (engineering build)) ! no aaa root ! diff --git a/src/tests/osm_end2end/clab/r2-startup.cfg b/src/tests/osm_end2end/clab/r2-startup.cfg index 6a1133703..e1ab661a0 100644 --- a/src/tests/osm_end2end/clab/r2-startup.cfg +++ b/src/tests/osm_end2end/clab/r2-startup.cfg @@ -1,4 +1,4 @@ -! device: r2 (cEOSLab, EOS-4.34.4M) +! device: r2 (cEOSLab, EOS-4.32.2F-38195967.4322F (engineering build)) ! no aaa root ! diff --git a/src/tests/osm_end2end/clab/r3-startup.cfg b/src/tests/osm_end2end/clab/r3-startup.cfg index 946de6f77..63c062593 100644 --- a/src/tests/osm_end2end/clab/r3-startup.cfg +++ b/src/tests/osm_end2end/clab/r3-startup.cfg @@ -1,4 +1,4 @@ -! device: r3 (cEOSLab, EOS-4.34.4M) +! device: r3 (cEOSLab, EOS-4.32.2F-38195967.4322F (engineering build)) ! no aaa root ! diff --git a/src/tests/osm_end2end/clab/target_config/r1-startup.cfg b/src/tests/osm_end2end/clab/target_config/r1-startup.cfg new file mode 100644 index 000000000..b70dd3a23 --- /dev/null +++ b/src/tests/osm_end2end/clab/target_config/r1-startup.cfg @@ -0,0 +1,62 @@ +! device: r1 (cEOSLab, EOS-4.32.2F-38195967.4322F (engineering build)) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$OmfaAwJRg/r44r5U$9Fca1O1G6Bgsd4NKwSyvdRJcHHk71jHAR3apDWAgSTN/t/j1iroEhz5J36HjWjOF/jEVC/R8Wa60VmbX6.cr70 +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname r1 +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +vlan 125 + name tfs-vlan-125 +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet2 + no switchport +! +interface Ethernet2.125 + encapsulation dot1q vlan 125 + ip address 10.254.172.69/30 +! +interface Ethernet10 + no switchport +! +interface Ethernet10.125 + encapsulation dot1q vlan 125 + ip address 172.16.1.1/24 +! +interface Management0 + ip address 172.20.20.101/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +ip route 172.16.3.0/24 10.254.172.70 metric 1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end diff --git a/src/tests/osm_end2end/clab/target_config/r2-startup.cfg b/src/tests/osm_end2end/clab/target_config/r2-startup.cfg new file mode 100644 index 000000000..3a545402c --- /dev/null +++ b/src/tests/osm_end2end/clab/target_config/r2-startup.cfg @@ -0,0 +1,63 @@ +! device: r2 (cEOSLab, EOS-4.32.2F-38195967.4322F (engineering build)) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$OmfaAwJRg/r44r5U$9Fca1O1G6Bgsd4NKwSyvdRJcHHk71jHAR3apDWAgSTN/t/j1iroEhz5J36HjWjOF/jEVC/R8Wa60VmbX6.cr70 +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname r2 +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +vlan 125 + name tfs-vlan-125 +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet1 + no switchport +! +interface Ethernet1.125 + encapsulation dot1q vlan 125 + ip address 10.254.172.70/30 +! +interface Ethernet3 + no switchport +! +interface Ethernet3.125 + encapsulation dot1q vlan 125 + ip address 10.254.187.117/30 +! +interface Management0 + ip address 172.20.20.102/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +ip route 172.16.1.0/24 10.254.172.69 metric 1 +ip route 172.16.3.0/24 10.254.187.118 metric 1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end diff --git a/src/tests/osm_end2end/clab/target_config/r3-startup.cfg b/src/tests/osm_end2end/clab/target_config/r3-startup.cfg new file mode 100644 index 000000000..d1842bb56 --- /dev/null +++ b/src/tests/osm_end2end/clab/target_config/r3-startup.cfg @@ -0,0 +1,62 @@ +! device: r3 (cEOSLab, EOS-4.32.2F-38195967.4322F (engineering build)) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$OmfaAwJRg/r44r5U$9Fca1O1G6Bgsd4NKwSyvdRJcHHk71jHAR3apDWAgSTN/t/j1iroEhz5J36HjWjOF/jEVC/R8Wa60VmbX6.cr70 +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname r3 +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +vlan 125 + name tfs-vlan-125 +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet2 + no switchport +! +interface Ethernet2.125 + encapsulation dot1q vlan 125 + ip address 10.254.187.118/30 +! +interface Ethernet10 + no switchport +! +interface Ethernet10.125 + encapsulation dot1q vlan 125 + ip address 172.16.3.1/24 +! +interface Management0 + ip address 172.20.20.103/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +ip route 172.16.1.0/24 10.254.187.117 metric 1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end -- GitLab From d5b77cd4355c59b7ede0ce19794c34039f838fb4 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 14:32:47 +0000 Subject: [PATCH 12/72] End-to-end test - OSM End-to-End: - Fixed ping tests in CI pipeline --- src/tests/osm_end2end/.gitlab-ci.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/tests/osm_end2end/.gitlab-ci.yml b/src/tests/osm_end2end/.gitlab-ci.yml index 43b6f2fcb..6e77643b6 100644 --- a/src/tests/osm_end2end/.gitlab-ci.yml +++ b/src/tests/osm_end2end/.gitlab-ci.yml @@ -211,8 +211,9 @@ end2end_test osm_end2end: # Run end-to-end test: test no connectivity with ping - ping_check "dc1" "172.16.1.10" "3 packets transmitted, 3 received, 0% packet loss" - - ping_check "dc1" "172.16.1.20" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" - - ping_check "dc1" "172.16.1.30" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + - ping_check "dc1" "172.16.1.1" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + - ping_check "dc1" "172.16.3.1" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + - ping_check "dc1" "172.16.3.10" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" # Run end-to-end test: configure OSM service - > @@ -231,8 +232,9 @@ end2end_test osm_end2end: # Run end-to-end test: test connectivity with ping - ping_check "dc1" "172.16.1.10" "3 packets transmitted, 3 received, 0% packet loss" - - ping_check "dc1" "172.16.1.20" "3 packets transmitted, 3 received, 0% packet loss" - - ping_check "dc1" "172.16.1.30" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + - ping_check "dc1" "172.16.1.1" "3 packets transmitted, 3 received, 0% packet loss" + - ping_check "dc1" "172.16.3.1" "3 packets transmitted, 3 received, 0% packet loss" + - ping_check "dc1" "172.16.3.10" "3 packets transmitted, 3 received, 0% packet loss" # Run end-to-end test: deconfigure OSM service - > @@ -251,8 +253,9 @@ end2end_test osm_end2end: # Run end-to-end test: test no connectivity with ping - ping_check "dc1" "172.16.1.10" "3 packets transmitted, 3 received, 0% packet loss" - - ping_check "dc1" "172.16.1.20" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" - - ping_check "dc1" "172.16.1.30" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + - ping_check "dc1" "172.16.1.1" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + - ping_check "dc1" "172.16.3.1" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" + - ping_check "dc1" "172.16.3.10" "3 packets transmitted, 0 received,( [\+]{0,1}[0-9]+ error[s]{0,1},)? 100% packet loss" # Run end-to-end test: cleanup scenario - > -- GitLab From 9ef72465c5a08543c54a472e8f19833201269a7d Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 16:28:44 +0000 Subject: [PATCH 13/72] NBI component - IETF L2VPN connector: - Fixed log levels --- src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py b/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py index e11beb62c..7588e2ca9 100644 --- a/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py +++ b/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py @@ -180,7 +180,7 @@ class L2VPN_SiteNetworkAccesses(Resource): }} MSG = '[_prepare_request_payload] request_data={:s}' - LOGGER.warning(MSG.format(str(request_data))) + LOGGER.debug(MSG.format(str(request_data))) return request_data errors.append('Unexpected request: {:s}'.format(str(request_data))) -- GitLab From 69e49544b5867bb235ff03e44c5435ed9afd4d4f Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 16:29:28 +0000 Subject: [PATCH 14/72] CI/CD pipeline: - Re-activated all tests --- .gitlab-ci.yml | 76 ++++++++++++++++++++-------------------- src/tests/.gitlab-ci.yml | 36 +++++++++---------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6627e11cb..53763f5e1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -28,44 +28,44 @@ workflow: # include the individual .gitlab-ci.yml of each micro-service and tests include: -# #- local: '/manifests/.gitlab-ci.yml' -# - local: '/src/monitoring/.gitlab-ci.yml' -# - local: '/src/nbi/.gitlab-ci.yml' -# - local: '/src/context/.gitlab-ci.yml' -# - local: '/src/device/.gitlab-ci.yml' -# - local: '/src/service/.gitlab-ci.yml' -# - local: '/src/qkd_app/.gitlab-ci.yml' -# - local: '/src/dbscanserving/.gitlab-ci.yml' -# - local: '/src/opticalattackmitigator/.gitlab-ci.yml' -# - local: '/src/opticalattackdetector/.gitlab-ci.yml' -# - local: '/src/opticalattackmanager/.gitlab-ci.yml' -# - local: '/src/opticalcontroller/.gitlab-ci.yml' -# - local: '/src/ztp/.gitlab-ci.yml' -# - local: '/src/policy/.gitlab-ci.yml' -# - local: '/src/automation/.gitlab-ci.yml' -# - local: '/src/forecaster/.gitlab-ci.yml' -# #- local: '/src/webui/.gitlab-ci.yml' -# #- local: '/src/l3_distributedattackdetector/.gitlab-ci.yml' -# #- local: '/src/l3_centralizedattackdetector/.gitlab-ci.yml' -# #- local: '/src/l3_attackmitigator/.gitlab-ci.yml' -# - local: '/src/slice/.gitlab-ci.yml' -# #- local: '/src/interdomain/.gitlab-ci.yml' -# - local: '/src/pathcomp/.gitlab-ci.yml' -# #- local: '/src/dlt/.gitlab-ci.yml' -# - local: '/src/load_generator/.gitlab-ci.yml' -# - local: '/src/bgpls_speaker/.gitlab-ci.yml' -# - local: '/src/kpi_manager/.gitlab-ci.yml' -# - local: '/src/kpi_value_api/.gitlab-ci.yml' -# #- local: '/src/kpi_value_writer/.gitlab-ci.yml' -# #- local: '/src/telemetry/.gitlab-ci.yml' -# - local: '/src/analytics/.gitlab-ci.yml' -# - local: '/src/qos_profile/.gitlab-ci.yml' -# - local: '/src/vnt_manager/.gitlab-ci.yml' -# - local: '/src/e2e_orchestrator/.gitlab-ci.yml' -# - local: '/src/ztp_server/.gitlab-ci.yml' -# - local: '/src/osm_client/.gitlab-ci.yml' -# - local: '/src/simap_connector/.gitlab-ci.yml' -# - local: '/src/pluggables/.gitlab-ci.yml' + #- local: '/manifests/.gitlab-ci.yml' + - local: '/src/monitoring/.gitlab-ci.yml' + - local: '/src/nbi/.gitlab-ci.yml' + - local: '/src/context/.gitlab-ci.yml' + - local: '/src/device/.gitlab-ci.yml' + - local: '/src/service/.gitlab-ci.yml' + - local: '/src/qkd_app/.gitlab-ci.yml' + - local: '/src/dbscanserving/.gitlab-ci.yml' + - local: '/src/opticalattackmitigator/.gitlab-ci.yml' + - local: '/src/opticalattackdetector/.gitlab-ci.yml' + - local: '/src/opticalattackmanager/.gitlab-ci.yml' + - local: '/src/opticalcontroller/.gitlab-ci.yml' + - local: '/src/ztp/.gitlab-ci.yml' + - local: '/src/policy/.gitlab-ci.yml' + - local: '/src/automation/.gitlab-ci.yml' + - local: '/src/forecaster/.gitlab-ci.yml' + #- local: '/src/webui/.gitlab-ci.yml' + #- local: '/src/l3_distributedattackdetector/.gitlab-ci.yml' + #- local: '/src/l3_centralizedattackdetector/.gitlab-ci.yml' + #- local: '/src/l3_attackmitigator/.gitlab-ci.yml' + - local: '/src/slice/.gitlab-ci.yml' + #- local: '/src/interdomain/.gitlab-ci.yml' + - local: '/src/pathcomp/.gitlab-ci.yml' + #- local: '/src/dlt/.gitlab-ci.yml' + - local: '/src/load_generator/.gitlab-ci.yml' + - local: '/src/bgpls_speaker/.gitlab-ci.yml' + - local: '/src/kpi_manager/.gitlab-ci.yml' + - local: '/src/kpi_value_api/.gitlab-ci.yml' + #- local: '/src/kpi_value_writer/.gitlab-ci.yml' + #- local: '/src/telemetry/.gitlab-ci.yml' + - local: '/src/analytics/.gitlab-ci.yml' + - local: '/src/qos_profile/.gitlab-ci.yml' + - local: '/src/vnt_manager/.gitlab-ci.yml' + - local: '/src/e2e_orchestrator/.gitlab-ci.yml' + - local: '/src/ztp_server/.gitlab-ci.yml' + - local: '/src/osm_client/.gitlab-ci.yml' + - local: '/src/simap_connector/.gitlab-ci.yml' + - local: '/src/pluggables/.gitlab-ci.yml' # This should be last one: end-to-end integration tests - local: '/src/tests/.gitlab-ci.yml' diff --git a/src/tests/.gitlab-ci.yml b/src/tests/.gitlab-ci.yml index 1fafbf3c2..4e2f85acc 100644 --- a/src/tests/.gitlab-ci.yml +++ b/src/tests/.gitlab-ci.yml @@ -15,22 +15,22 @@ # include the individual .gitlab-ci.yml of each end-to-end integration test include: - local: '/src/tests/osm_end2end/.gitlab-ci.yml' -# - local: '/src/tests/ofc22/.gitlab-ci.yml' -# #- local: '/src/tests/oeccpsc22/.gitlab-ci.yml' -# - local: '/src/tests/ecoc22/.gitlab-ci.yml' -# #- local: '/src/tests/nfvsdn22/.gitlab-ci.yml' -# #- local: '/src/tests/ofc23/.gitlab-ci.yml' -# - local: '/src/tests/ofc24/.gitlab-ci.yml' -# - local: '/src/tests/eucnc24/.gitlab-ci.yml' -# #- local: '/src/tests/ofc25-camara-agg-net-controller/.gitlab-ci.yml' -# #- local: '/src/tests/ofc25-camara-e2e-controller/.gitlab-ci.yml' -# #- local: '/src/tests/ofc25/.gitlab-ci.yml' -# - local: '/src/tests/ryu-openflow/.gitlab-ci.yml' -# - local: '/src/tests/qkd_end2end/.gitlab-ci.yml' -# - local: '/src/tests/acl_end2end/.gitlab-ci.yml' -# - local: '/src/tests/l2_vpn_gnmi_oc/.gitlab-ci.yml' + - local: '/src/tests/ofc22/.gitlab-ci.yml' + #- local: '/src/tests/oeccpsc22/.gitlab-ci.yml' + - local: '/src/tests/ecoc22/.gitlab-ci.yml' + #- local: '/src/tests/nfvsdn22/.gitlab-ci.yml' + #- local: '/src/tests/ofc23/.gitlab-ci.yml' + - local: '/src/tests/ofc24/.gitlab-ci.yml' + - local: '/src/tests/eucnc24/.gitlab-ci.yml' + #- local: '/src/tests/ofc25-camara-agg-net-controller/.gitlab-ci.yml' + #- local: '/src/tests/ofc25-camara-e2e-controller/.gitlab-ci.yml' + #- local: '/src/tests/ofc25/.gitlab-ci.yml' + - local: '/src/tests/ryu-openflow/.gitlab-ci.yml' + - local: '/src/tests/qkd_end2end/.gitlab-ci.yml' + - local: '/src/tests/acl_end2end/.gitlab-ci.yml' + - local: '/src/tests/l2_vpn_gnmi_oc/.gitlab-ci.yml' -# - local: '/src/tests/tools/mock_tfs_nbi_dependencies/.gitlab-ci.yml' -# - local: '/src/tests/tools/mock_qkd_node/.gitlab-ci.yml' -# - local: '/src/tests/tools/mock_osm_nbi/.gitlab-ci.yml' -# - local: '/src/tests/tools/simap_server/.gitlab-ci.yml' + - local: '/src/tests/tools/mock_tfs_nbi_dependencies/.gitlab-ci.yml' + - local: '/src/tests/tools/mock_qkd_node/.gitlab-ci.yml' + - local: '/src/tests/tools/mock_osm_nbi/.gitlab-ci.yml' + - local: '/src/tests/tools/simap_server/.gitlab-ci.yml' -- GitLab From 5748b12f09a6ba252cb80b7550d1a9772c025a02 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 18:23:27 +0000 Subject: [PATCH 15/72] NBI component - IETF L2VPN connector: - Added bearers for SNS4SNS'26 --- src/nbi/service/ietf_l2vpn/Constants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/nbi/service/ietf_l2vpn/Constants.py b/src/nbi/service/ietf_l2vpn/Constants.py index 6fd5db9d8..82a5e4608 100644 --- a/src/nbi/service/ietf_l2vpn/Constants.py +++ b/src/nbi/service/ietf_l2vpn/Constants.py @@ -31,6 +31,10 @@ BEARER_MAPPINGS = { 'OSM-E2E:r1:Ethernet10': ('r1', 'Ethernet10', None, None, 0, '172.16.1.1', 24, None, None), 'OSM-E2E:r3:Ethernet10': ('r3', 'Ethernet10', None, None, 0, '172.16.3.1', 24, None, None), + # SNS4SNS'26 + 'SNS4SNS26:r1:Ethernet10': ('r1', 'Ethernet10', None, None, 0, '192.168.251.5', 24, None, None), + 'SNS4SNS26:r2:Ethernet10': ('r2', 'Ethernet10', None, None, 0, '192.168.252.5', 24, None, None), + # OFC'22 'OFC22:R1-EMU:13/1/2': ('R1-EMU', '13/1/2', '10.10.10.1', '65000:100', 400, '3.3.2.1', 24, None, None), 'OFC22:R2-EMU:13/1/2': ('R2-EMU', '13/1/2', '12.12.12.1', '65000:120', 450, '3.4.2.1', 24, None, None), -- GitLab From ff183f5c231f540b0b55e9739981a260b2305bd7 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 18:24:50 +0000 Subject: [PATCH 16/72] Test - Tools - Mock OSM: - Made tool executable and configurable - Added README.md file - Added example config files --- src/tests/tools/mock_osm/README.md | 41 +++ src/tests/tools/mock_osm/__main__.py | 252 ++++++++++++++++++ .../tools/mock_osm/example_connection.json | 6 + src/tests/tools/mock_osm/example_mapping.json | 82 ++++++ 4 files changed, 381 insertions(+) create mode 100644 src/tests/tools/mock_osm/README.md create mode 100644 src/tests/tools/mock_osm/__main__.py create mode 100644 src/tests/tools/mock_osm/example_connection.json create mode 100644 src/tests/tools/mock_osm/example_mapping.json diff --git a/src/tests/tools/mock_osm/README.md b/src/tests/tools/mock_osm/README.md new file mode 100644 index 000000000..a197303cb --- /dev/null +++ b/src/tests/tools/mock_osm/README.md @@ -0,0 +1,41 @@ +# Mock OSM (tests.tools.mock_osm) + +This package provides a small interactive shell for testing WIM connectivity +through the MockOSM connector. + +## Run + +```bash +python -m tests.tools.mock_osm example_connection.json example_mapping.json +``` + +## Commands + +- `create [vlan ]` + - `ELINE` requires exactly 2 endpoints + - `ELAN` requires at least 2 endpoints +- `status` +- `delete` +- `exit` + +Endpoints are provided as a list of strings (service endpoint IDs), for example: + +```text +(mock-osm) create ELINE ep-R1-1/2 ep-R4-1/3 +``` + +Optional VLAN tagging for all endpoints: + +```text +(mock-osm) create ELINE ep-R1-1/2 ep-R4-1/3 vlan 1234 +``` + +## Example configs + +See: +- `src/tests/tools/mock_osm/example_connection.json` +- `src/tests/tools/mock_osm/example_mapping.json` + +The mapping file is a JSON list where each entry includes the +`service_endpoint_id`, `device-id`, and `service_mapping_info` with `bearer` +and `site-id`. diff --git a/src/tests/tools/mock_osm/__main__.py b/src/tests/tools/mock_osm/__main__.py new file mode 100644 index 000000000..2f94a9a63 --- /dev/null +++ b/src/tests/tools/mock_osm/__main__.py @@ -0,0 +1,252 @@ +# 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 argparse +import cmd +import json +import logging +import shlex +import sys +from typing import Any, Dict, List, Tuple + +from .MockOSM import MockOSM + +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +SUPPORTED_SERVICE_TYPES = {"ELINE", "ELAN"} + + +def _load_json_file(path: str) -> Any: + with open(path, "r", encoding="utf-8") as handle: + return json.load(handle) + + +def _first_present(data: Dict[str, Any], keys: Tuple[str, ...]) -> Any: + for key in keys: + if key in data: + return data[key] + return None + + +def _parse_connection_config(config: Dict[str, Any]) -> Tuple[str, str, str]: + if not isinstance(config, dict): + raise ValueError("Connection config must be a JSON object.") + + wim_ip = _first_present(config, ("wim_ip", "ip", "host", "address")) + wim_port = _first_present(config, ("wim_port", "port")) + wim_user = _first_present(config, ("wim_user", "user", "username")) + wim_pass = _first_present(config, ("wim_pass", "wim_password", "pass", "password")) + + missing = [] + if not wim_ip: + missing.append("wim_ip") + if wim_port is None: + missing.append("wim_port") + if not wim_user: + missing.append("wim_user") + if wim_pass is None: + missing.append("wim_pass") + + if missing: + raise ValueError( + "Connection config missing required fields: {}".format(", ".join(missing)) + ) + + try: + wim_port = int(wim_port) + except (TypeError, ValueError) as exc: + raise ValueError("wim_port must be an integer") from exc + + wim_url = "http://{:s}:{:d}".format(str(wim_ip), wim_port) + return wim_url, str(wim_user), str(wim_pass) + + +def _parse_mapping_config(mapping: Any) -> Dict[str, Dict[str, Any]]: + if not isinstance(mapping, list): + raise ValueError("Mapping config must be a JSON list.") + + mapping_by_id = {} + for index, entry in enumerate(mapping): + if not isinstance(entry, dict): + raise ValueError("Mapping entry {:d} must be a JSON object".format(index)) + service_endpoint_id = entry.get("service_endpoint_id") + if not service_endpoint_id or not isinstance(service_endpoint_id, str): + raise ValueError( + "Mapping entry {:d} missing service_endpoint_id".format(index) + ) + if service_endpoint_id in mapping_by_id: + LOGGER.warning( + "Duplicate service_endpoint_id in mapping: %s", service_endpoint_id + ) + mapping_by_id[service_endpoint_id] = entry + + return mapping_by_id + + +class MockOSMShell(cmd.Cmd): + intro = "Welcome to the MockOSM shell.\nType help or ? to list commands.\n" + prompt = "(mock-osm) " + + def __init__(self, mock_osm: MockOSM, mapping_by_id: Dict[str, Dict[str, Any]]): + super().__init__() + self.mock_osm = mock_osm + self.mapping_by_id = mapping_by_id + + def do_create(self, arg: str) -> None: + "Create a connectivity service: create [vlan ]" + try: + service_type, endpoints = self._parse_create_args(arg) + service_uuid = self.mock_osm.create_connectivity_service( + service_type, endpoints + ) + print("Service {:s} created".format(service_uuid)) + except Exception as exc: + print("Error: {:s}".format(str(exc))) + + def do_status(self, arg: str) -> None: + "Retrieve status of services" + service_uuids = list(self.mock_osm.conn_info.keys()) + for service_uuid in service_uuids: + status = self.mock_osm.get_connectivity_service_status(service_uuid) + print("Status of Service {:s} is {:s}".format(service_uuid, str(status))) + + def do_delete(self, arg: str) -> None: + "Delete all services" + service_uuids = list(self.mock_osm.conn_info.keys()) + for service_uuid in service_uuids: + self.mock_osm.delete_connectivity_service(service_uuid) + print("Service {:s} deleted".format(service_uuid)) + + def do_exit(self, arg: str) -> bool: + "Exit MockOSM" + print("Bye!") + return True + + def _parse_create_args(self, arg: str) -> Tuple[str, List[Dict[str, Any]]]: + tokens = shlex.split(arg) + if len(tokens) < 2: + raise ValueError( + "Usage: create [vlan ]" + ) + + service_type = tokens[0] + endpoints_tokens = tokens[1:] + vlan_id = None + if "vlan" in endpoints_tokens: + vlan_index = endpoints_tokens.index("vlan") + if vlan_index == len(endpoints_tokens) - 1: + raise ValueError("vlan requires ") + if vlan_index + 2 != len(endpoints_tokens): + raise ValueError("vlan must be the last argument") + vlan_token = endpoints_tokens[vlan_index + 1] + try: + vlan_id = int(vlan_token) + except (TypeError, ValueError) as exc: + raise ValueError("vlan-id must be an integer") from exc + endpoints_tokens = endpoints_tokens[:vlan_index] + + endpoints = self._load_endpoints(endpoints_tokens) + self._validate_service_request(service_type, endpoints) + connection_points = [ + { + "service_endpoint_id": endpoint, + "service_endpoint_encapsulation_type": ( + "dot1q" if vlan_id is not None else "none" + ), + **( + {"service_endpoint_encapsulation_info": {"vlan": vlan_id}} + if vlan_id is not None + else {} + ), + } + for endpoint in endpoints + ] + return service_type, connection_points + + def _load_endpoints(self, tokens: List[str]) -> List[str]: + endpoints = [] + for token in tokens: + if "," in token: + endpoints.extend([item for item in token.split(",") if item]) + else: + endpoints.append(token) + return endpoints + + def _validate_service_request( + self, service_type: str, endpoints: List[str] + ) -> None: + if not isinstance(service_type, str) or not service_type: + raise ValueError("Service type must be a non-empty string.") + + if service_type not in SUPPORTED_SERVICE_TYPES: + raise ValueError( + "Unsupported service type. Supported: {}".format( + ", ".join(sorted(SUPPORTED_SERVICE_TYPES)) + ) + ) + + if not endpoints: + raise ValueError("Endpoints list must not be empty.") + + if service_type == "ELINE" and len(endpoints) != 2: + raise ValueError("ELINE requires exactly 2 endpoints.") + + if service_type == "ELAN" and len(endpoints) < 2: + raise ValueError("ELAN requires at least 2 endpoints.") + + for index, endpoint in enumerate(endpoints): + if not isinstance(endpoint, str) or not endpoint: + raise ValueError( + "Endpoint {:d} must be a non-empty string".format(index) + ) + if endpoint not in self.mapping_by_id: + raise ValueError( + "Endpoint {:s} not found in WIM port mapping".format(endpoint) + ) + + +def _parse_args(argv: List[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="MockOSM shell") + parser.add_argument( + "connection_config", + help="JSON with WIM IP, port, user, and password", + ) + parser.add_argument( + "mapping_config", + help="JSON with pre-generated WIM port mapping", + ) + return parser.parse_args(argv) + + +def main(argv: List[str]) -> int: + args = _parse_args(argv) + + try: + connection_config = _load_json_file(args.connection_config) + mapping_config = _load_json_file(args.mapping_config) + wim_url, wim_user, wim_pass = _parse_connection_config(connection_config) + mapping_by_id = _parse_mapping_config(mapping_config) + except Exception as exc: + print("Configuration error: {:s}".format(str(exc)), file=sys.stderr) + return 2 + + mock_osm = MockOSM(wim_url, mapping_config, wim_user, wim_pass) + MockOSMShell(mock_osm, mapping_by_id).cmdloop() + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/src/tests/tools/mock_osm/example_connection.json b/src/tests/tools/mock_osm/example_connection.json new file mode 100644 index 000000000..ab8944453 --- /dev/null +++ b/src/tests/tools/mock_osm/example_connection.json @@ -0,0 +1,6 @@ +{ + "wim_ip": "10.0.2.10", + "wim_port": 80, + "wim_user": "admin", + "wim_pass": "admin" +} diff --git a/src/tests/tools/mock_osm/example_mapping.json b/src/tests/tools/mock_osm/example_mapping.json new file mode 100644 index 000000000..10897cac1 --- /dev/null +++ b/src/tests/tools/mock_osm/example_mapping.json @@ -0,0 +1,82 @@ +[ + { + "device-id": "R1", + "service_endpoint_id": "ep-R1-1/2", + "service_mapping_info": { + "bearer": { + "bearer-reference": "R1:1/2" + }, + "site-id": "1" + } + }, + { + "device-id": "R1", + "service_endpoint_id": "ep-R1-1/3", + "service_mapping_info": { + "bearer": { + "bearer-reference": "R1:1/3" + }, + "site-id": "1" + } + }, + { + "device-id": "R2", + "service_endpoint_id": "ep-R2-1/2", + "service_mapping_info": { + "bearer": { + "bearer-reference": "R2:1/2" + }, + "site-id": "2" + } + }, + { + "device-id": "R2", + "service_endpoint_id": "ep-R2-1/3", + "service_mapping_info": { + "bearer": { + "bearer-reference": "R2:1/3" + }, + "site-id": "2" + } + }, + { + "device-id": "R3", + "service_endpoint_id": "ep-R3-1/2", + "service_mapping_info": { + "bearer": { + "bearer-reference": "R3:1/2" + }, + "site-id": "3" + } + }, + { + "device-id": "R3", + "service_endpoint_id": "ep-R3-1/3", + "service_mapping_info": { + "bearer": { + "bearer-reference": "R3:1/3" + }, + "site-id": "3" + } + }, + { + "device-id": "R4", + "service_endpoint_id": "ep-R4-1/2", + "service_mapping_info": { + "bearer": { + "bearer-reference": "R4:1/2" + }, + "site-id": "4" + } + }, + { + "device-id": "R4", + "service_endpoint_id": "ep-R4-1/3", + "service_mapping_info": { + "bearer": { + "bearer-reference": "R4:1/3" + }, + "site-id": "4" + } + } +] -- GitLab From 7a94e4c569a331e9c6af35114e91bee1fa0c8640 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 18:34:47 +0000 Subject: [PATCH 17/72] End-to-end test - SNS4SNS 2026: - Added basic files --- src/tests/sns4sns26/clab/r1-startup.cfg | 54 ++++++++++ src/tests/sns4sns26/clab/r2-startup.cfg | 54 ++++++++++ src/tests/sns4sns26/clab/sns4sns26.clab.yml | 51 +++++++++ .../sns4sns26/data/mock_osm_connection.json | 6 ++ .../sns4sns26/data/mock_osm_mapping.json | 4 + src/tests/sns4sns26/data/tfs-topology.json | 100 ++++++++++++++++++ src/tests/sns4sns26/scripts/clab-cli-dc1.sh | 16 +++ src/tests/sns4sns26/scripts/clab-cli-dc2.sh | 16 +++ src/tests/sns4sns26/scripts/clab-cli-r1.sh | 16 +++ src/tests/sns4sns26/scripts/clab-cli-r2.sh | 16 +++ src/tests/sns4sns26/scripts/clab-deploy.sh | 22 ++++ src/tests/sns4sns26/scripts/clab-destroy.sh | 18 ++++ src/tests/sns4sns26/scripts/clab-inspect.sh | 17 +++ 13 files changed, 390 insertions(+) create mode 100644 src/tests/sns4sns26/clab/r1-startup.cfg create mode 100644 src/tests/sns4sns26/clab/r2-startup.cfg create mode 100644 src/tests/sns4sns26/clab/sns4sns26.clab.yml create mode 100644 src/tests/sns4sns26/data/mock_osm_connection.json create mode 100644 src/tests/sns4sns26/data/mock_osm_mapping.json create mode 100644 src/tests/sns4sns26/data/tfs-topology.json create mode 100644 src/tests/sns4sns26/scripts/clab-cli-dc1.sh create mode 100644 src/tests/sns4sns26/scripts/clab-cli-dc2.sh create mode 100644 src/tests/sns4sns26/scripts/clab-cli-r1.sh create mode 100644 src/tests/sns4sns26/scripts/clab-cli-r2.sh create mode 100644 src/tests/sns4sns26/scripts/clab-deploy.sh create mode 100644 src/tests/sns4sns26/scripts/clab-destroy.sh create mode 100644 src/tests/sns4sns26/scripts/clab-inspect.sh diff --git a/src/tests/sns4sns26/clab/r1-startup.cfg b/src/tests/sns4sns26/clab/r1-startup.cfg new file mode 100644 index 000000000..c7ee1bc0a --- /dev/null +++ b/src/tests/sns4sns26/clab/r1-startup.cfg @@ -0,0 +1,54 @@ +! Command: show running-config +! device: r1 (cEOSLab, EOS-4.34.4M-45127473.4344M (engineering build)) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$v.q.IH8.dY2VUI8P$S6GkfbDxjEm8kHeU/5oEWrlcTFS2vBr4mPK4s8d0w2gi6wWR1jcajM8fqg93405IfDm6yLqSh4IC1AKbwIdKr/ +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname r1 +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet2 +! +interface Ethernet9 +! +interface Ethernet10 + no switchport + ip address 192.168.251.5/24 + mtu 1400 +! +interface Management0 + ip address 172.20.20.101/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end diff --git a/src/tests/sns4sns26/clab/r2-startup.cfg b/src/tests/sns4sns26/clab/r2-startup.cfg new file mode 100644 index 000000000..d0fb9c697 --- /dev/null +++ b/src/tests/sns4sns26/clab/r2-startup.cfg @@ -0,0 +1,54 @@ +! Command: show running-config +! device: r2 (cEOSLab, EOS-4.34.4M-45127473.4344M (engineering build)) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$Fu.Mcm.e.zgLUCv9$bEp3TJMx3.6GqNcx5MpLEpWyNgVrvjW6zDXhbu7.iIeqIuH7z0rN7zGnSWU18lIGI7B9BK3ShmPIYsF7hPLup/ +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname r2 +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet1 +! +interface Ethernet9 +! +interface Ethernet10 + no switchport + ip address 192.168.252.5/24 + mtu 1400 +! +interface Management0 + ip address 172.20.20.102/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end diff --git a/src/tests/sns4sns26/clab/sns4sns26.clab.yml b/src/tests/sns4sns26/clab/sns4sns26.clab.yml new file mode 100644 index 000000000..c07146855 --- /dev/null +++ b/src/tests/sns4sns26/clab/sns4sns26.clab.yml @@ -0,0 +1,51 @@ +# 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. + +# Ref: https://containerlab.dev/manual/network/#macvlan-links +# Ref: https://containerlab.dev/manual/network/#host-links + + +# ETSI Joint SDG Lab + +name: sns4sns26 + +mgmt: + network: mgmt-net + ipv4-subnet: 172.20.20.0/24 + mtu: 1400 + +topology: + kinds: + arista_ceos: + kind: arista_ceos + image: ceos:4.34.4M + linux: + kind: linux + image: ghcr.io/hellt/network-multitool:latest + + nodes: + r1: + kind: arista_ceos + mgmt-ipv4: 172.20.20.101 + startup-config: r1-startup.cfg + + r2: + kind: arista_ceos + mgmt-ipv4: 172.20.20.102 + startup-config: r2-startup.cfg + + links: + - endpoints: ["r1:eth2", "r2:eth1"] + - endpoints: ["r1:eth10", "macvlan:enp0s4"] # connect to site A virtual network + - endpoints: ["r2:eth10", "macvlan:enp0s5"] # connect to site B virtual network diff --git a/src/tests/sns4sns26/data/mock_osm_connection.json b/src/tests/sns4sns26/data/mock_osm_connection.json new file mode 100644 index 000000000..ab8944453 --- /dev/null +++ b/src/tests/sns4sns26/data/mock_osm_connection.json @@ -0,0 +1,6 @@ +{ + "wim_ip": "10.0.2.10", + "wim_port": 80, + "wim_user": "admin", + "wim_pass": "admin" +} diff --git a/src/tests/sns4sns26/data/mock_osm_mapping.json b/src/tests/sns4sns26/data/mock_osm_mapping.json new file mode 100644 index 000000000..8ab6009d1 --- /dev/null +++ b/src/tests/sns4sns26/data/mock_osm_mapping.json @@ -0,0 +1,4 @@ +[ + {"service_endpoint_id": "R1-Eth10", "device-id": "r1", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:r1:Ethernet10"}, "site-id": "A"}}, + {"service_endpoint_id": "R2-Eth10", "device-id": "r2", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:r2:Ethernet10"}, "site-id": "B"}} +] diff --git a/src/tests/sns4sns26/data/tfs-topology.json b/src/tests/sns4sns26/data/tfs-topology.json new file mode 100644 index 000000000..74065d146 --- /dev/null +++ b/src/tests/sns4sns26/data/tfs-topology.json @@ -0,0 +1,100 @@ +{ + "contexts": [ + {"context_id": {"context_uuid": {"uuid": "admin"}}} + ], + "topologies": [ + {"topology_id": {"context_id": {"context_uuid": {"uuid": "admin"}}, "topology_uuid": {"uuid": "admin"}}} + ], + "devices": [ + { + "device_id": {"device_uuid": {"uuid": "cluster-a"}}, "name": "Cluster A", "device_type": "emu-datacenter", + "device_drivers": ["DEVICEDRIVER_UNDEFINED"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "type": "copper"}, {"uuid": "int", "type": "copper"} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "cluster-b"}}, "name": "Cluster B", "device_type": "emu-datacenter", + "device_drivers": ["DEVICEDRIVER_UNDEFINED"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "0"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": {"endpoints": [ + {"uuid": "eth1", "type": "copper"}, {"uuid": "int", "type": "copper"} + ]}}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "r1"}}, "name": "Router 1", "device_type": "packet-router", + "device_drivers": ["DEVICEDRIVER_GNMI_OPENCONFIG"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "172.20.20.101"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "6030"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": { + "username": "admin", "password": "admin", "use_tls": false + }}} + ]} + }, + { + "device_id": {"device_uuid": {"uuid": "r2"}}, "name": "Router 2", "device_type": "packet-router", + "device_drivers": ["DEVICEDRIVER_GNMI_OPENCONFIG"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "172.20.20.102"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "6030"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": { + "username": "admin", "password": "admin", "use_tls": false + }}} + ]} + } + ], + "links": [ + { + "link_id": {"link_uuid": {"uuid": "r1/Ethernet2==r2/Ethernet1"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet2"}}, + {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "r2/Ethernet1==r1/Ethernet2"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet1"}}, + {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet2"}} + ] + }, + + { + "link_id": {"link_uuid": {"uuid": "r1/Ethernet10==cluster-a/eth1"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, + {"device_id": {"device_uuid": {"uuid": "cluster-a"}}, "endpoint_uuid": {"uuid": "eth1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "cluster-a/eth1==r1/Ethernet10"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "cluster-a"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} + ] + }, + + { + "link_id": {"link_uuid": {"uuid": "r2/Ethernet10==cluster-b/eth1"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, + {"device_id": {"device_uuid": {"uuid": "cluster-b"}}, "endpoint_uuid": {"uuid": "eth1"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "cluster-b/eth1==r2/Ethernet10"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "cluster-b"}}, "endpoint_uuid": {"uuid": "eth1"}}, + {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} + ] + } + ] +} diff --git a/src/tests/sns4sns26/scripts/clab-cli-dc1.sh b/src/tests/sns4sns26/scripts/clab-cli-dc1.sh new file mode 100644 index 000000000..4e22b3eec --- /dev/null +++ b/src/tests/sns4sns26/scripts/clab-cli-dc1.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 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. + +docker exec -it clab-sns4sns26-dc1 bash diff --git a/src/tests/sns4sns26/scripts/clab-cli-dc2.sh b/src/tests/sns4sns26/scripts/clab-cli-dc2.sh new file mode 100644 index 000000000..54b4394af --- /dev/null +++ b/src/tests/sns4sns26/scripts/clab-cli-dc2.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 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. + +docker exec -it clab-sns4sns26-dc2 bash diff --git a/src/tests/sns4sns26/scripts/clab-cli-r1.sh b/src/tests/sns4sns26/scripts/clab-cli-r1.sh new file mode 100644 index 000000000..df9c15c61 --- /dev/null +++ b/src/tests/sns4sns26/scripts/clab-cli-r1.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 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. + +docker exec -it clab-sns4sns26-r1 Cli diff --git a/src/tests/sns4sns26/scripts/clab-cli-r2.sh b/src/tests/sns4sns26/scripts/clab-cli-r2.sh new file mode 100644 index 000000000..fcb577295 --- /dev/null +++ b/src/tests/sns4sns26/scripts/clab-cli-r2.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# 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. + +docker exec -it clab-sns4sns26-r2 Cli diff --git a/src/tests/sns4sns26/scripts/clab-deploy.sh b/src/tests/sns4sns26/scripts/clab-deploy.sh new file mode 100644 index 000000000..8554532da --- /dev/null +++ b/src/tests/sns4sns26/scripts/clab-deploy.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# 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. + +sudo ip link set ens4 down +sudo ip link set ens5 down +sudo ip link set ens4 up +sudo ip link set ens5 up + +cd ~/tfs-ctrl/src/tests/sns4sns26 +sudo containerlab deploy --topo clab/sns4sns26.clab.yml diff --git a/src/tests/sns4sns26/scripts/clab-destroy.sh b/src/tests/sns4sns26/scripts/clab-destroy.sh new file mode 100644 index 000000000..af92ca092 --- /dev/null +++ b/src/tests/sns4sns26/scripts/clab-destroy.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# 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. + +cd ~/tfs-ctrl/src/tests/sns4sns26 +sudo containerlab destroy --topo clab/sns4sns26.clab.yml +sudo rm -rf clab/clab-sns4sns26/ clab/.sns4sns26.clab.yml.bak diff --git a/src/tests/sns4sns26/scripts/clab-inspect.sh b/src/tests/sns4sns26/scripts/clab-inspect.sh new file mode 100644 index 000000000..941f2606c --- /dev/null +++ b/src/tests/sns4sns26/scripts/clab-inspect.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# 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. + +cd ~/tfs-ctrl/src/tests/sns4sns26 +sudo containerlab inspect --topo clab/sns4sns26.clab.yml -- GitLab From b43decfca7b45b07ea0fe249b33247d92d76727a Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 18:42:30 +0000 Subject: [PATCH 18/72] NBI component - IETF L2VPN connector: - Fixed auto-generation of unknown bearers --- src/nbi/service/ietf_l2vpn/Handlers.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/nbi/service/ietf_l2vpn/Handlers.py b/src/nbi/service/ietf_l2vpn/Handlers.py index 1b8fc2b93..18b17c4b2 100644 --- a/src/nbi/service/ietf_l2vpn/Handlers.py +++ b/src/nbi/service/ietf_l2vpn/Handlers.py @@ -86,7 +86,17 @@ def process_site_network_access( bearer_mapping = BEARER_MAPPINGS.get(bearer_reference) if bearer_mapping is None: if ':' in bearer_reference: - bearer_mapping = str(bearer_reference).split(':', maxsplit=1) + bearer_mapping = str(bearer_reference).split(':') + if len(bearer_mapping) == 2: + # assume device:endpoint + pass + elif len(bearer_mapping) == 3: + # assume prefix:device:endpoint + bearer_mapping.pop(0) + else: + MSG = 'Bearer({:s}) not found; unable to auto-generated mapping' + raise Exception(MSG.format(str(bearer_reference))) + bearer_mapping.extend([None, None, None, None, None, None, None]) bearer_mapping = tuple(bearer_mapping) MSG = 'Bearer({:s}) not found; auto-generated mapping: {:s}' -- GitLab From 6807fc9e099087ec911dc92c393c609e74358170 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 19:05:38 +0000 Subject: [PATCH 19/72] NBI component - IETF L2VPN connector: - Fixed bearers for SNS4SNS'26 --- src/nbi/service/ietf_l2vpn/Constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nbi/service/ietf_l2vpn/Constants.py b/src/nbi/service/ietf_l2vpn/Constants.py index 82a5e4608..9ed1c64fe 100644 --- a/src/nbi/service/ietf_l2vpn/Constants.py +++ b/src/nbi/service/ietf_l2vpn/Constants.py @@ -32,8 +32,8 @@ BEARER_MAPPINGS = { 'OSM-E2E:r3:Ethernet10': ('r3', 'Ethernet10', None, None, 0, '172.16.3.1', 24, None, None), # SNS4SNS'26 - 'SNS4SNS26:r1:Ethernet10': ('r1', 'Ethernet10', None, None, 0, '192.168.251.5', 24, None, None), - 'SNS4SNS26:r2:Ethernet10': ('r2', 'Ethernet10', None, None, 0, '192.168.252.5', 24, None, None), + 'SNS4SNS26:SiteA': ('cluster-a', 'eth1', None, None, 0, '192.168.251.100', 24, None, None), + 'SNS4SNS26:SiteB': ('cluster-b', 'eth1', None, None, 0, '192.168.252.100', 24, None, None), # OFC'22 'OFC22:R1-EMU:13/1/2': ('R1-EMU', '13/1/2', '10.10.10.1', '65000:100', 400, '3.3.2.1', 24, None, None), -- GitLab From 54c8de37bde43cdff4e2896a7f12861c1309ba7a Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 19:06:08 +0000 Subject: [PATCH 20/72] End-to-end test - SNS4SNS 2026: - Fixed Mock OSM WIM Port Mapping --- src/tests/sns4sns26/data/mock_osm_mapping.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/sns4sns26/data/mock_osm_mapping.json b/src/tests/sns4sns26/data/mock_osm_mapping.json index 8ab6009d1..7edc0ed84 100644 --- a/src/tests/sns4sns26/data/mock_osm_mapping.json +++ b/src/tests/sns4sns26/data/mock_osm_mapping.json @@ -1,4 +1,4 @@ [ - {"service_endpoint_id": "R1-Eth10", "device-id": "r1", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:r1:Ethernet10"}, "site-id": "A"}}, - {"service_endpoint_id": "R2-Eth10", "device-id": "r2", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:r2:Ethernet10"}, "site-id": "B"}} + {"service_endpoint_id": "SiteA", "device-id": "cluster-a", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:SiteA"}, "site-id": "A"}}, + {"service_endpoint_id": "SiteB", "device-id": "cluster-b", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:SiteB"}, "site-id": "B"}} ] -- GitLab From 07a64a49995f43f48b3e684d3c347b4ccfd8baa3 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 19:19:16 +0000 Subject: [PATCH 21/72] End-to-end test - SNS4SNS 2026: - Updated topology descriptor --- src/tests/sns4sns26/data/tfs-topology.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/sns4sns26/data/tfs-topology.json b/src/tests/sns4sns26/data/tfs-topology.json index 74065d146..0dd6280e1 100644 --- a/src/tests/sns4sns26/data/tfs-topology.json +++ b/src/tests/sns4sns26/data/tfs-topology.json @@ -7,7 +7,7 @@ ], "devices": [ { - "device_id": {"device_uuid": {"uuid": "cluster-a"}}, "name": "Cluster A", "device_type": "emu-datacenter", + "device_id": {"device_uuid": {"uuid": "cluster-a"}}, "device_type": "emu-datacenter", "device_drivers": ["DEVICEDRIVER_UNDEFINED"], "device_config": {"config_rules": [ {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, @@ -18,7 +18,7 @@ ]} }, { - "device_id": {"device_uuid": {"uuid": "cluster-b"}}, "name": "Cluster B", "device_type": "emu-datacenter", + "device_id": {"device_uuid": {"uuid": "cluster-b"}}, "device_type": "emu-datacenter", "device_drivers": ["DEVICEDRIVER_UNDEFINED"], "device_config": {"config_rules": [ {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "127.0.0.1"}}, @@ -29,7 +29,7 @@ ]} }, { - "device_id": {"device_uuid": {"uuid": "r1"}}, "name": "Router 1", "device_type": "packet-router", + "device_id": {"device_uuid": {"uuid": "router-1"}}, "device_type": "packet-router", "device_drivers": ["DEVICEDRIVER_GNMI_OPENCONFIG"], "device_config": {"config_rules": [ {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "172.20.20.101"}}, @@ -40,7 +40,7 @@ ]} }, { - "device_id": {"device_uuid": {"uuid": "r2"}}, "name": "Router 2", "device_type": "packet-router", + "device_id": {"device_uuid": {"uuid": "router-2"}}, "device_type": "packet-router", "device_drivers": ["DEVICEDRIVER_GNMI_OPENCONFIG"], "device_config": {"config_rules": [ {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "172.20.20.102"}}, -- GitLab From 9393130b9d0fa7c6d940b82c4a543c8344072a09 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 19:28:39 +0000 Subject: [PATCH 22/72] End-to-end test - SNS4SNS 2026: - Updated topology descriptor --- src/tests/sns4sns26/data/tfs-topology.json | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/tests/sns4sns26/data/tfs-topology.json b/src/tests/sns4sns26/data/tfs-topology.json index 0dd6280e1..d05cdfb07 100644 --- a/src/tests/sns4sns26/data/tfs-topology.json +++ b/src/tests/sns4sns26/data/tfs-topology.json @@ -53,47 +53,47 @@ ], "links": [ { - "link_id": {"link_uuid": {"uuid": "r1/Ethernet2==r2/Ethernet1"}}, + "link_id": {"link_uuid": {"uuid": "router-1/Ethernet2==router-2/Ethernet1"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet2"}}, - {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet1"}} + {"device_id": {"device_uuid": {"uuid": "router-1"}}, "endpoint_uuid": {"uuid": "Ethernet2"}}, + {"device_id": {"device_uuid": {"uuid": "router-2"}}, "endpoint_uuid": {"uuid": "Ethernet1"}} ] }, { - "link_id": {"link_uuid": {"uuid": "r2/Ethernet1==r1/Ethernet2"}}, + "link_id": {"link_uuid": {"uuid": "router-2/Ethernet1==router-1/Ethernet2"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet1"}}, - {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet2"}} + {"device_id": {"device_uuid": {"uuid": "router-2"}}, "endpoint_uuid": {"uuid": "Ethernet1"}}, + {"device_id": {"device_uuid": {"uuid": "router-1"}}, "endpoint_uuid": {"uuid": "Ethernet2"}} ] }, { - "link_id": {"link_uuid": {"uuid": "r1/Ethernet10==cluster-a/eth1"}}, + "link_id": {"link_uuid": {"uuid": "router-1/Ethernet10==cluster-a/eth1"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, + {"device_id": {"device_uuid": {"uuid": "router-1"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, {"device_id": {"device_uuid": {"uuid": "cluster-a"}}, "endpoint_uuid": {"uuid": "eth1"}} ] }, { - "link_id": {"link_uuid": {"uuid": "cluster-a/eth1==r1/Ethernet10"}}, + "link_id": {"link_uuid": {"uuid": "cluster-a/eth1==router-1/Ethernet10"}}, "link_endpoint_ids": [ {"device_id": {"device_uuid": {"uuid": "cluster-a"}}, "endpoint_uuid": {"uuid": "eth1"}}, - {"device_id": {"device_uuid": {"uuid": "r1"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} + {"device_id": {"device_uuid": {"uuid": "router-1"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} ] }, { - "link_id": {"link_uuid": {"uuid": "r2/Ethernet10==cluster-b/eth1"}}, + "link_id": {"link_uuid": {"uuid": "router-2/Ethernet10==cluster-b/eth1"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, + {"device_id": {"device_uuid": {"uuid": "router-2"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, {"device_id": {"device_uuid": {"uuid": "cluster-b"}}, "endpoint_uuid": {"uuid": "eth1"}} ] }, { - "link_id": {"link_uuid": {"uuid": "cluster-b/eth1==r2/Ethernet10"}}, + "link_id": {"link_uuid": {"uuid": "cluster-b/eth1==router-2/Ethernet10"}}, "link_endpoint_ids": [ {"device_id": {"device_uuid": {"uuid": "cluster-b"}}, "endpoint_uuid": {"uuid": "eth1"}}, - {"device_id": {"device_uuid": {"uuid": "r2"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} + {"device_id": {"device_uuid": {"uuid": "router-2"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} ] } ] -- GitLab From 904df11403979f46bcfefc96134efbe6d517fe8b Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 23 Jan 2026 20:08:05 +0000 Subject: [PATCH 23/72] Service component - L3NM gNMI OpenConfig: - Fixed removal of interfaces and static routes --- .../ConfigRuleComposer.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index cfe4d1d62..a9e7b9223 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -101,6 +101,10 @@ def _network_instance_interface(ni_name : str, interface : str, sub_interface_in data = {'name': ni_name, 'id': sub_interface_name, 'interface': interface, 'subinterface': sub_interface_index} return path, data +def _interface_switched_vlan(interface : str) -> Tuple[str, Dict]: + path = '/interface[{:s}]/ethernet/switched-vlan'.format(interface) + return path, {} + class EndpointComposer: def __init__(self, endpoint_uuid : str) -> None: self.uuid = endpoint_uuid @@ -314,25 +318,32 @@ class DeviceComposer: json_config_rule(*_network_instance(network_instance_name, 'L3VRF')) parent_interfaces : Set[str] = set() + configured_parents : Set[str] = set() for endpoint in self.endpoints.values(): - if not delete and endpoint.objekt is not None: - if endpoint.objekt.name not in parent_interfaces: - config_rules.append(json_config_rule_set(*_interface( - endpoint.objekt.name, index=0, address_ip=None, address_prefix=None, enabled=True - ))) - parent_interfaces.add(endpoint.objekt.name) + if endpoint.objekt is None: + continue + parent_interfaces.add(endpoint.objekt.name) + if not delete and endpoint.objekt.name not in configured_parents: + config_rules.append(json_config_rule_set(*_interface( + endpoint.objekt.name, index=0, address_ip=None, address_prefix=None, enabled=True + ))) + configured_parents.add(endpoint.objekt.name) config_rules.extend(endpoint.get_config_rules( network_instance_name, self.service_vlan_id, access_vlan_tagged=self.access_vlan_tagged, delete=delete )) self.vlan_ids.update(endpoint.explicit_vlan_ids) + if delete: + for if_name in sorted(parent_interfaces): + config_rules.append(json_config_rule_delete(*_interface_switched_vlan(if_name))) + for vlan_id in sorted(self.vlan_ids): vlan_name = 'tfs-vlan-{:s}'.format(str(vlan_id)) config_rules.append(json_config_rule(*_network_instance_vlan( network_instance_name, vlan_id, vlan_name=vlan_name ))) - if len(self.static_routes) > 0: + if len(self.static_routes) > 0 and not delete: config_rules.append( json_config_rule(*_network_instance_protocol_static(network_instance_name)) ) -- GitLab From b8bc6158b386357e6512e1de1ce6ee2ed634c877 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 08:05:45 +0000 Subject: [PATCH 24/72] End-to-end test - SNS4SNS 2026: - Updated topology descriptor - Updated config files - Updated scripts --- src/tests/sns4sns26/clab/r2-startup.cfg | 7 +-- src/tests/sns4sns26/clab/r3-startup.cfg | 52 +++++++++++++++++++ src/tests/sns4sns26/clab/sns4sns26.clab.yml | 8 ++- .../sns4sns26/data/mock_osm_mapping.json | 4 +- src/tests/sns4sns26/data/tfs-topology.json | 34 ++++++++++-- src/tests/sns4sns26/scripts/clab-cli-dc2.sh | 16 ------ .../{clab-cli-dc1.sh => clab-cli-r3.sh} | 2 +- 7 files changed, 93 insertions(+), 30 deletions(-) create mode 100644 src/tests/sns4sns26/clab/r3-startup.cfg delete mode 100644 src/tests/sns4sns26/scripts/clab-cli-dc2.sh rename src/tests/sns4sns26/scripts/{clab-cli-dc1.sh => clab-cli-r3.sh} (94%) diff --git a/src/tests/sns4sns26/clab/r2-startup.cfg b/src/tests/sns4sns26/clab/r2-startup.cfg index d0fb9c697..22ad9852b 100644 --- a/src/tests/sns4sns26/clab/r2-startup.cfg +++ b/src/tests/sns4sns26/clab/r2-startup.cfg @@ -30,12 +30,7 @@ management api netconf ! interface Ethernet1 ! -interface Ethernet9 -! -interface Ethernet10 - no switchport - ip address 192.168.252.5/24 - mtu 1400 +interface Ethernet3 ! interface Management0 ip address 172.20.20.102/24 diff --git a/src/tests/sns4sns26/clab/r3-startup.cfg b/src/tests/sns4sns26/clab/r3-startup.cfg new file mode 100644 index 000000000..7faa090ad --- /dev/null +++ b/src/tests/sns4sns26/clab/r3-startup.cfg @@ -0,0 +1,52 @@ +! Command: show running-config +! device: r3 (cEOSLab, EOS-4.34.4M-45127473.4344M (engineering build)) +! +no aaa root +! +username admin privilege 15 role network-admin secret sha512 $6$Fu.Mcm.e.zgLUCv9$bEp3TJMx3.6GqNcx5MpLEpWyNgVrvjW6zDXhbu7.iIeqIuH7z0rN7zGnSWU18lIGI7B9BK3ShmPIYsF7hPLup/ +! +management api http-commands + no shutdown +! +no service interface inactive port-id allocation disabled +! +transceiver qsfp default-mode 4x10G +! +service routing protocols model multi-agent +! +hostname r3 +! +spanning-tree mode mstp +! +system l1 + unsupported speed action error + unsupported error-correction action error +! +management api gnmi + transport grpc default +! +management api netconf + transport ssh default +! +interface Ethernet2 +! +interface Ethernet10 + no switchport + ip address 192.168.252.5/24 + mtu 1400 +! +interface Management0 + ip address 172.20.20.103/24 +! +ip routing +! +ip route 0.0.0.0/0 172.20.20.1 +! +router multicast + ipv4 + software-forwarding kernel + ! + ipv6 + software-forwarding kernel +! +end diff --git a/src/tests/sns4sns26/clab/sns4sns26.clab.yml b/src/tests/sns4sns26/clab/sns4sns26.clab.yml index c07146855..8f8a94aec 100644 --- a/src/tests/sns4sns26/clab/sns4sns26.clab.yml +++ b/src/tests/sns4sns26/clab/sns4sns26.clab.yml @@ -45,7 +45,13 @@ topology: mgmt-ipv4: 172.20.20.102 startup-config: r2-startup.cfg + r3: + kind: arista_ceos + mgmt-ipv4: 172.20.20.103 + startup-config: r3-startup.cfg + links: - endpoints: ["r1:eth2", "r2:eth1"] + - endpoints: ["r2:eth3", "r3:eth2"] - endpoints: ["r1:eth10", "macvlan:enp0s4"] # connect to site A virtual network - - endpoints: ["r2:eth10", "macvlan:enp0s5"] # connect to site B virtual network + - endpoints: ["r3:eth10", "macvlan:enp0s5"] # connect to site B virtual network diff --git a/src/tests/sns4sns26/data/mock_osm_mapping.json b/src/tests/sns4sns26/data/mock_osm_mapping.json index 7edc0ed84..b9d04e949 100644 --- a/src/tests/sns4sns26/data/mock_osm_mapping.json +++ b/src/tests/sns4sns26/data/mock_osm_mapping.json @@ -1,4 +1,4 @@ [ - {"service_endpoint_id": "SiteA", "device-id": "cluster-a", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:SiteA"}, "site-id": "A"}}, - {"service_endpoint_id": "SiteB", "device-id": "cluster-b", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:SiteB"}, "site-id": "B"}} + {"service_endpoint_id": "SiteA", "device-id": "router-1", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:SiteA"}, "site-id": "A"}}, + {"service_endpoint_id": "SiteB", "device-id": "router-3", "service_mapping_info": {"bearer": {"bearer-reference": "SNS4SNS26:SiteB"}, "site-id": "B"}} ] diff --git a/src/tests/sns4sns26/data/tfs-topology.json b/src/tests/sns4sns26/data/tfs-topology.json index d05cdfb07..3c73515d8 100644 --- a/src/tests/sns4sns26/data/tfs-topology.json +++ b/src/tests/sns4sns26/data/tfs-topology.json @@ -49,6 +49,17 @@ "username": "admin", "password": "admin", "use_tls": false }}} ]} + }, + { + "device_id": {"device_uuid": {"uuid": "router-3"}}, "device_type": "packet-router", + "device_drivers": ["DEVICEDRIVER_GNMI_OPENCONFIG"], + "device_config": {"config_rules": [ + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/address", "resource_value": "172.20.20.103"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/port", "resource_value": "6030"}}, + {"action": "CONFIGACTION_SET", "custom": {"resource_key": "_connect/settings", "resource_value": { + "username": "admin", "password": "admin", "use_tls": false + }}} + ]} } ], "links": [ @@ -67,6 +78,21 @@ ] }, + { + "link_id": {"link_uuid": {"uuid": "router-2/Ethernet3==router-3/Ethernet2"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "router-2"}}, "endpoint_uuid": {"uuid": "Ethernet3"}}, + {"device_id": {"device_uuid": {"uuid": "router-3"}}, "endpoint_uuid": {"uuid": "Ethernet2"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "router-3/Ethernet2==router-2/Ethernet3"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "router-3"}}, "endpoint_uuid": {"uuid": "Ethernet2"}}, + {"device_id": {"device_uuid": {"uuid": "router-2"}}, "endpoint_uuid": {"uuid": "Ethernet3"}} + ] + }, + { "link_id": {"link_uuid": {"uuid": "router-1/Ethernet10==cluster-a/eth1"}}, "link_endpoint_ids": [ @@ -83,17 +109,17 @@ }, { - "link_id": {"link_uuid": {"uuid": "router-2/Ethernet10==cluster-b/eth1"}}, + "link_id": {"link_uuid": {"uuid": "router-3/Ethernet10==cluster-b/eth1"}}, "link_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": "router-2"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, + {"device_id": {"device_uuid": {"uuid": "router-3"}}, "endpoint_uuid": {"uuid": "Ethernet10"}}, {"device_id": {"device_uuid": {"uuid": "cluster-b"}}, "endpoint_uuid": {"uuid": "eth1"}} ] }, { - "link_id": {"link_uuid": {"uuid": "cluster-b/eth1==router-2/Ethernet10"}}, + "link_id": {"link_uuid": {"uuid": "cluster-b/eth1==router-3/Ethernet10"}}, "link_endpoint_ids": [ {"device_id": {"device_uuid": {"uuid": "cluster-b"}}, "endpoint_uuid": {"uuid": "eth1"}}, - {"device_id": {"device_uuid": {"uuid": "router-2"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} + {"device_id": {"device_uuid": {"uuid": "router-3"}}, "endpoint_uuid": {"uuid": "Ethernet10"}} ] } ] diff --git a/src/tests/sns4sns26/scripts/clab-cli-dc2.sh b/src/tests/sns4sns26/scripts/clab-cli-dc2.sh deleted file mode 100644 index 54b4394af..000000000 --- a/src/tests/sns4sns26/scripts/clab-cli-dc2.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# 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. - -docker exec -it clab-sns4sns26-dc2 bash diff --git a/src/tests/sns4sns26/scripts/clab-cli-dc1.sh b/src/tests/sns4sns26/scripts/clab-cli-r3.sh similarity index 94% rename from src/tests/sns4sns26/scripts/clab-cli-dc1.sh rename to src/tests/sns4sns26/scripts/clab-cli-r3.sh index 4e22b3eec..fcb577295 100644 --- a/src/tests/sns4sns26/scripts/clab-cli-dc1.sh +++ b/src/tests/sns4sns26/scripts/clab-cli-r3.sh @@ -13,4 +13,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -docker exec -it clab-sns4sns26-dc1 bash +docker exec -it clab-sns4sns26-r2 Cli -- GitLab From 515e3324860b7cecd7fd927419529e034a88b120 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 08:05:59 +0000 Subject: [PATCH 25/72] NBI component - IETF L2VPN connector: - Fixed bearers for SNS4SNS'26 --- src/nbi/service/ietf_l2vpn/Constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nbi/service/ietf_l2vpn/Constants.py b/src/nbi/service/ietf_l2vpn/Constants.py index 9ed1c64fe..6968daad1 100644 --- a/src/nbi/service/ietf_l2vpn/Constants.py +++ b/src/nbi/service/ietf_l2vpn/Constants.py @@ -32,8 +32,8 @@ BEARER_MAPPINGS = { 'OSM-E2E:r3:Ethernet10': ('r3', 'Ethernet10', None, None, 0, '172.16.3.1', 24, None, None), # SNS4SNS'26 - 'SNS4SNS26:SiteA': ('cluster-a', 'eth1', None, None, 0, '192.168.251.100', 24, None, None), - 'SNS4SNS26:SiteB': ('cluster-b', 'eth1', None, None, 0, '192.168.252.100', 24, None, None), + 'SNS4SNS26:SiteA': ('router-1', 'Ethernet10', None, None, 0, '192.168.251.5', 24, None, None), + 'SNS4SNS26:SiteB': ('router-3', 'Ethernet10', None, None, 0, '192.168.252.5', 24, None, None), # OFC'22 'OFC22:R1-EMU:13/1/2': ('R1-EMU', '13/1/2', '10.10.10.1', '65000:100', 400, '3.3.2.1', 24, None, None), -- GitLab From c6e3a0db613be3307d9e4859a495c236146cc271 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 08:32:09 +0000 Subject: [PATCH 26/72] NBI component: - Reordered Dockerfile to increase buildcache hits --- src/nbi/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nbi/Dockerfile b/src/nbi/Dockerfile index 1b0d841f3..025120a94 100644 --- a/src/nbi/Dockerfile +++ b/src/nbi/Dockerfile @@ -72,7 +72,6 @@ RUN python3 -m pip install -r requirements.txt # Add component files into working directory WORKDIR /var/teraflow -COPY src/nbi/. nbi/ COPY src/context/__init__.py context/__init__.py COPY src/context/client/. context/client/ COPY src/device/__init__.py device/__init__.py @@ -95,6 +94,7 @@ COPY src/vnt_manager/__init__.py vnt_manager/__init__.py COPY src/vnt_manager/client/. vnt_manager/client/ RUN mkdir -p /var/teraflow/tests/tools COPY src/tests/tools/mock_osm/. tests/tools/mock_osm/ +COPY src/nbi/. nbi/ # Start the service # NOTE: Configured single worker to prevent issues with multi-worker synchronization. To be invetsigated. -- GitLab From 86c42070b855b0c212d7bec04112ed468d0cd26b Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 08:32:24 +0000 Subject: [PATCH 27/72] End-to-end test - SNS4SNS 2026: - Updated router startup configs --- src/tests/sns4sns26/clab/r1-startup.cfg | 4 ---- src/tests/sns4sns26/clab/r3-startup.cfg | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/tests/sns4sns26/clab/r1-startup.cfg b/src/tests/sns4sns26/clab/r1-startup.cfg index c7ee1bc0a..65a0db647 100644 --- a/src/tests/sns4sns26/clab/r1-startup.cfg +++ b/src/tests/sns4sns26/clab/r1-startup.cfg @@ -30,11 +30,7 @@ management api netconf ! interface Ethernet2 ! -interface Ethernet9 -! interface Ethernet10 - no switchport - ip address 192.168.251.5/24 mtu 1400 ! interface Management0 diff --git a/src/tests/sns4sns26/clab/r3-startup.cfg b/src/tests/sns4sns26/clab/r3-startup.cfg index 7faa090ad..23db6adae 100644 --- a/src/tests/sns4sns26/clab/r3-startup.cfg +++ b/src/tests/sns4sns26/clab/r3-startup.cfg @@ -31,8 +31,6 @@ management api netconf interface Ethernet2 ! interface Ethernet10 - no switchport - ip address 192.168.252.5/24 mtu 1400 ! interface Management0 -- GitLab From 0d56613c4d2f90253503685da9b585562743715d Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 09:23:48 +0000 Subject: [PATCH 28/72] NBI component - IETF L2VPN connector: - Fixed parsing of encalsulation types - Fixed adaptations of OSM request on encapsulation types --- src/nbi/service/ietf_l2vpn/Handlers.py | 24 +++++++++++++++---- .../ietf_l2vpn/L2VPN_SiteNetworkAccesses.py | 7 +++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/nbi/service/ietf_l2vpn/Handlers.py b/src/nbi/service/ietf_l2vpn/Handlers.py index 18b17c4b2..53d2f889b 100644 --- a/src/nbi/service/ietf_l2vpn/Handlers.py +++ b/src/nbi/service/ietf_l2vpn/Handlers.py @@ -113,12 +113,26 @@ def process_site_network_access( service_uuid = network_access['vpn-attachment']['vpn-id'] network_access_connection = network_access['connection'] - encapsulation_type = network_access_connection['encapsulation-type'] + + encapsulation_type = network_access_connection.get('encapsulation-type', 'ietf-l2vpn-svc:ethernet') encapsulation_type = encapsulation_type.replace('ietf-l2vpn-svc:', '') - if encapsulation_type != 'vlan': - encapsulation_type = network_access_connection['encapsulation-type'] - MSG = 'EncapsulationType({:s}) not supported' - raise NotImplementedError(MSG.format(str(encapsulation_type))) + + eth_inf_type = network_access_connection.get('eth-inf-type', 'ietf-l2vpn-svc:untagged') + eth_inf_type = eth_inf_type.replace('ietf-l2vpn-svc:', '') + + if encapsulation_type == 'ethernet' and eth_inf_type == 'untagged': + if 'tagged-interface' in network_access_connection: + MSG = 'Malformed NetworkAccessConnection({:s})' + raise Exception(MSG.format(str(network_access_connection))) + elif encapsulation_type == 'vlan' and eth_inf_type == 'tagged': + if 'tagged-interface' not in network_access_connection: + MSG = 'Malformed NetworkAccessConnection({:s})' + raise Exception(MSG.format(str(network_access_connection))) + else: + encapsulation_type = network_access_connection.get('encapsulation-type', 'ietf-l2vpn-svc:ethernet') + eth_inf_type = network_access_connection.get('eth-inf-type', 'ietf-l2vpn-svc:untagged') + MSG = 'Combination EncapsulationType({:s})/EthernetInterfaceType({:s}) not supported' + raise NotImplementedError(MSG.format(str(encapsulation_type), str(eth_inf_type))) cvlan_tag_id = None if 'tagged-interface' in network_access_connection: diff --git a/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py b/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py index 7588e2ca9..caf868e71 100644 --- a/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py +++ b/src/nbi/service/ietf_l2vpn/L2VPN_SiteNetworkAccesses.py @@ -25,6 +25,7 @@ from nbi.service._tools.Authentication import HTTP_AUTH from nbi.service._tools.HttpStatusCodes import ( HTTP_CREATED, HTTP_NOCONTENT, HTTP_SERVERERROR ) +from .Constants import DEFAULT_MTU from .Handlers import process_site_network_access from .YangValidator import YangValidator @@ -113,8 +114,12 @@ class L2VPN_SiteNetworkAccesses(Resource): if 'encapsulation-type' in connection: if connection['encapsulation-type'] == 'dot1q-vlan-tagged': connection['encapsulation-type'] = 'vlan' + if 'eth-inf-type' not in connection: + connection['eth-inf-type'] = 'tagged' else: connection['encapsulation-type'] = 'ethernet' + if 'eth-inf-type' not in connection: + connection['eth-inf-type'] = 'untagged' if 'tagged-interface' in connection: tagged_interface = connection['tagged-interface'] if 'dot1q-vlan-tagged' in tagged_interface: @@ -131,7 +136,7 @@ class L2VPN_SiteNetworkAccesses(Resource): if 'service' not in site_network_access: site_network_access['service'] = dict() if 'svc-mtu' not in site_network_access['service']: - site_network_access['service']['svc-mtu'] = 1500 + site_network_access['service']['svc-mtu'] = DEFAULT_MTU context_client = ContextClient() vpn_services = list() -- GitLab From 469138e4f815a1416de17c2ef282e48c0619b9df Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 11:06:38 +0000 Subject: [PATCH 29/72] Service component - L3NM gNMI OpenConfig: - Fix rule composer to do not remove (sub-)interfaces not in VLANs and preserve MTUs --- .../ConfigRuleComposer.py | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index a9e7b9223..d8a431b34 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -54,15 +54,16 @@ def _safe_bool(value: Optional[object]) -> Optional[bool]: def _interface( interface : str, if_type : Optional[str] = 'l3ipvlan', index : int = 0, vlan_id : Optional[int] = None, address_ip : Optional[str] = None, address_prefix : Optional[int] = None, mtu : Optional[int] = None, - enabled : bool = True + enabled : Optional[bool] = True ) -> Tuple[str, Dict]: path = '/interface[{:s}]/subinterface[{:d}]'.format(interface, index) - data = {'name': interface, 'type': if_type, 'index': index, 'enabled': enabled} + data = {'name': interface, 'type': if_type, 'index': index} if if_type is not None: data['type'] = if_type if vlan_id is not None: data['vlan_id'] = vlan_id if address_ip is not None: data['address_ip'] = address_ip if address_prefix is not None: data['address_prefix'] = address_prefix if mtu is not None: data['mtu'] = mtu + if enabled is not None: data['enabled'] = enabled return path, data def _network_instance(ni_name : str, ni_type : str) -> Tuple[str, Dict]: @@ -114,6 +115,7 @@ class EndpointComposer: self.ipv4_prefix_len = None self.explicit_vlan_ids : Set[int] = set() self.force_trunk = False + self.mtu : Optional[int] = None def _add_vlan_id(self, vlan_id : Optional[int]) -> None: if vlan_id is not None: @@ -124,6 +126,9 @@ class EndpointComposer: return vlan_id = _safe_int(json_settings.get('vlan_id', json_settings.get('vlan-id'))) self._add_vlan_id(vlan_id) + mtu = _safe_int(json_settings.get('mtu')) + if mtu is not None: + self.mtu = mtu def configure(self, endpoint_obj : Optional[EndPoint], settings : Optional[TreeNode]) -> None: if endpoint_obj is not None: @@ -185,17 +190,25 @@ class EndpointComposer: ))) if delete: - config_rules.extend([ - json_config_rule(*_interface( - self.objekt.name, index=sub_interface_index, address_ip=None, - address_prefix=None, enabled=False, vlan_id=vlan_id - )), - ]) + if sub_interface_index == 0: + config_rules.extend([ + json_config_rule(*_interface( + self.objekt.name, index=sub_interface_index, address_ip=self.ipv4_address, + address_prefix=self.ipv4_prefix_len, enabled=None, vlan_id=vlan_id, mtu=None + )), + ]) + else: + config_rules.extend([ + json_config_rule(*_interface( + self.objekt.name, index=sub_interface_index, address_ip=None, + address_prefix=None, enabled=None, vlan_id=vlan_id, mtu=None + )), + ]) else: config_rules.extend([ json_config_rule(*_interface( self.objekt.name, index=sub_interface_index, address_ip=self.ipv4_address, - address_prefix=self.ipv4_prefix_len, enabled=True, vlan_id=vlan_id + address_prefix=self.ipv4_prefix_len, enabled=True, vlan_id=vlan_id, mtu=self.mtu )), ]) return config_rules @@ -207,6 +220,7 @@ class EndpointComposer: 'address_prefix': self.ipv4_prefix_len, 'explicit_vlan_ids': list(self.explicit_vlan_ids), 'force_trunk' : self.force_trunk, + 'mtu' : self.mtu, } def __str__(self): @@ -226,6 +240,7 @@ class DeviceComposer: self.service_vlan_id : Optional[int] = None self.access_vlan_tagged = False self.vlan_ids : Set[int] = set() + self.interface_mtu : Dict[str, int] = dict() def set_endpoint_alias(self, endpoint_name : str, endpoint_uuid : str) -> None: self.aliases[endpoint_name] = endpoint_uuid @@ -238,6 +253,7 @@ class DeviceComposer: def configure(self, device_obj : Device, settings : Optional[TreeNode]) -> None: self.objekt = device_obj + self.interface_mtu = dict() for endpoint_obj in device_obj.device_endpoints: endpoint_uuid = endpoint_obj.endpoint_id.endpoint_uuid.uuid self.set_endpoint_alias(endpoint_obj.name, endpoint_uuid) @@ -255,6 +271,8 @@ class DeviceComposer: resource_value = json.loads(config_rule_custom.resource_value) management = resource_value.get('management', False) if management: mgmt_ifaces.add(if_name) + mtu = _safe_int(resource_value.get('mtu')) + if mtu is not None: self.interface_mtu[if_name] = mtu # Find data plane interfaces for config_rule in device_obj.device_config.config_rules: @@ -287,6 +305,13 @@ class DeviceComposer: next_hop = resource_value['next_hop'] self.static_routes.setdefault(prefix, dict())[metric] = next_hop + for if_name, mtu in self.interface_mtu.items(): + if if_name in mgmt_ifaces: continue + if if_name not in self.aliases: continue + endpoint = self.get_endpoint(if_name) + if endpoint.mtu is None: + endpoint.mtu = mtu + if settings is None: return json_settings : Dict = settings.value static_routes : List[Dict] = json_settings.get('static_routes', []) @@ -325,7 +350,8 @@ class DeviceComposer: parent_interfaces.add(endpoint.objekt.name) if not delete and endpoint.objekt.name not in configured_parents: config_rules.append(json_config_rule_set(*_interface( - endpoint.objekt.name, index=0, address_ip=None, address_prefix=None, enabled=True + endpoint.objekt.name, index=0, address_ip=None, address_prefix=None, + enabled=True, mtu=endpoint.mtu ))) configured_parents.add(endpoint.objekt.name) config_rules.extend(endpoint.get_config_rules( @@ -334,10 +360,6 @@ class DeviceComposer: )) self.vlan_ids.update(endpoint.explicit_vlan_ids) - if delete: - for if_name in sorted(parent_interfaces): - config_rules.append(json_config_rule_delete(*_interface_switched_vlan(if_name))) - for vlan_id in sorted(self.vlan_ids): vlan_name = 'tfs-vlan-{:s}'.format(str(vlan_id)) config_rules.append(json_config_rule(*_network_instance_vlan( -- GitLab From bb0cd35891699dccf3c24c1eb8aee1548cb5d971 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 11:07:14 +0000 Subject: [PATCH 30/72] Device component - gNMI OpenConfig Driver: - Enhance deletion of interfaces to respect MTUs on parent interfaces --- .../gnmi_openconfig/handlers/Interface.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py b/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py index d59bff7a6..8fb0d0331 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py @@ -28,9 +28,22 @@ class InterfaceHandler(_Handler): self, resource_key : str, resource_value : Dict, yang_handler : YangHandler, delete : bool = False ) -> Tuple[str, str]: if_name = get_str(resource_value, 'name' ) # ethernet-1/1 - sif_index = get_int(resource_value, 'index', 0) # 0 + sif_index = get_int(resource_value, 'index', None) # 0 if delete: + address_ip = get_str(resource_value, 'address_ip', None) + if address_ip is not None: + if sif_index is None: + return None, None + PATH_TMPL = ( + '/interfaces/interface[name={:s}]/subinterfaces/subinterface[index={:d}]' + '/openconfig-if-ip:ipv4/addresses/address[ip={:s}]' + ) + str_path = PATH_TMPL.format(if_name, sif_index, address_ip) + return str_path, json.dumps({}) + + if sif_index is None: + return None, None PATH_TMPL = '/interfaces/interface[name={:s}]/subinterfaces/subinterface[index={:d}]' str_path = PATH_TMPL.format(if_name, sif_index) str_data = json.dumps({}) @@ -51,7 +64,7 @@ class InterfaceHandler(_Handler): return str_path, str_data - enabled = get_bool(resource_value, 'enabled', True) # True/False + enabled = get_bool(resource_value, 'enabled', None) # True/False #if_type = get_str (resource_value, 'type' ) # 'l3ipvlan' vlan_id = get_int (resource_value, 'vlan_id', ) # 127 address_ip = get_str (resource_value, 'address_ip' ) # 172.16.0.1 @@ -65,6 +78,14 @@ class InterfaceHandler(_Handler): if enabled is not None: yang_if.create_path('config/enabled', enabled) if mtu is not None: yang_if.create_path('config/mtu', mtu) + if sif_index is None: + str_path = '/interfaces/interface[name={:s}]'.format(if_name) + str_data = yang_if.print_mem('json') + json_data = json.loads(str_data) + json_data = json_data['openconfig-interfaces:interface'][0] + str_data = json.dumps(json_data) + return str_path, str_data + yang_sifs : libyang.DContainer = yang_if.create_path('subinterfaces') yang_sif_path = 'subinterface[index="{:d}"]'.format(sif_index) yang_sif : libyang.DContainer = yang_sifs.create_path(yang_sif_path) -- GitLab From 1ab335d46ac2166371289b981edbf1aadee39217 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 11:52:33 +0000 Subject: [PATCH 31/72] Device component - gNMI OpenConfig Driver: - Fixed management of MTUs --- .../gnmi_openconfig/handlers/Interface.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py b/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py index 8fb0d0331..45298596d 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py @@ -20,6 +20,8 @@ from .YangHandler import YangHandler LOGGER = logging.getLogger(__name__) +MIN_MTU = 68 + class InterfaceHandler(_Handler): def get_resource_key(self) -> str: return '/interface/subinterface' def get_path(self) -> str: return '/openconfig-interfaces:interfaces' @@ -27,7 +29,7 @@ class InterfaceHandler(_Handler): def compose( self, resource_key : str, resource_value : Dict, yang_handler : YangHandler, delete : bool = False ) -> Tuple[str, str]: - if_name = get_str(resource_value, 'name' ) # ethernet-1/1 + if_name = get_str(resource_value, 'name' ) # ethernet-1/1 sif_index = get_int(resource_value, 'index', None) # 0 if delete: @@ -76,7 +78,9 @@ class InterfaceHandler(_Handler): yang_if : libyang.DContainer = yang_ifs.create_path(yang_if_path) yang_if.create_path('config/name', if_name ) if enabled is not None: yang_if.create_path('config/enabled', enabled) - if mtu is not None: yang_if.create_path('config/mtu', mtu) + + if mtu is not None and mtu >= MIN_MTU: + yang_if.create_path('config/mtu', mtu) if sif_index is None: str_path = '/interfaces/interface[name={:s}]'.format(if_name) @@ -142,7 +146,6 @@ class InterfaceHandler(_Handler): _interface = { 'name' : interface_name, 'type' : interface_type, - 'mtu' : interface_state['mtu'], 'admin-status' : interface_state['admin-status'], 'oper-status' : interface_state['oper-status'], 'management' : interface_state['management'], @@ -157,6 +160,9 @@ class InterfaceHandler(_Handler): _interface['hardware-port'] = interface_state['hardware-port'] if 'transceiver' in interface_state: _interface['transceiver'] = interface_state['transceiver'] + if 'mtu' in interface_state: + mtu = interface_state['mtu'] + if mtu > 0: _interface['mtu'] = mtu entry_interface_key = '/interface[{:s}]'.format(interface_name) entries.append((entry_interface_key, _interface)) @@ -185,6 +191,12 @@ class InterfaceHandler(_Handler): _subinterface['name'] = subinterface_state['name'] if 'enabled' in subinterface_state: _subinterface['enabled'] = subinterface_state['enabled'] + if 'mtu' in subinterface_state: + mtu = subinterface_state['mtu'] + if mtu > 0: + _subinterface['mtu'] = mtu + if 'mtu' not in _interface: + _interface['mtu'] = mtu if 'vlan' in subinterface: vlan = subinterface['vlan'] -- GitLab From 1009c6eb697f173086b32f7768eaf7587adcbe16 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 11:52:46 +0000 Subject: [PATCH 32/72] End-to-end test - SNS4SNS 2026: - Updated router startup configs --- src/tests/sns4sns26/clab/r1-startup.cfg | 1 + src/tests/sns4sns26/clab/r3-startup.cfg | 1 + 2 files changed, 2 insertions(+) diff --git a/src/tests/sns4sns26/clab/r1-startup.cfg b/src/tests/sns4sns26/clab/r1-startup.cfg index 65a0db647..fdac2ee97 100644 --- a/src/tests/sns4sns26/clab/r1-startup.cfg +++ b/src/tests/sns4sns26/clab/r1-startup.cfg @@ -32,6 +32,7 @@ interface Ethernet2 ! interface Ethernet10 mtu 1400 + no switchport ! interface Management0 ip address 172.20.20.101/24 diff --git a/src/tests/sns4sns26/clab/r3-startup.cfg b/src/tests/sns4sns26/clab/r3-startup.cfg index 23db6adae..d11649d88 100644 --- a/src/tests/sns4sns26/clab/r3-startup.cfg +++ b/src/tests/sns4sns26/clab/r3-startup.cfg @@ -32,6 +32,7 @@ interface Ethernet2 ! interface Ethernet10 mtu 1400 + no switchport ! interface Management0 ip address 172.20.20.103/24 -- GitLab From 1c32941f04787e16bf1a74988e8289f84e732f27 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 11:53:37 +0000 Subject: [PATCH 33/72] Service component - L3NM gNMI OpenConfig: - Fixed management of MTUs - Code styling --- .../ConfigRuleComposer.py | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index d8a431b34..6327e84ee 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -58,12 +58,12 @@ def _interface( ) -> Tuple[str, Dict]: path = '/interface[{:s}]/subinterface[{:d}]'.format(interface, index) data = {'name': interface, 'type': if_type, 'index': index} - if if_type is not None: data['type'] = if_type - if vlan_id is not None: data['vlan_id'] = vlan_id - if address_ip is not None: data['address_ip'] = address_ip + if if_type is not None: data['type' ] = if_type + if vlan_id is not None: data['vlan_id' ] = vlan_id + if address_ip is not None: data['address_ip' ] = address_ip if address_prefix is not None: data['address_prefix'] = address_prefix - if mtu is not None: data['mtu'] = mtu - if enabled is not None: data['enabled'] = enabled + if mtu is not None: data['mtu' ] = mtu + if enabled is not None: data['enabled' ] = enabled return path, data def _network_instance(ni_name : str, ni_type : str) -> Tuple[str, Dict]: @@ -102,9 +102,6 @@ def _network_instance_interface(ni_name : str, interface : str, sub_interface_in data = {'name': ni_name, 'id': sub_interface_name, 'interface': interface, 'subinterface': sub_interface_index} return path, data -def _interface_switched_vlan(interface : str) -> Tuple[str, Dict]: - path = '/interface[{:s}]/ethernet/switched-vlan'.format(interface) - return path, {} class EndpointComposer: def __init__(self, endpoint_uuid : str) -> None: @@ -127,7 +124,7 @@ class EndpointComposer: vlan_id = _safe_int(json_settings.get('vlan_id', json_settings.get('vlan-id'))) self._add_vlan_id(vlan_id) mtu = _safe_int(json_settings.get('mtu')) - if mtu is not None: + if mtu is not None and mtu > 0: self.mtu = mtu def configure(self, endpoint_obj : Optional[EndPoint], settings : Optional[TreeNode]) -> None: @@ -215,12 +212,12 @@ class EndpointComposer: def dump(self) -> Dict: return { - 'index' : self.sub_interface_index, - 'address_ip' : self.ipv4_address, - 'address_prefix': self.ipv4_prefix_len, - 'explicit_vlan_ids': list(self.explicit_vlan_ids), - 'force_trunk' : self.force_trunk, - 'mtu' : self.mtu, + 'index' : self.sub_interface_index, + 'address_ip' : self.ipv4_address, + 'address_prefix' : self.ipv4_prefix_len, + 'explicit_vlan_ids' : list(self.explicit_vlan_ids), + 'force_trunk' : self.force_trunk, + 'mtu' : self.mtu, } def __str__(self): -- GitLab From 233a24c1862ca131ddfc7a53ad882333bff3b939 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 11:58:42 +0000 Subject: [PATCH 34/72] Service component - L3NM gNMI OpenConfig: - Added cleanup on ConfigRuleComposer per request - Code styling --- .../ConfigRuleComposer.py | 7 +++++ .../L3NMGnmiOpenConfigServiceHandler.py | 2 ++ .../l3nm_gnmi_openconfig/VlanIdPropagator.py | 26 ++++++++++--------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index 6327e84ee..af1bde157 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -400,6 +400,13 @@ class ConfigRuleComposer: self.vlan_id : Optional[int] = None self.access_vlan_tagged = False + def clean(self) -> None: + self.objekt : Optional[Service] = None + self.aliases : Dict[str, str] = dict() # device_name => device_uuid + self.devices : Dict[str, DeviceComposer] = dict() # device_uuid => DeviceComposer + self.vlan_id : Optional[int] = None + self.access_vlan_tagged = False + def set_device_alias(self, device_name : str, device_uuid : str) -> None: self.aliases[device_name] = device_uuid diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py index e877c6c8d..bc34af818 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/L3NMGnmiOpenConfigServiceHandler.py @@ -45,6 +45,8 @@ class L3NMGnmiOpenConfigServiceHandler(_ServiceHandler): self.__endpoint_map : Dict[Tuple[str, str], Tuple[str, str]] = dict() def _compose_config_rules(self, endpoints : List[Tuple[str, str, Optional[str]]]) -> None: + self.__config_rule_composer.clean() + if len(endpoints) % 2 != 0: raise Exception('Number of endpoints should be even') service_settings = self.__settings_handler.get_service_settings() diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py index 69e2afb62..5b4052786 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py @@ -15,23 +15,25 @@ import json, logging from typing import List, Optional, Tuple from common.DeviceTypes import DeviceTypeEnum -from .ConfigRuleComposer import ConfigRuleComposer +from .ConfigRuleComposer import ConfigRuleComposer, DeviceComposer LOGGER = logging.getLogger(__name__) +ROUTER_TYPES = { + DeviceTypeEnum.PACKET_ROUTER.value, + DeviceTypeEnum.EMULATED_PACKET_ROUTER.value, + DeviceTypeEnum.PACKET_POP.value, + DeviceTypeEnum.PACKET_RADIO_ROUTER.value, + DeviceTypeEnum.EMULATED_PACKET_RADIO_ROUTER.value, +} + +def _is_router_device(self, device : DeviceComposer) -> bool: + return device.objekt is not None and device.objekt.device_type in ROUTER_TYPES + + class VlanIdPropagator: def __init__(self, config_rule_composer : ConfigRuleComposer) -> None: self._config_rule_composer = config_rule_composer - self._router_types = { - DeviceTypeEnum.PACKET_ROUTER.value, - DeviceTypeEnum.EMULATED_PACKET_ROUTER.value, - DeviceTypeEnum.PACKET_POP.value, - DeviceTypeEnum.PACKET_RADIO_ROUTER.value, - DeviceTypeEnum.EMULATED_PACKET_RADIO_ROUTER.value, - } - - def _is_router_device(self, device) -> bool: - return device.objekt is not None and device.objekt.device_type in self._router_types def compose(self, connection_hop_list : List[Tuple[str, str, Optional[str]]]) -> None: link_endpoints = self._compute_link_endpoints(connection_hop_list) @@ -82,6 +84,6 @@ class VlanIdPropagator: device_b = self._config_rule_composer.get_device(device_uuid_b) endpoint_b = device_b.get_endpoint(endpoint_uuid_b) - if self._is_router_device(device_a) and self._is_router_device(device_b): + if _is_router_device(device_a) and _is_router_device(device_b): endpoint_a.set_force_trunk() endpoint_b.set_force_trunk() -- GitLab From 9d62192b041b7110868c51b1d06a48c9c66013d8 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 12:10:25 +0000 Subject: [PATCH 35/72] Service component - L3NM gNMI OpenConfig: - Fixed VlanIdPropagator::is_router_device() --- .../service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py index 5b4052786..88fdb20f1 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/VlanIdPropagator.py @@ -27,7 +27,7 @@ ROUTER_TYPES = { DeviceTypeEnum.EMULATED_PACKET_RADIO_ROUTER.value, } -def _is_router_device(self, device : DeviceComposer) -> bool: +def _is_router_device(device : DeviceComposer) -> bool: return device.objekt is not None and device.objekt.device_type in ROUTER_TYPES -- GitLab From 2e36a4a66cbaef0d8711d24ef6ccaf9b9203aa77 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 12:27:25 +0000 Subject: [PATCH 36/72] Service component - L3NM gNMI OpenConfig: - Removed garbage management of parent interfaces --- .../l3nm_gnmi_openconfig/ConfigRuleComposer.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py index af1bde157..2966e3d6f 100644 --- a/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py +++ b/src/service/service/service_handlers/l3nm_gnmi_openconfig/ConfigRuleComposer.py @@ -339,18 +339,9 @@ class DeviceComposer: if network_instance_name != DEFAULT_NETWORK_INSTANCE: json_config_rule(*_network_instance(network_instance_name, 'L3VRF')) - parent_interfaces : Set[str] = set() - configured_parents : Set[str] = set() for endpoint in self.endpoints.values(): if endpoint.objekt is None: continue - parent_interfaces.add(endpoint.objekt.name) - if not delete and endpoint.objekt.name not in configured_parents: - config_rules.append(json_config_rule_set(*_interface( - endpoint.objekt.name, index=0, address_ip=None, address_prefix=None, - enabled=True, mtu=endpoint.mtu - ))) - configured_parents.add(endpoint.objekt.name) config_rules.extend(endpoint.get_config_rules( network_instance_name, self.service_vlan_id, access_vlan_tagged=self.access_vlan_tagged, delete=delete -- GitLab From c9c72b6b3a8f517779b42f7231a756012178926f Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 13:04:37 +0000 Subject: [PATCH 37/72] Device component - gNMI OpenConfig Driver: - Fixed removal of IP addresses on subinterfaces --- .../gnmi_openconfig/handlers/Interface.py | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py b/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py index 45298596d..76c3f8e24 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/Interface.py @@ -33,37 +33,48 @@ class InterfaceHandler(_Handler): sif_index = get_int(resource_value, 'index', None) # 0 if delete: + if sif_index is None: + return None, None + + root_node : libyang.DContainer = yang_handler.get_data_path( + '/openconfig-interfaces:interfaces' + ) + address_ip = get_str(resource_value, 'address_ip', None) - if address_ip is not None: - if sif_index is None: - return None, None + if address_ip is None: + PATH_TMPL = '/interfaces/interface[name={:s}]/subinterfaces/subinterface[index={:d}]' + str_path = PATH_TMPL.format(if_name, sif_index) + + yang_sif = root_node.find_path('/'.join([ + '', # add slash at the beginning + 'openconfig-interfaces:interfaces', + 'interface[name="{:s}"]'.format(if_name), + 'subinterfaces', + 'subinterface[index="{:d}"]'.format(sif_index), + ])) + else: PATH_TMPL = ( '/interfaces/interface[name={:s}]/subinterfaces/subinterface[index={:d}]' '/openconfig-if-ip:ipv4/addresses/address[ip={:s}]' ) str_path = PATH_TMPL.format(if_name, sif_index, address_ip) - return str_path, json.dumps({}) - if sif_index is None: - return None, None - PATH_TMPL = '/interfaces/interface[name={:s}]/subinterfaces/subinterface[index={:d}]' - str_path = PATH_TMPL.format(if_name, sif_index) - str_data = json.dumps({}) + yang_sif = root_node.find_path('/'.join([ + '', # add slash at the beginning + 'openconfig-interfaces:interfaces', + 'interface[name="{:s}"]'.format(if_name), + 'subinterfaces', + 'subinterface[index="{:d}"]'.format(sif_index), + 'openconfig-if-ip:ipv4', + 'addresses', + 'address[ip="{:s}"]'.format(address_ip) + ])) - root_node : libyang.DContainer = yang_handler.get_data_path( - '/openconfig-interfaces:interfaces' - ) - yang_sif = root_node.find_path('/'.join([ - '', # add slash at the beginning - 'openconfig-interfaces:interfaces', - 'interface[name="{:s}"]'.format(if_name), - 'subinterfaces', - 'subinterface[index="{:d}"]'.format(sif_index), - ])) if yang_sif is not None: yang_sif.unlink() yang_sif.free() + str_data = json.dumps({}) return str_path, str_data enabled = get_bool(resource_value, 'enabled', None) # True/False -- GitLab From 0dfabbf9bcd5709137dcc59b8524f6f3b691805a Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 13:15:00 +0000 Subject: [PATCH 38/72] Device component - gNMI OpenConfig Driver: - Fixed removal of static routes on network instances --- .../handlers/NetworkInstanceStaticRoute.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/device/service/drivers/gnmi_openconfig/handlers/NetworkInstanceStaticRoute.py b/src/device/service/drivers/gnmi_openconfig/handlers/NetworkInstanceStaticRoute.py index 78d77c8cd..4379fd08a 100644 --- a/src/device/service/drivers/gnmi_openconfig/handlers/NetworkInstanceStaticRoute.py +++ b/src/device/service/drivers/gnmi_openconfig/handlers/NetworkInstanceStaticRoute.py @@ -40,6 +40,23 @@ class NetworkInstanceStaticRouteHandler(_Handler): PATH_TMPL = '/network-instances/network-instance[name={:s}]/protocols' PATH_TMPL += '/protocol[identifier={:s}][name={:s}]/static-routes/static[prefix={:s}]' str_path = PATH_TMPL.format(ni_name, identifier, proto_name, prefix) + + root_node : libyang.DContainer = yang_handler.get_data_path( + '/openconfig-network-instance:network-instances' + ) + yang_ni_pr_sr_path = root_node.find_path('/'.join([ + '', # add slash at the beginning + 'openconfig-network-instance:network-instances', + 'network-instance[name="{:s}"]'.format(ni_name), + 'protocols', + 'protocol[identifier="{:s}"][name="{:s}"]'.format(identifier, proto_name), + 'static-routes', + 'static[prefix="{:s}"]'.format(prefix) + ])) + if yang_ni_pr_sr_path is not None: + yang_ni_pr_sr_path.unlink() + yang_ni_pr_sr_path.free() + str_data = json.dumps({}) return str_path, str_data -- GitLab From 98714cfab638234f862e7a882071144d8813e562 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Sat, 24 Jan 2026 13:48:02 +0000 Subject: [PATCH 39/72] NBI component - IETF L2VPN connector: - Minor comment fix --- src/nbi/service/ietf_l2vpn/Constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nbi/service/ietf_l2vpn/Constants.py b/src/nbi/service/ietf_l2vpn/Constants.py index 6968daad1..7eeb0a4b3 100644 --- a/src/nbi/service/ietf_l2vpn/Constants.py +++ b/src/nbi/service/ietf_l2vpn/Constants.py @@ -21,7 +21,7 @@ DEFAULT_BGP_ROUTE_TARGET = '{:d}:{:d}'.format(DEFAULT_BGP_AS, 333) # Logical Resources component whenever the component is available. # Bearer mappings: -# venue:device_uuid:endpoint_uuid => ( +# prefix:device_uuid:endpoint_uuid => ( # device_uuid, endpoint_uuid, router_id, route_dist, sub_if_index, # address_ip, address_prefix, remote_router, circuit_id # ) -- GitLab From 6d2e7372b3772d255dbd65e029daf28cef136352 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Fri, 30 Jan 2026 20:24:12 +0000 Subject: [PATCH 40/72] NBI component: - Removed overwrite of loggers - Removed unneeded imports --- src/nbi/service/NbiApplication.py | 2 +- src/nbi/service/dscm_oc/routes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nbi/service/NbiApplication.py b/src/nbi/service/NbiApplication.py index ad02c754c..65e9c5ac3 100644 --- a/src/nbi/service/NbiApplication.py +++ b/src/nbi/service/NbiApplication.py @@ -18,7 +18,7 @@ from typing import Any, List, Optional, Tuple from flask import Flask, request from flask_restful import Api, Resource from flask_socketio import Namespace, SocketIO -from common.tools.kafka.Variables import KafkaConfig, KafkaTopic +#from common.tools.kafka.Variables import KafkaConfig, KafkaTopic from nbi.Config import SECRET_KEY diff --git a/src/nbi/service/dscm_oc/routes.py b/src/nbi/service/dscm_oc/routes.py index e0ca2766f..b597cef48 100644 --- a/src/nbi/service/dscm_oc/routes.py +++ b/src/nbi/service/dscm_oc/routes.py @@ -33,7 +33,7 @@ from pluggables.client.PluggablesClient import PluggablesClient LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') + blueprint = Blueprint("testconf_dscm", __name__) -- GitLab From 72fca45e4977efd7068b846951e99c340669d6c7 Mon Sep 17 00:00:00 2001 From: Lluis Gifre Renom Date: Sun, 1 Feb 2026 17:38:27 +0000 Subject: [PATCH 41/72] WebUI component: - Fixed URL for D3 library to point to CDN JS Delivr --- src/webui/service/templates/main/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webui/service/templates/main/home.html b/src/webui/service/templates/main/home.html index 4b66463ca..e0ac9f1fe 100644 --- a/src/webui/service/templates/main/home.html +++ b/src/webui/service/templates/main/home.html @@ -88,7 +88,7 @@ - +