diff --git a/app.py b/app.py index 33cf4504f825185235bd83e3384a23d6d9902d4c..d3f1706c0634f1a795ff0788719d762c7f8cd5e6 100644 --- a/app.py +++ b/app.py @@ -25,7 +25,8 @@ from swagger.restconf_namespace import restconf_ns from src.config.constants import NSC_PORT from src.webui.gui import gui_bp from src.config.config import create_config -from src.database.db import init_db +from src.database.db import init_db as init_slice +from src.database.service_db import init_db as init_service # Paths that do not require authentication (Swagger UI and its static assets) # /nsc → Swagger UI @@ -55,7 +56,8 @@ def _check_basic_auth(app): def create_app(): """Create Flask application with configured API and namespaces.""" - init_db() + init_slice() + init_service() app = Flask(__name__) app = create_config(app) CORS(app) diff --git a/src/api/main.py b/src/api/main.py index 221a45ff6c091f71d54a16008607d643ef507e21..0fe0ff35059cce6f0e6a1a3872994cd21b665ab3 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -18,6 +18,7 @@ from src.utils.send_response import send_response import logging from flask import current_app from src.database.db import get_data, delete_data, get_all_data, delete_all_data +from src.database.service_db import delete_by_slice_id, get_data_by_slice_id from src.realizer.tfs.helpers.tfs_connector import tfs_connector from src.utils.safe_get import safe_get from src.database.sysrepo_store import get_data_store, create_data_store, delete_data_store, update_data_store, normalize_libyang_data @@ -618,7 +619,11 @@ class Api: slice_type = "L2" logging.warning(f"Slice type not found in slice intent. Defaulting to L2") logging.debug(f"Send slice to delete in TFS with slice_type {slice_type}") - tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, slice.get("id")) + services = get_data_by_slice_id(slice.get("id")) + for service in services: + id = service.get("service_id") + tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, id) + delete_by_slice_id(slice.get("id")) if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() @@ -670,7 +675,11 @@ class Api: slice_type = "L2" logging.warning(f"Slice type not found in slice intent. Defaulting to L2") logging.debug(f"Send slice to delete in TFS with slice_type {slice_type}") - tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, existing_slice.get("id")) + services = get_data_by_slice_id(existing_slice.get("id")) + for service in services: + id = service.get("service_id") + tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, id) + delete_by_slice_id(slice.get("id")) if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() @@ -689,7 +698,11 @@ class Api: slice_type = "L2" logging.warning(f"Slice type not found in slice intent. Defaulting to L2") logging.debug(f"Send slice to delete in TFS with slice_type {slice_type}") - tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, slice.get("id")) + services = get_data_by_slice_id(slice.get("id")) + for service in services: + id = service.get("service_id") + tfs_connector().nbi_delete(current_app.config["RESTCONF_IP"], slice_type, id) + delete_by_slice_id(slice.get("id")) if current_app.config["TFS_L2VPN_SUPPORT"]: self.slice_service.tfs_l2vpn_delete() delete_data_store(xpath) diff --git a/src/config/.env.example b/src/config/.env.example index 513c6db0cb8e3d4d3f77e1b5bbd528f0cdaa8616..346018715e988daad56147eb46e45d577ea39d79 100644 --- a/src/config/.env.example +++ b/src/config/.env.example @@ -66,7 +66,7 @@ TFS_E2E_IP=127.0.0.1 # ------------------------- # Restconf Controller # ------------------------- -RESTCONF_IP=192.168.27.165 +RESTCONF_IP=127.0.0.1 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO diff --git a/src/database/service_db.py b/src/database/service_db.py new file mode 100644 index 0000000000000000000000000000000000000000..e897bab529e721b8cd7a1a32e118327ce3c88a64 --- /dev/null +++ b/src/database/service_db.py @@ -0,0 +1,233 @@ +# Copyright 2022-2026 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. + +# This file is an original contribution from Telefonica Innovación Digital S.L. + +import sqlite3, logging + +# Database file +DB_NAME = "service.db" + +# Initialize database and create table +def init_db(): + """ + Initialize the SQLite database and create the service table if not exists. + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS service ( + service_id TEXT PRIMARY KEY, + slice_id TEXT NOT NULL + ) + """) + conn.commit() + conn.close() + +# Save data to the database +def save_data(service_id: str, slice_id: str): + """ + Save a new service entry to the database. + + Args: + service_id (str): Unique identifier for the service + slice_id (dict): Unique identifier for the slice + + Raises: + ValueError: If a service with the given service_id already exists + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + try: + cursor.execute("INSERT INTO service (service_id, slice_id) VALUES (?, ?)", (service_id, slice_id)) + conn.commit() + # Handle duplicate service ID + except sqlite3.IntegrityError: + raise ValueError(f"Service with id '{service_id}' already exists.") + finally: + conn.close() + +# Update data in the database +def update_data(service_id: str, new_slice_id: str): + """ + Update an existing service entry in the database. + + Args: + service_id (str): Unique identifier for the service + slice_id (str): Unique identifier for the slice + + Raises: + ValueError: If no service is found with the given service_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("UPDATE service SET slice_id = ? WHERE service_id = ?", (new_slice_id, service_id)) + if cursor.rowcount == 0: + raise ValueError(f"No slice found with id '{service_id}' to update.") + else: + logging.debug(f"Slice '{service_id}' updated.") + conn.commit() + conn.close() + +# Delete data from the database +def delete_data(service_id: str): + """ + Delete a service entry from the database. + + Args: + service_id (str): Unique identifier for the service to delete + + Raises: + ValueError: If no service is found with the given service_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("DELETE FROM service WHERE service_id = ?", (service_id,)) + if cursor.rowcount == 0: + raise ValueError(f"No service found with id '{service_id}' to delete.") + else: + logging.debug(f"Service '{service_id}' deleted.") + conn.commit() + conn.close() + +# Get data from the database +def get_data(service_id: str) -> dict[str, str]: + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + cursor.execute("SELECT * FROM service WHERE service_id = ?", (service_id,)) + row = cursor.fetchone() + + if not row: + conn.close() + raise ValueError(f"No service found with id '{service_id}'.") + + column_names = [desc[0] for desc in cursor.description] + conn.close() + + return dict(zip(column_names, row)) + + +# Get all services +def get_all_data() -> list[dict[str, str]]: + """ + Retrieve all service entries from the database. + + Returns: + list: List of service data dictionaries including service_id and slice_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("SELECT * FROM service") + + rows = cursor.fetchall() + column_names = [description[0] for description in cursor.description] + + conn.close() + + return [dict(zip(column_names, row)) for row in rows] + + +def delete_all_data(): + """ + Delete all service entries from the database. + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + cursor.execute("DELETE FROM service") + conn.commit() + conn.close() + logging.debug("All service data deleted.") + + +def get_data_by_slice_id(slice_id: str) -> list[dict[str, str]]: + """ + Retrieve all service entries associated with a given slice_id. + + Args: + slice_id (str): Identifier of the slice + + Returns: + list: List of service data dictionaries including service_id and slice_id + + Raises: + ValueError: If no services are found with the given slice_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + cursor.execute("SELECT * FROM service WHERE slice_id = ?", (slice_id,)) + rows = cursor.fetchall() + + if not rows: + conn.close() + raise ValueError(f"No services found with slice_id '{slice_id}'.") + + column_names = [desc[0] for desc in cursor.description] + conn.close() + + return [dict(zip(column_names, row)) for row in rows] + +def delete_by_slice_id(slice_id: str): + """ + Delete all service entries associated with a given slice_id. + + Args: + slice_id (str): Identifier of the slice whose services will be deleted + + Raises: + ValueError: If no services are found with the given slice_id + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + cursor.execute("DELETE FROM service WHERE slice_id = ?", (slice_id,)) + + if cursor.rowcount == 0: + conn.close() + raise ValueError(f"No services found with slice_id '{slice_id}' to delete.") + + logging.debug(f"All services with slice_id '{slice_id}' deleted.") + + conn.commit() + conn.close() + + +# Example usage +if __name__ == "__main__": + init_db() + + # Save a service + save_data("service-001", "slice-001") + save_data("service-002", "slice-001") + save_data("service-003", "slice-002") + + # Get the service + result = get_data("service-001") + if result: + print(f"Retrieved service for service-001: {result}") + + # Update the service + update_data("service-001", "slice-002") + + # Delete the service + delete_data("service-001") + + delete_by_slice_id("slice-001") + + + result = get_all_data() + if result: + print(f"Retrieved data: {result}") + delete_all_data() diff --git a/src/mapper/main.py b/src/mapper/main.py index 6c6601db1b918846b967d1f668c3aaa6a4201a23..6a3b160fcb65b49cbc86c79dec97cf2f9ab2da44 100644 --- a/src/mapper/main.py +++ b/src/mapper/main.py @@ -23,6 +23,7 @@ from .process_connnectivity import process_connectivity from src.realizer.main import realizer from flask import current_app from src.database.sysrepo_store import get_data_store, create_data_store, delete_data_store, update_data_store, normalize_libyang_data +from src.database.service_db import save_data, update_data def mapper(ietf_intent, controller_type="TFS"): """ @@ -158,6 +159,9 @@ def mapper(ietf_intent, controller_type="TFS"): "way": way } services.append(service) + if not current_app.config["DUMMY_MODE"]: + # Save mapping from service_id to slice_id + save_data(service_id=safe_get(service, ["id"]), slice_id=service_id) logging.debug(f"Service added: {service}") # Break only for point-to-point