Commit 81cd8cb4 authored by Pablo Armingol's avatar Pablo Armingol
Browse files

feat: implement dynamic IPoWDM L3VPN provisioning and configuration management...

feat: implement dynamic IPoWDM L3VPN provisioning and configuration management for IETF L3VPN driver.
parent 6b78db9e
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -68,7 +68,7 @@ export TFS_COMPONENTS="context device pathcomp service nbi webui"
#export TFS_COMPONENTS="${TFS_COMPONENTS} forecaster"

# Uncomment to activate E2E Orchestrator
#export TFS_COMPONENTS="${TFS_COMPONENTS} e2e_orchestrator"
export TFS_COMPONENTS="${TFS_COMPONENTS} e2e_orchestrator"

# Uncomment to activate VNT Manager
#export TFS_COMPONENTS="${TFS_COMPONENTS} vnt_manager"
@@ -148,7 +148,7 @@ export CRDB_PASSWORD="tfs123"
export CRDB_DEPLOY_MODE="single"

# Disable flag for dropping database, if it exists.
export CRDB_DROP_DATABASE_IF_EXISTS=""
export CRDB_DROP_DATABASE_IF_EXISTS="YES"

# Disable flag for re-deploying CockroachDB from scratch.
export CRDB_REDEPLOY=""
@@ -216,7 +216,7 @@ export QDB_TABLE_MONITORING_KPIS="tfs_monitoring_kpis"
export QDB_TABLE_SLICE_GROUPS="tfs_slice_groups"

# Disable flag for dropping tables if they exist.
export QDB_DROP_TABLES_IF_EXIST=""
export QDB_DROP_TABLES_IF_EXIST="YES"

# Disable flag for re-deploying QuestDB from scratch.
export QDB_REDEPLOY=""
+186 −0
Original line number Diff line number Diff line
{
  "initial": {
    "name": "channel-5",
    "frequency": "194000000",
    "target_output_power": "0.0",
    "operation": "merge",
    "digital_subcarriers_groups": [
      {
        "group_id": 1,
        "digital-subcarrier-id": [
          {
            "subcarrier-id": 1,
            "active": false
          },
          {
            "subcarrier-id": 2,
            "active": false
          },
          {
            "subcarrier-id": 3,
            "active": false
          },
          {
            "subcarrier-id": 4,
            "active": false
          }
        ]
      },
      {
        "group_id": 2,
        "digital-subcarrier-id": [
          {
            "subcarrier-id": 5,
            "active": true
          },
          {
            "subcarrier-id": 6,
            "active": true
          },
          {
            "subcarrier-id": 7,
            "active": true
          },
          {
            "subcarrier-id": 8,
            "active": true
          }
        ]
      },
      {
        "group_id": 3,
        "digital-subcarrier-id": [
          {
            "subcarrier-id": 9,
            "active": false
          },
          {
            "subcarrier-id": 10,
            "active": false
          },
          {
            "subcarrier-id": 11,
            "active": false
          },
          {
            "subcarrier-id": 12,
            "active": false
          }
        ]
      },
      {
        "group_id": 4,
        "digital-subcarrier-id": [
          {
            "subcarrier-id": 13,
            "active": false
          },
          {
            "subcarrier-id": 14,
            "active": false
          },
          {
            "subcarrier-id": 15,
            "active": false
          },
          {
            "subcarrier-id": 16,
            "active": false
          }
        ]
      }
    ]
  },
  "second": {
    "name": "channel-5",
    "frequency": "194000000",
    "target_output_power": "0.0",
    "operation": "merge",
    "digital_subcarriers_groups": [
      {
        "group_id": 1,
        "digital-subcarrier-id": [
          {
            "subcarrier-id": 1,
            "active": false
          },
          {
            "subcarrier-id": 2,
            "active": false
          },
          {
            "subcarrier-id": 3,
            "active": false
          },
          {
            "subcarrier-id": 4,
            "active": false
          }
        ]
      },
      {
        "group_id": 2,
        "digital-subcarrier-id": [
          {
            "subcarrier-id": 5,
            "active": true
          },
          {
            "subcarrier-id": 6,
            "active": true
          },
          {
            "subcarrier-id": 7,
            "active": true
          },
          {
            "subcarrier-id": 8,
            "active": true
          }
        ]
      },
      {
        "group_id": 3,
        "digital-subcarrier-id": [
          {
            "subcarrier-id": 9,
            "active": false
          },
          {
            "subcarrier-id": 10,
            "active": false
          },
          {
            "subcarrier-id": 11,
            "active": false
          },
          {
            "subcarrier-id": 12,
            "active": false
          }
        ]
      },
      {
        "group_id": 4,
        "digital-subcarrier-id": [
          {
            "subcarrier-id": 13,
            "active": false
          },
          {
            "subcarrier-id": 14,
            "active": false
          },
          {
            "subcarrier-id": 15,
            "active": false
          },
          {
            "subcarrier-id": 16,
            "active": false
          }
        ]
      }
    ]
  }
}
 No newline at end of file
