Loading src/config/.env.example +2 −0 Original line number Diff line number Diff line Loading @@ -69,6 +69,8 @@ TFS_E2E_IP=127.0.0.1 RESTCONF_IP=127.0.0.1 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO DATAPLANE_SUPPORT=FRR # ------------------------- # WebUI Loading src/config/config.py +1 −0 Original line number Diff line number Diff line Loading @@ -67,5 +67,6 @@ def create_config(app: Flask): # Restconf Controller app.config["RESTCONF_IP"] = os.getenv("RESTCONF_IP", "127.0.0.1") app.config["SDN_CONTROLLER_TYPE"] = os.getenv("SDN_CONTROLLER_TYPE", "TFS") app.config["DATAPLANE_SUPPORT"] = os.getenv("DATAPLANE_SUPPORT", "FRR") return app src/realizer/restconf/connectors/frr_connector.py 0 → 100644 +89 −0 Original line number Diff line number Diff line # frr_connector.py import logging from netmiko import ConnectHandler class frr_connector(): """Class to interact with FRR devices via SSH using Netmiko.""" def __init__(self, address): self.address = address def execute_commands(self, commands): try: device = { 'device_type': 'linux', 'host': self.address, 'username': 'root', 'password': 'root', } connection = ConnectHandler(**device) output = connection.send_config_set(commands) logging.debug(output) connection.disconnect() except Exception as e: logging.error(f"Failed to execute commands on {self.address}: {str(e)}") raise def setup_slice(self, config: dict, assignments: dict[int, int]) -> list[str]: """ Build PBR commands dynamically based on active slot assignments. Args: config: Device config (interfaces, addresses, etc.) assignments: {slot_number: dscp_value} for currently active slices. e.g. {1: 52, 2: 51} Returns: List of FRR/vtysh commands. """ commands = ["vtysh", "conf te"] # Nexthop groups - one per non-default slice + DEFAULT for i, iface in enumerate(config['output_interfaces'], start=1): commands += [ f"nexthop-group SLICE-{i}", f"nexthop {iface}", "exit", ] # PBR rules - one per active slot, ordered by seq seq = 10 for slot, dscp in sorted(assignments.items()): commands += [ f"pbr-map PBR-DSCP seq {seq}", f"match mark {dscp}", f"set nexthop-group SLICE-{slot}", "exit", ] seq += 10 # Default catch-all rule always last commands += [ "pbr-map PBR-DSCP seq 100", "match dst-ip 0.0.0.0/0", "set nexthop-group DEFAULT", "exit", ] # Apply PBR policy to input interface commands += [ f"interface {config['input_interface']}", f"ip address {config['input_address']}/24", "pbr-policy PBR-DSCP", "exit", "end", ] return commands def delete_all_slices(self) -> list[str]: return [ "vtysh", "conf te", "no pbr-map PBR-DSCP", "no nexthop-group SLICE-1", "no nexthop-group SLICE-2", "no nexthop-group DEFAULT", "end", ] No newline at end of file src/realizer/restconf/restconf_connect.py +48 −0 Original line number Diff line number Diff line from src.utils.slice_manager import SliceManager from .connectors.tfs_connector import tfs_connector from .connectors.frr_connector import frr_connector from src.utils.send_response import send_response from src.utils.safe_get import safe_get from flask import current_app from src.config.constants import NBI_L2_PATH, NBI_L3_PATH import logging FRR_DEVICES = [ { "management_address": "10.60.125.20", "input_address": "192.168.24.2", "input_interface": "eth2", "output_interfaces": ["192.168.23.3", "192.168.25.5", "192.168.12.1"], }, { "management_address": "10.60.125.50", "input_address": "192.168.56.5", "input_interface": "eth4", "output_interfaces": ["192.168.35.3", "192.168.25.2", "192.168.15.1"], }, ] _slice_manager = SliceManager() def restconf_connect(requests, restconf_ip): """ Connect to controller and upload services. Loading @@ -24,6 +44,34 @@ def restconf_connect(requests, restconf_ip): elif key == "ietf-l3vpn-svc:l3vpn-svc": path = NBI_L3_PATH dscp = safe_get(intent, [ "sites", 0, "site-network-accesses", [0], "service", "qos", "qos-classification-policy", "rule", 0, "match-flow", "dscp" ]) if dscp is not None and current_app.config["DATAPLANE_SUPPORT"] == "FRR": if _slice_manager.is_full(): return send_response( False, code=429, message=f"No available slices. Current assignments: {_slice_manager.get_active_assignments()}" ) slot = _slice_manager.assign_slot(dscp) if slot is None: return send_response(False, code=429, message=f"Could not assign slot for DSCP {dscp}") logging.info(f"Assigned DSCP {dscp} to SLICE-{slot}") assignments = _slice_manager.get_active_assignments() for device_config in FRR_DEVICES: connector = frr_connector(device_config["management_address"]) commands = connector.setup_slice(device_config, assignments) try: connector.execute_commands(commands) except Exception as e: return send_response(False, code=500, message=f"FRR config failed on {device_config['management_address']}: {str(e)}") else: return send_response(False, code=400, message=f"Unsupported service type: {key}") Loading src/tests/requests/frr_request.json 0 → 100644 +296 −0 Original line number Diff line number Diff line { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": { "slo-sle-template": [ { "id": "gold", "description": "", "slo-policy": { "metric-bound": [ { "metric-type": "two-way-bandwidth", "metric-unit": "Mbps", "bound": 100 }, { "metric-type": "two-way-delay-maximum", "metric-unit": "milliseconds", "bound": 5 } ] }, "sle-policy": { "security": "", "isolation": "", "path-constraints": { "service-functions": {}, "diversity": { "diversity-type": "" } } } }, { "id": "silver", "description": "", "slo-policy": { "metric-bound": [ { "metric-type": "two-way-bandwidth", "metric-unit": "Mbps", "bound": 10 }, { "metric-type": "two-way-delay-maximum", "metric-unit": "milliseconds", "bound": 20 } ] }, "sle-policy": { "security": "", "isolation": "", "path-constraints": { "service-functions": {}, "diversity": { "diversity-type": "" } } } }, { "id": "bronze", "description": "", "slo-policy": { "metric-bound": [ { "metric-type": "two-way-bandwidth", "metric-unit": "Mbps", "bound": 1 }, { "metric-type": "two-way-delay-maximum", "metric-unit": "milliseconds", "bound": 20 } ] }, "sle-policy": { "security": "", "isolation": "", "path-constraints": { "service-functions": {}, "diversity": { "diversity-type": "" } } } } ] }, "slice-service": [ { "id": "unity-transport-slice", "description": "Transport network slice service comprising three paths with three different slo templates. SDPs are matched by DSCP of incoming traffic", "service-tags": { "tag-type": [ { "tag-type": "service", "tag-type-value": [ "L3" ] } ] }, "slo-sle-template": "bronze", "status": {}, "sdps": { "sdp": [ { "id": "SDP1", "geo-location": {}, "node-id": "CU", "sdp-ip-address": "", "tp-ref": "", "service-match-criteria": { "match-criterion": [ { "index": 1, "match-type": [ { "type": "dscp", "dscp": [51] } ], "target-connection-group-id": "CU_DU_1" }, { "index": 2, "match-type": [ { "type": "dscp", "dscp": [52] } ], "target-connection-group-id": "CU_DU_2" }, { "index": 3, "match-type": [ { "type": "any" } ], "target-connection-group-id": "CU_DU_3" } ] }, "incoming-qos-policy": {}, "outgoing-qos-policy": {}, "sdp-peering": { "peer-sap-id": "", "protocols": {} }, "ac-svc-ref": [], "attachment-circuits": { "attachment-circuit": [ { "id": "acSDP1", "ac-node-id": "PE-A", "ac-tp-id": "GigabitEthernet5/0/0/0", "ac-ipv4-address": "4.4.4.4", "ac-ipv4-prefix-length": 24, "status": {} } ] }, "status": {}, "sdp-monitoring": {} }, { "id": "SDP2", "geo-location": {}, "node-id": "DU", "sdp-ip-address": "", "tp-ref": "", "service-match-criteria": { "match-criterion": [ { "index": 1, "match-type": [ { "type": "dscp", "dscp": [51] } ], "target-connection-group-id": "CU_DU_1" }, { "index": 2, "match-type": [ { "type": "dscp", "dscp": [52] } ], "target-connection-group-id": "CU_DU_2" }, { "index": 3, "match-type": [ { "type": "any" } ], "target-connection-group-id": "CU_DU_3" } ] }, "incoming-qos-policy": {}, "outgoing-qos-policy": {}, "sdp-peering": { "peer-sap-id": "", "protocols": {} }, "ac-svc-ref": [], "attachment-circuits": { "attachment-circuit": [ { "id": "acSDP2", "ac-node-id": "PE-B", "ac-tp-id": "GigabitEthernet5/0/0/0", "ac-ipv4-address": "10.60.60.105", "ac-ipv4-prefix-length": 24, "status": {} } ] }, "status": {}, "sdp-monitoring": {} } ] }, "connection-groups": { "connection-group": [ { "id": "CU_DU_1", "connectivity-type": "point-to-point", "slo-sle-template": "silver", "connectivity-construct": [ { "id": "1", "p2p-sender-sdp": "SDP1", "p2p-receiver-sdp": "SDP2" }, { "id": "2", "p2p-sender-sdp": "SDP2", "p2p-receiver-sdp": "SDP1" } ], "status": {} }, { "id": "CU_DU_2", "connectivity-type": "point-to-point", "slo-sle-template": "gold", "connectivity-construct": [ { "id": "3", "p2p-sender-sdp": "SDP1", "p2p-receiver-sdp": "SDP2" }, { "id": "4", "p2p-sender-sdp": "SDP2", "p2p-receiver-sdp": "SDP1" } ], "status": {} }, { "id": "CU_DU_3", "connectivity-type": "point-to-point", "connectivity-construct": [ { "id": "5", "p2p-sender-sdp": "SDP1", "p2p-receiver-sdp": "SDP2" }, { "id": "6", "p2p-sender-sdp": "SDP2", "p2p-receiver-sdp": "SDP1" } ], "status": {} } ] } } ] } } Loading
src/config/.env.example +2 −0 Original line number Diff line number Diff line Loading @@ -69,6 +69,8 @@ TFS_E2E_IP=127.0.0.1 RESTCONF_IP=127.0.0.1 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO DATAPLANE_SUPPORT=FRR # ------------------------- # WebUI Loading
src/config/config.py +1 −0 Original line number Diff line number Diff line Loading @@ -67,5 +67,6 @@ def create_config(app: Flask): # Restconf Controller app.config["RESTCONF_IP"] = os.getenv("RESTCONF_IP", "127.0.0.1") app.config["SDN_CONTROLLER_TYPE"] = os.getenv("SDN_CONTROLLER_TYPE", "TFS") app.config["DATAPLANE_SUPPORT"] = os.getenv("DATAPLANE_SUPPORT", "FRR") return app
src/realizer/restconf/connectors/frr_connector.py 0 → 100644 +89 −0 Original line number Diff line number Diff line # frr_connector.py import logging from netmiko import ConnectHandler class frr_connector(): """Class to interact with FRR devices via SSH using Netmiko.""" def __init__(self, address): self.address = address def execute_commands(self, commands): try: device = { 'device_type': 'linux', 'host': self.address, 'username': 'root', 'password': 'root', } connection = ConnectHandler(**device) output = connection.send_config_set(commands) logging.debug(output) connection.disconnect() except Exception as e: logging.error(f"Failed to execute commands on {self.address}: {str(e)}") raise def setup_slice(self, config: dict, assignments: dict[int, int]) -> list[str]: """ Build PBR commands dynamically based on active slot assignments. Args: config: Device config (interfaces, addresses, etc.) assignments: {slot_number: dscp_value} for currently active slices. e.g. {1: 52, 2: 51} Returns: List of FRR/vtysh commands. """ commands = ["vtysh", "conf te"] # Nexthop groups - one per non-default slice + DEFAULT for i, iface in enumerate(config['output_interfaces'], start=1): commands += [ f"nexthop-group SLICE-{i}", f"nexthop {iface}", "exit", ] # PBR rules - one per active slot, ordered by seq seq = 10 for slot, dscp in sorted(assignments.items()): commands += [ f"pbr-map PBR-DSCP seq {seq}", f"match mark {dscp}", f"set nexthop-group SLICE-{slot}", "exit", ] seq += 10 # Default catch-all rule always last commands += [ "pbr-map PBR-DSCP seq 100", "match dst-ip 0.0.0.0/0", "set nexthop-group DEFAULT", "exit", ] # Apply PBR policy to input interface commands += [ f"interface {config['input_interface']}", f"ip address {config['input_address']}/24", "pbr-policy PBR-DSCP", "exit", "end", ] return commands def delete_all_slices(self) -> list[str]: return [ "vtysh", "conf te", "no pbr-map PBR-DSCP", "no nexthop-group SLICE-1", "no nexthop-group SLICE-2", "no nexthop-group DEFAULT", "end", ] No newline at end of file
src/realizer/restconf/restconf_connect.py +48 −0 Original line number Diff line number Diff line from src.utils.slice_manager import SliceManager from .connectors.tfs_connector import tfs_connector from .connectors.frr_connector import frr_connector from src.utils.send_response import send_response from src.utils.safe_get import safe_get from flask import current_app from src.config.constants import NBI_L2_PATH, NBI_L3_PATH import logging FRR_DEVICES = [ { "management_address": "10.60.125.20", "input_address": "192.168.24.2", "input_interface": "eth2", "output_interfaces": ["192.168.23.3", "192.168.25.5", "192.168.12.1"], }, { "management_address": "10.60.125.50", "input_address": "192.168.56.5", "input_interface": "eth4", "output_interfaces": ["192.168.35.3", "192.168.25.2", "192.168.15.1"], }, ] _slice_manager = SliceManager() def restconf_connect(requests, restconf_ip): """ Connect to controller and upload services. Loading @@ -24,6 +44,34 @@ def restconf_connect(requests, restconf_ip): elif key == "ietf-l3vpn-svc:l3vpn-svc": path = NBI_L3_PATH dscp = safe_get(intent, [ "sites", 0, "site-network-accesses", [0], "service", "qos", "qos-classification-policy", "rule", 0, "match-flow", "dscp" ]) if dscp is not None and current_app.config["DATAPLANE_SUPPORT"] == "FRR": if _slice_manager.is_full(): return send_response( False, code=429, message=f"No available slices. Current assignments: {_slice_manager.get_active_assignments()}" ) slot = _slice_manager.assign_slot(dscp) if slot is None: return send_response(False, code=429, message=f"Could not assign slot for DSCP {dscp}") logging.info(f"Assigned DSCP {dscp} to SLICE-{slot}") assignments = _slice_manager.get_active_assignments() for device_config in FRR_DEVICES: connector = frr_connector(device_config["management_address"]) commands = connector.setup_slice(device_config, assignments) try: connector.execute_commands(commands) except Exception as e: return send_response(False, code=500, message=f"FRR config failed on {device_config['management_address']}: {str(e)}") else: return send_response(False, code=400, message=f"Unsupported service type: {key}") Loading
src/tests/requests/frr_request.json 0 → 100644 +296 −0 Original line number Diff line number Diff line { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": { "slo-sle-template": [ { "id": "gold", "description": "", "slo-policy": { "metric-bound": [ { "metric-type": "two-way-bandwidth", "metric-unit": "Mbps", "bound": 100 }, { "metric-type": "two-way-delay-maximum", "metric-unit": "milliseconds", "bound": 5 } ] }, "sle-policy": { "security": "", "isolation": "", "path-constraints": { "service-functions": {}, "diversity": { "diversity-type": "" } } } }, { "id": "silver", "description": "", "slo-policy": { "metric-bound": [ { "metric-type": "two-way-bandwidth", "metric-unit": "Mbps", "bound": 10 }, { "metric-type": "two-way-delay-maximum", "metric-unit": "milliseconds", "bound": 20 } ] }, "sle-policy": { "security": "", "isolation": "", "path-constraints": { "service-functions": {}, "diversity": { "diversity-type": "" } } } }, { "id": "bronze", "description": "", "slo-policy": { "metric-bound": [ { "metric-type": "two-way-bandwidth", "metric-unit": "Mbps", "bound": 1 }, { "metric-type": "two-way-delay-maximum", "metric-unit": "milliseconds", "bound": 20 } ] }, "sle-policy": { "security": "", "isolation": "", "path-constraints": { "service-functions": {}, "diversity": { "diversity-type": "" } } } } ] }, "slice-service": [ { "id": "unity-transport-slice", "description": "Transport network slice service comprising three paths with three different slo templates. SDPs are matched by DSCP of incoming traffic", "service-tags": { "tag-type": [ { "tag-type": "service", "tag-type-value": [ "L3" ] } ] }, "slo-sle-template": "bronze", "status": {}, "sdps": { "sdp": [ { "id": "SDP1", "geo-location": {}, "node-id": "CU", "sdp-ip-address": "", "tp-ref": "", "service-match-criteria": { "match-criterion": [ { "index": 1, "match-type": [ { "type": "dscp", "dscp": [51] } ], "target-connection-group-id": "CU_DU_1" }, { "index": 2, "match-type": [ { "type": "dscp", "dscp": [52] } ], "target-connection-group-id": "CU_DU_2" }, { "index": 3, "match-type": [ { "type": "any" } ], "target-connection-group-id": "CU_DU_3" } ] }, "incoming-qos-policy": {}, "outgoing-qos-policy": {}, "sdp-peering": { "peer-sap-id": "", "protocols": {} }, "ac-svc-ref": [], "attachment-circuits": { "attachment-circuit": [ { "id": "acSDP1", "ac-node-id": "PE-A", "ac-tp-id": "GigabitEthernet5/0/0/0", "ac-ipv4-address": "4.4.4.4", "ac-ipv4-prefix-length": 24, "status": {} } ] }, "status": {}, "sdp-monitoring": {} }, { "id": "SDP2", "geo-location": {}, "node-id": "DU", "sdp-ip-address": "", "tp-ref": "", "service-match-criteria": { "match-criterion": [ { "index": 1, "match-type": [ { "type": "dscp", "dscp": [51] } ], "target-connection-group-id": "CU_DU_1" }, { "index": 2, "match-type": [ { "type": "dscp", "dscp": [52] } ], "target-connection-group-id": "CU_DU_2" }, { "index": 3, "match-type": [ { "type": "any" } ], "target-connection-group-id": "CU_DU_3" } ] }, "incoming-qos-policy": {}, "outgoing-qos-policy": {}, "sdp-peering": { "peer-sap-id": "", "protocols": {} }, "ac-svc-ref": [], "attachment-circuits": { "attachment-circuit": [ { "id": "acSDP2", "ac-node-id": "PE-B", "ac-tp-id": "GigabitEthernet5/0/0/0", "ac-ipv4-address": "10.60.60.105", "ac-ipv4-prefix-length": 24, "status": {} } ] }, "status": {}, "sdp-monitoring": {} } ] }, "connection-groups": { "connection-group": [ { "id": "CU_DU_1", "connectivity-type": "point-to-point", "slo-sle-template": "silver", "connectivity-construct": [ { "id": "1", "p2p-sender-sdp": "SDP1", "p2p-receiver-sdp": "SDP2" }, { "id": "2", "p2p-sender-sdp": "SDP2", "p2p-receiver-sdp": "SDP1" } ], "status": {} }, { "id": "CU_DU_2", "connectivity-type": "point-to-point", "slo-sle-template": "gold", "connectivity-construct": [ { "id": "3", "p2p-sender-sdp": "SDP1", "p2p-receiver-sdp": "SDP2" }, { "id": "4", "p2p-sender-sdp": "SDP2", "p2p-receiver-sdp": "SDP1" } ], "status": {} }, { "id": "CU_DU_3", "connectivity-type": "point-to-point", "connectivity-construct": [ { "id": "5", "p2p-sender-sdp": "SDP1", "p2p-receiver-sdp": "SDP2" }, { "id": "6", "p2p-sender-sdp": "SDP2", "p2p-receiver-sdp": "SDP1" } ], "status": {} } ] } } ] } }