+76 −0
Original line number Diff line number Diff line
{
  "initial": {
    "operation": "merge",
    "channels": [
      {
        "name": "channel-5",
        "frequency": "194006250",
        "target_output_power": "-99.0",
        "digital_subcarriers_groups": [
          {
            "group_id": 1,
            "digital-subcarrier-id": [
              {"subcarrier-id": 1, "active": true},
              {"subcarrier-id": 2, "active": true},
              {"subcarrier-id": 3, "active": true},
              {"subcarrier-id": 4, "active": true}
            ]
          }
        ]
      },
      {
        "name": "channel-7",
        "frequency": "194018750",
        "target_output_power": "-99.0",
        "digital_subcarriers_groups": [
          {
            "group_id": 1,
            "digital-subcarrier-id": [
              {"subcarrier-id": 1, "active": false},
              {"subcarrier-id": 2, "active": false},
              {"subcarrier-id": 3, "active": false},
              {"subcarrier-id": 4, "active": false}
            ]
          }
        ]
      }
    ]
  },
  "second": {
    "operation": "merge",
    "channels": [
      {
        "name": "channel-5",
        "frequency": "194006250",
        "target_output_power": "-99.0",
        "digital_subcarriers_groups": [
          {
            "group_id": 1,
            "digital-subcarrier-id": [
              {"subcarrier-id": 1, "active": false},
              {"subcarrier-id": 2, "active": false},
              {"subcarrier-id": 3, "active": false},
              {"subcarrier-id": 4, "active": false}
            ]
          }
        ]
      },
      {
        "name": "channel-7",
        "frequency": "194018750",
        "target_output_power": "-99.0",
        "digital_subcarriers_groups": [
          {
            "group_id": 1,
            "digital-subcarrier-id": [
              {"subcarrier-id": 1, "active": true},
              {"subcarrier-id": 2, "active": true},
              {"subcarrier-id": 3, "active": true},
              {"subcarrier-id": 4, "active": true}
            ]
          }
        ]
      }
    ]
  }
}
+107 −33
Original line number Diff line number Diff line
@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import time
from asyncio import exceptions
import json
import logging
import os
@@ -25,9 +27,11 @@ HEADERS = {
    "Content-Type": "application/yang-data+json"
}

AUTH = ("admin", "admin")

executor = ThreadPoolExecutor()

def generate_l3vpn_template_pair( vpn_id, svc_mtu=1500, svc_input_bandwidth=1000000000, svc_output_bandwidth=1000000000, latency_boundary=10, guaranteed_bw_percent=100):
def generate_l3vpn_template_pair( vpn_id, svc_mtu, svc_input_bandwidth=1000000000, svc_output_bandwidth=1000000000, latency_boundary=10, guaranteed_bw_percent=100):
    template_path = os.path.join(os.path.dirname(__file__), "l3vpn.json")
    with open(template_path, "r") as f:
        template = json.load(f)
@@ -73,7 +77,7 @@ def create_request(resource_value):
            val = json.loads(val)
    except Exception as e:
        LOGGER.error(f"Error parsing resource_value[1] JSON: {e}")
        return None
        return False

    try:
        node_src = val['rule_set']['src'][0]
@@ -104,53 +108,123 @@ def create_request(resource_value):
    dests_uuids = [dst['uuid'] for dst in dsts]
    src_uuid = src[0]['uuid']
    vpn_id = src_uuid + "-" + "-".join(dests_uuids)
    LOGGER.info(f"VPN ID: {vpn_id}")

    resource_key = resource_value[0]
    is_initial = "T1.1" in resource_key

    try:
        components = val['rule_set']['transceiver']['components']
        for i, device in enumerate(components):
            name = sites_input[i]['uuid']
            LOGGER.info(f"NODE TO CONFIGURE: \n{name}: {json.dumps(device, indent=2)}")
            # try:
            #     response = patch_optical_channel_frequency(device, name)
            #     LOGGER.debug(f"RESPONSE :\n {response}")
            # except Exception as e:
            #     LOGGER.error(f"Error configuring node: {e}")
            #     return
            LOGGER.info(f"NODE DATA: \n {json.dumps(device, indent=2)}")
            is_hub = (name == src_uuid)
            
            if is_hub:
                role = "HUB"
                dev_uuid = "R1"
                comp_path = "component=1"
                channel_target = "channel-5"
                template_file = "hub.json"
            else:
                role = "LEAF"
                dev_uuid = "R2"
                comp_path = "component=1"
                channel_target = "channel-5, channel-7"
                template_file = "leaf.json"
            
            config_key = "initial" if is_initial else "second"
            template_path = os.path.join(os.path.dirname(__file__), template_file)
            with open(template_path, "r", encoding="utf-8") as f:
                payload = json.load(f)[config_key]
            
            LOGGER.info(f"NODE TO CONFIGURE:\nDevice role: {role}\nDevice UUID: {dev_uuid}\nComponent path: {comp_path}\nPayload channel target: {channel_target}\nPayload:\n{json.dumps(payload, indent=2)}")
            try:
                if is_hub: 
                    url =f"http://192.168.202.254:80/restconf/data/device=R1/openconfig-platform:components/component=1/optical-channel/config"
                else: 
                    url = f"http://192.168.202.254:80/restconf/data/device=R2/openconfig-platform:components/component=1/optical-channel/config"

                send_request("DELETE", url, headers=HEADERS, auth=AUTH)
                time.sleep(2)
                LOGGER.info(f"Device {dev_uuid} deleted successfully")
                LOGGER.info(f"Device {dev_uuid} payload: \n{json.dumps(payload, indent=2)}")
                # send_request("POST", url, headers=HEADERS, auth=AUTH, data=payload)
                time.sleep(2)
                LOGGER.info(f"Device {dev_uuid} posted successfully")

            except Exception as e:
                LOGGER.error(f"Error patching device {dev_uuid}: {e}")
                return False
                
    except Exception as e:
        LOGGER.error(f"Error parsing components: {e}")
        return

    components = resource_value[1]['rule_set']['transceiver']['components']
    for i, device in enumerate(components):
        name = sites_input[i]['uuid']

        if name == "T2.1":device["frequency"]= 195000000
        if name == "T1.1":device["frequency"]= 195006250
        if name == "T1.2":device["frequency"]= 195018750
        if name == "T1.3":device["frequency"]= 195031250

        LOGGER.debug(f"NODE TO CONFIGURE: \n{name}: {json.dumps(device, indent=2)}")
    templates = []
    for dst in dsts:
        LOGGER.info(f"dst: {dst}")
        vpn = "L3VPN_"+src[0]['uuid']+"_"+dst['uuid']
        LOGGER.info(f"VPN ID: {vpn}")
        templates.append(generate_l3vpn_template_pair(vpn_id=vpn_id, svc_mtu="1500", svc_input_bandwidth=1000000000, svc_output_bandwidth=1000000000, latency_boundary=10, guaranteed_bw_percent=100))
        templates.append(generate_l3vpn_template_pair(vpn_id=vpn, svc_mtu="1500", svc_input_bandwidth=1000000000, svc_output_bandwidth=1000000000, latency_boundary=10, guaranteed_bw_percent=100))
    LOGGER.info(f"Generated L3VPN P2MP service JSONs:\n{json.dumps(templates, indent=2)}")
    try: 
        for template in templates:
            url = f"http://192.168.202.254:80/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services"
            vpn_id_val = template['ietf-l3vpn-svc:l3vpn-svc']['vpn-services']['vpn-service'][0]['vpn-id']
            
            # Delete first if it already exists
            send_request("DELETE", url = url + f"/vpn-service={vpn_id_val}", headers=HEADERS, auth=AUTH)
            LOGGER.info(f"L3VPN service {vpn_id_val} deleted (if existed) successfully")
            time.sleep(1)
            # Post the template to create it
            LOGGER.info(f"L3VPN service {vpn_id_val} data: \n{json.dumps(template, indent=2)}")
            send_request("POST", url, headers=HEADERS, auth=AUTH, data=template)
            # LOGGER.info(f"L3VPN service {vpn_id_val} created successfully")
    except Exception as e:
        LOGGER.error(f"Error creating L3VPN P2MP service: {e}")
        return False
    return True


    # url = "http://192.168.202.254:80/restconf/data/ietf-l3vpn-svc:l3vpn-svc/vpn-services"
    # headers = {
    #     'accept': 'application/json',
    #     'Content-Type': 'application/json'
    # }

    # for template in templates:
    #     LOGGER.info("Generated L3VPN P2MP service JSON:\n%s", json.dumps(template, indent=2))
def send_request(method, url, headers=None, auth=("admin", "admin"), data=None):
    print(f"\n[+] {method} -> {url}")
    if data:
        print("Payload:")
        print(json.dumps(data, indent=2))
    
    #     response = requests.post(url = url, headers= headers, json=template)
    #     LOGGER.debug(response)
    try:
        if method == "POST":
            response = requests.post(url, headers=headers, auth=auth, json=data, timeout=10)
        elif method == "PUT":
            response = requests.put(url, headers=headers, auth=auth, json=data, timeout=10)
        elif method == "GET":
            response = requests.get(url, headers=headers, auth=auth, timeout=10)
        elif method == "DELETE":
            response = requests.delete(url, headers=headers, auth=auth, timeout=10)
        else:
            print(f"[-] Unsupported method: {method}")
            return None

        print(f"Response Status Code: {response.status_code}")
        try:
            resp_json = response.json()
            print("Response Payload:")
            print(json.dumps(resp_json, indent=2))
        except ValueError:
            print(f"Response Text (raw): {response.text}")
        return response
    except Exception as e:
        print(f"[-] Error during request: {e}")
        return None

def patch_optical_channel_frequency(data, DEVICE_ID):
    encoded_path = f"http://192.168.202.254:80/restconf/data/device={DEVICE_ID}/openconfig-platform:components/component=channel-1/optical-channel/config"

    patch_data = data
    response = requests.patch(f"{encoded_path}",
                            json=patch_data,
                            headers=HEADERS)
    assert response.status_code == 200
    return response
+1 −1
Original line number Diff line number Diff line
@@ -14,4 +14,4 @@
# limitations under the License.

export FLASK_ENV=development
gunicorn -w 4 --worker-class eventlet -b 0.0.0.0:18080 --log-level DEBUG nbi.service.app:app
gunicorn -w 4 --worker-class eventlet --timeout 120 -b 0.0.0.0:18080 --log-level DEBUG nbi.service.app:app
Loading