Commit 39b800e1 authored by Pablo Armingol's avatar Pablo Armingol
Browse files

integration of IP-link service in OCdriver

parent 8f5b56cc
Loading
Loading
Loading
Loading
+64 −10
Original line number Diff line number Diff line
@@ -12,9 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import time
import json
import logging, pytz, queue, re, threading
#import lxml.etree as ET
import logging, pytz, re, threading
from typing import Any, List, Tuple, Union
from apscheduler.executors.pool import ThreadPoolExecutor
from apscheduler.jobstores.memory import MemoryJobStore
@@ -22,12 +22,11 @@ from apscheduler.schedulers.background import BackgroundScheduler
from ncclient.manager import Manager, connect_ssh
from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method
from common.tools.client.RetryDecorator import delay_exponential
from common.type_checkers.Checkers import  chk_type
from common.type_checkers.Checkers import chk_length, chk_string, chk_type
from device.service.driver_api.Exceptions import UnsupportedResourceKeyException
from device.service.driver_api._Driver import _Driver
from device.service.driver_api.AnyTreeTools import TreeNode
from .templates import compose_config, cli_compose_config, ufi_interface, cisco_interface
from .templates.VPN.common import seperate_port_config
#from .Tools import xml_pretty_print, xml_to_dict, xml_to_file
from .templates.VPN.roadms import (
    create_optical_band, disable_media_channel, delete_optical_band, create_media_channel_v2
)
@@ -36,11 +35,10 @@ from .RetryDecorator import retry
from context.client.ContextClient import ContextClient
from common.proto.context_pb2 import OpticalConfig
from .templates.discovery_tool.transponders import  transponder_values_extractor
from .templates.discovery_tool.roadms import roadm_values_extractor, extract_media_channels
from .templates.discovery_tool.roadms import roadm_values_extractor
from .templates.discovery_tool.open_roadm import openroadm_values_extractor
from .templates.VPN.openroadm import network_media_channel_handler


DEBUG_MODE = False
logging.getLogger('ncclient.manager').setLevel(logging.DEBUG if DEBUG_MODE else logging.WARNING)
logging.getLogger('ncclient.transport.ssh').setLevel(logging.DEBUG if DEBUG_MODE else logging.WARNING)
@@ -51,9 +49,6 @@ logging.getLogger('monitoring-client').setLevel(logging.INFO if DEBUG_MODE else
RE_GET_ENDPOINT_FROM_INTERFACE_KEY = re.compile(r'.*interface\[([^\]]+)\].*')
RE_GET_ENDPOINT_FROM_INTERFACE_XPATH = re.compile(r".*interface\[oci\:name\='([^\]]+)'\].*")

# Collection of samples through NetConf is very slow and each request collects all the data.
# Populate a cache periodically (when first interface is interrogated).
# Evict data after some seconds, when data is considered as outdated

SAMPLE_EVICTION_SECONDS = 30.0 # seconds
SAMPLE_RESOURCE_KEY = 'interfaces/interface/state/counters'
@@ -201,6 +196,65 @@ def edit_config(
        #results[i] = True
            results.append(result)
    
    if netconf_handler.vendor == "CISCO":
        if "L2VSI" in resources[0][1]:
            #Configure by CLI
            logger.warning("CLI Configuration")
            cli_compose_config(resources, delete=delete, host= netconf_handler._NetconfSessionHandler__address, user=netconf_handler._NetconfSessionHandler__username, passw=netconf_handler._NetconfSessionHandler__password)        
            for i,resource in enumerate(resources):
                results.append(True)
        else: 
            logger.warning("CLI Configuration CISCO INTERFACE")
            cisco_interface(resources, delete=delete, host= netconf_handler._NetconfSessionHandler__address, user=netconf_handler._NetconfSessionHandler__username, passw=netconf_handler._NetconfSessionHandler__password)        
            for i,resource in enumerate(resources):
                results.append(True)
    elif netconf_handler.vendor == "UFISPACE": 
        #Configure by CLI
        logger.warning("CLI Configuration: {:s}".format(resources))
        ufi_interface(resources, delete=delete, host= netconf_handler._NetconfSessionHandler__address, user=netconf_handler._NetconfSessionHandler__username, passw=netconf_handler._NetconfSessionHandler__password)        
        for i,resource in enumerate(resources):
            results.append(True)
    else:
        for i,resource in enumerate(resources):
            str_resource_name = 'resources[#{:d}]'.format(i)
            try:
                logger.debug('[{:s}] resource = {:s}'.format(str_method, str(resource)))
                chk_type(str_resource_name, resource, (list, tuple))
                chk_length(str_resource_name, resource, min_length=2, max_length=2)
                resource_key,resource_value = resource
                chk_string(str_resource_name + '.key', resource_key, allow_empty=False)
                str_config_messages = compose_config(                                                                          # get template for configuration
                    resource_key, resource_value, delete=delete, vendor=netconf_handler.vendor, message_renderer=netconf_handler.message_renderer)
                for str_config_message in str_config_messages:                                                                 # configuration of the received templates 
                    if str_config_message is None: raise UnsupportedResourceKeyException(resource_key)
                    logger.debug('[{:s}] str_config_message[{:d}] = {:s}'.format(
                    str_method, len(str_config_message), str(str_config_message)))
                    netconf_handler.edit_config(                                                                               # configure the device
                        config=str_config_message, target=target, default_operation=default_operation,
                        test_option=test_option, error_option=error_option, format=format)
                    if commit_per_rule:
                        netconf_handler.commit()                                                                               # configuration commit
                    if 'table_connections' in resource_key:
                        time.sleep(5) # CPU usage might exceed critical level after route redistribution, BGP daemon needs time to reload
                
                #results[i] = True
                results.append(True)
            except Exception as e: # pylint: disable=broad-except
                str_operation = 'preparing' if target == 'candidate' else ('deleting' if delete else 'setting')
                msg = '[{:s}] Exception {:s} {:s}: {:s}'
                logger.exception(msg.format(str_method, str_operation, str_resource_name, str(resource)))
                #results[i] = e # if validation fails, store the exception
                results.append(e)

        if not commit_per_rule:
            try:
                netconf_handler.commit()
            except Exception as e: # pylint: disable=broad-except
                msg = '[{:s}] Exception committing: {:s}'
                str_operation = 'preparing' if target == 'candidate' else ('deleting' if delete else 'setting')
                logger.exception(msg.format(str_method, str_operation, str(resources)))
                results = [e for _ in resources] # if commit fails, set exception in each resource

    return results         

class OCDriver(_Driver):
+59 −0
Original line number Diff line number Diff line
# Copyright 2022-2024 ETSI OSG/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 yattag import Doc, indent

def ip_link_mgmt(data,vendor, delete):
    doc, tag, text = Doc().tagtext()

    ID    = data['endpoint_id']['endpoint_uuid']['uuid']
    DATA  = data["rule_set"]
    
    with tag('interfaces', xmlns="http://openconfig.net/yang/interfaces"):
        if delete == True: 
            with tag('interface' ,'xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="delete"'):
                with tag('name'):text(ID)
        else:
            with tag('interface'):
                with tag('name'):text(ID)
                with tag('config'):
                    with tag('name'):text(ID)
                    with tag('type', 'xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type"'):text('ianaift:l3ipvlan')
                    with tag('enabled'):text('true')    
                with tag('subinterfaces'):
                    with tag('subinterface'):
                        if vendor is None or vendor == 'ADVA':
                            with tag('index'): text('0')
                        with tag('config'):
                            with tag('index'): text('0')
                            if vendor == 'ADVA' and not 'vlan'in data: 
                                with tag('untagged-allowed', 'xmlns="http://www.advaoptical.com/cim/adva-dnos-oc-interfaces"'):text('true')
                        with tag('vlan',  xmlns="http://openconfig.net/yang/vlan"):
                            with tag('match'):
                                with tag('single-tagged'):
                                    with tag('config'):
                                        with tag('vlan-id'):text(DATA['vlan'])
                        with tag('ipv4',  xmlns="http://openconfig.net/yang/interfaces/ip"):
                            with tag('addresses'):
                                with tag('address'):
                                    with tag('ip'):text(DATA['ip'])
                                    with tag('config'):
                                        with tag('ip'):text(DATA['ip'])
                                        with tag('prefix-length'):text(DATA['mask'])
    result = indent(
        doc.getvalue(),
        indentation = ' '*2,
        newline = '\r\n'
    )
    return result
 No newline at end of file
+6 −10
Original line number Diff line number Diff line
@@ -13,12 +13,10 @@
# limitations under the License.

import re,logging
import json
import lxml.etree as ET
from typing import Collection, Dict, Any
from typing import Collection, Dict
from .IP_LINK.IP_LINK_multivendor import ip_link_mgmt

from yattag import Doc, indent
from .VPN.physical import create_optical_channel

def add_value_from_tag(target : Dict, field_name: str, field_value : ET.Element, cast=None) -> None:
    if isinstance(field_value,str) or field_value is None or field_value.text is None: return
@@ -49,14 +47,12 @@ def add_value_from_collection(target : Dict, field_name: str, field_value : Coll
# Return:
  [dict] Set of templates generated according to the configuration rule
"""
def generate_templates(resource_key: str, resource_value: str, channel:str) -> str:    # template management to be configured
def generate_templates(resource_key: str, resource_value: str, delete: bool,vendor:str) -> str:    # template management to be configured

    result_templates = []
    data={}
    data['name']=channel
    data['resource_key']=resource_key
    data['value']=resource_value
    #result_templates.append(create_physical_config(data))
    list_resource_key = resource_key.split("/")                                         # the rule resource key management
    if "ip_link" in list_resource_key[1]:                                      # network instance rules management
        result_templates.append(ip_link_mgmt(resource_value,vendor,delete))

    return result_templates

+285 −0
Original line number Diff line number Diff line
@@ -12,3 +12,288 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import json, logging, lxml.etree as ET, re
import time
from typing import Any, Dict, Optional
from jinja2 import Environment, PackageLoader, select_autoescape
import paramiko
from .Tools import generate_templates

LOGGER = logging.getLogger(__name__)



LOGGER = logging.getLogger(__name__)
RE_REMOVE_FILTERS = re.compile(r'\[[^\]]+\]')
RE_REMOVE_FILTERS_2 = re.compile(r'\/[a-z]+:')
EMPTY_CONFIG = '<config></config>'
EMPTY_FILTER = '<filter></filter>'
JINJA_ENV = Environment(loader=PackageLoader('device.service.drivers.openconfig'), autoescape=select_autoescape())

"""
# Method Name: compose_config
  
# Parameters:
  - resource_key:    [str]  Variable to identify the rule to be executed.
  - resource_value:  [str]  Variable with the configuration parameters of the rule to be executed.
  - delete:          [bool] Variable to identify whether to create or delete the rule.
  - vendor:          [str]  Variable to identify the vendor of the equipment to be configured.
  - message_renderer [str]  Variable to dientify template generation method. Can be "jinja" or "pyangbind".
  
# Functionality:
  This method calls the function obtains the equipment configuration template according to the value of the variable "message_renderer".
  Depending on the value of this variable, it gets the template with "jinja" or "pyangbind". 
  
# Return:
  [dict] Set of templates obtained according to the configuration method
"""

def compose_config( # template generation
    resource_key : str, resource_value : str, delete : bool = False, vendor : Optional[str] = None, message_renderer = str
) -> str:

    if (message_renderer == "pyangbind"):
        templates = (generate_templates(resource_key, resource_value, delete, vendor))
        return [
            '<config>{:s}</config>'.format(template) # format correction
            for template in templates
            ]

    elif (message_renderer == "jinja"):
        templates = []
        template_name = '{:s}/edit_config.xml'.format(RE_REMOVE_FILTERS.sub('', resource_key))
        templates.append(JINJA_ENV.get_template(template_name))
        data : Dict[str, Any] = json.loads(resource_value)

        operation = 'delete' if delete else 'merge' # others
        #operation = 'delete' if delete else '' # ipinfusion?

        return [
            '<config>{:s}</config>'.format(
            template.render(**data, operation=operation, vendor=vendor).strip())
            for template in templates
            ]
        
    else:
        raise ValueError('Invalid message_renderer value: {}'.format(message_renderer)) 

"""
# Method Name: cli_compose_config
  
# Parameters:
  - resource_key:    [str]  Variable to identify the rule to be executed.
  - resource_value:  [str]  Variable with the configuration parameters of the rule to be executed.
  - delete:          [bool] Variable to identify whether to create or delete the rule.
  - vendor:          [str]  Variable to identify the vendor of the equipment to be configured.
  - message_renderer [str]  Variable to dientify template generation method. Can be "jinja" or "pyangbind".
  
# Functionality:
  This method calls the function obtains the equipment configuration template according to the value of the variable "message_renderer".
  Depending on the value of this variable, it gets the template with "jinja" or "pyangbind". 
  
# Return:
  [dict] Set of templates obtained according to the configuration method
"""

def cli_compose_config(resources, delete: bool, host: str, user: str, passw: str):     #Method used for configuring via CLI directly L2VPN in CISCO devices
      
    key_value_data = {}

    for path, json_str in resources:
        key_value_data[path] = json_str

    # Iterate through the resources and extract parameter values dynamically
    for path, json_str in resources:
        data = json.loads(json_str)
        if 'VC_ID' in data:            vc_id = data['VC_ID']
        if 'connection_point' in data: connection_point = data['connection_point']
        if 'remote_system' in data:    remote_system = data['remote_system']
        if 'interface' in data:
            interface = data['interface']
            interface = interface.split("-")                       #New Line To Avoid Bad Endpoint Name In CISCO
            interface = interface[1]
        if 'vlan_id' in data:          vlan_id = data['vlan_id']
        if 'name' in data:             ni_name = data['name']
        if 'type' in data:             ni_type = data['type']
        if 'index' in data:            subif_index = data['index']
        if 'description' in data:      description = data['description']
        else:                          description = " "
      
    # initialize the SSH client
    ssh_client = paramiko.SSHClient()
    ssh_client.load_system_host_keys()
    # add to known hosts
    ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        
    try:
        ssh_client.connect(hostname=host, username=user, password=passw, look_for_keys=False)
        #print("Connection successful")
        LOGGER.warning("Connection successful")
    except:
        #print("[!] Cannot connect to the SSH Server")
        LOGGER.warning("[!] Cannot connect to the SSH Server")
        exit()
        
    try:
        # Open an SSH shell
        channel = ssh_client.invoke_shell()
        channel.send('enable\n')
        time.sleep(1)
        channel.send('conf term\n')
        time.sleep(0.1)
        channel.send(f"interface {interface} l2transport\n")
        time.sleep(0.1)
        channel.send('description l2vpn_vpws_example\n')
        time.sleep(0.1)
        channel.send(f"encapsulation dot1q {vlan_id}\n")
        time.sleep(0.1)
        channel.send('mtu 9088\n')
        time.sleep(0.1)
        channel.send('commit\n')
        time.sleep(0.1)
        
        channel.send('l2vpn\n')
        time.sleep(0.1)
        channel.send('load-balancing flow src-dst-ip\n')
        time.sleep(0.1)
        channel.send('pw-class l2vpn_vpws_profile_example\n')
        time.sleep(0.1)
        channel.send('encapsulation mpls\n')
        time.sleep(0.1)
        channel.send('transport-mode vlan passthrough\n')
        time.sleep(0.1)
        channel.send('control-word\n')
        time.sleep(0.1)
        channel.send('exit\n')
        time.sleep(0.1)
        channel.send('l2vpn\n')
        time.sleep(0.1)
        channel.send('xconnect group l2vpn_vpws_group_example\n')
        time.sleep(0.1)
        channel.send(f"p2p {ni_name}\n")
        time.sleep(0.1)
        channel.send(f"interface {interface}\n")                                #Ignore the VlanID because the interface already includes the vlanid tag
        time.sleep(0.1)
        channel.send(f"neighbor ipv4 {remote_system} pw-id {vc_id}\n")
        time.sleep(0.1)
        channel.send('pw-class l2vpn_vpws_profile_example\n')
        time.sleep(0.1)
        channel.send('exit\n')
        time.sleep(0.1)
        channel.send(f"description {description}\n")
        time.sleep(0.1)
        channel.send('commit\n')
        time.sleep(0.1) 
        # Capturar la salida del comando
        output = channel.recv(65535).decode('utf-8')
        #print(output)
        LOGGER.warning(output)
        # Close the SSH shell
        channel.close()

    except Exception as e:
        LOGGER.exception(f"Error with the CLI configuration: {e}")

    # Close the SSH client
    ssh_client.close()
    
def ufi_interface(resources, delete: bool, host: str, user: str, passw: str):     #Method used for configuring via CLI directly L2VPN in CISCO devices

    key_value_data = {}

    for path, json_str in resources:
        key_value_data[path] = json_str

    # initialize the SSH client
    ssh_client = paramiko.SSHClient()
    ssh_client.load_system_host_keys()
    # add to known hosts
    ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    try:
        ssh_client.connect(hostname=host, username=user, password=passw, look_for_keys=False)
        LOGGER.warning("Connection successful")
    except:
        LOGGER.warning("[!] Cannot connect to the SSH Server")
        exit()
    interface = 'ge100-0/0/3/1'  
    ip = '1.1.1.1'
    mask = '24'
    vlan = '1212'
    try:
        # Open an SSH shell
        channel = ssh_client.invoke_shell()
        time.sleep(5)
        channel.send('config\n')
        time.sleep(1)
        channel.send(f'interfaces {interface} \n')
        time.sleep(1)
        channel.send('admin-state enabled \n')
        time.sleep(1)
        channel.send(f'ipv4-address {ip}/{mask} \n')
        time.sleep(1)
        channel.send(f'vlan-id {vlan} \n')
        time.sleep(1)
        channel.send('commit\n')
        time.sleep(1)

        output = channel.recv(65535).decode('utf-8')
        LOGGER.warning(output)
        # Close the SSH shell
        channel.close()

    except Exception as e:
        LOGGER.exception(f"Error with the CLI configuration: {e}")

    # Close the SSH client
    ssh_client.close()

def cisco_interface(resources, delete: bool, host: str, user: str, passw: str):     #Method used for configuring via CLI directly L2VPN in CISCO devices

    key_value_data = {}
    for path, json_str in resources:
        key_value_data[path] = json_str

    # initialize the SSH client
    ssh_client = paramiko.SSHClient()
    ssh_client.load_system_host_keys()
    # add to known hosts
    ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    try:
        ssh_client.connect(hostname=host, username=user, password=passw, look_for_keys=False)
        LOGGER.warning("Connection successful")
    except:
        LOGGER.warning("[!] Cannot connect to the SSH Server")
        exit()
    interface = 'FourHundredGigE0/0/0/10.1212'  
    ip = '1.1.1.1'
    mask = '24'
    vlan = '1212'
    try:
        # Open an SSH shell
        channel = ssh_client.invoke_shell()
        time.sleep(1)
        channel.send('config\n')
        time.sleep(0.1)
        channel.send(f'interface {interface} \n')
        time.sleep(0.1)
        channel.send('no shutdown\n')
        time.sleep(0.1)
        channel.send(f'ipv4 address {ip}/{mask} \n')
        time.sleep(0.1)
        channel.send(f'encapsulation dot1q {vlan} \n')
        time.sleep(0.1)
        channel.send('commit\n')
        time.sleep(0.1)

        output = channel.recv(65535).decode('utf-8')
        LOGGER.warning(output)
        # Close the SSH shell
        channel.close()

    except Exception as e:
        LOGGER.exception(f"Error with the CLI configuration: {e}")

    # Close the SSH client
    ssh_client.close()  
+6 −0
Original line number Diff line number Diff line
@@ -115,6 +115,12 @@ SERVICE_HANDLERS = [
            FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_OPENCONFIG,
        }
    ]),
    (IP_LinkServiceHandler, [
        {
            FilterFieldEnum.SERVICE_TYPE  : ServiceTypeEnum.SERVICETYPE_IPLINK,
            FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_OC,
        }
    ]),
    (QKDServiceHandler, [
        {
            FilterFieldEnum.SERVICE_TYPE  : ServiceTypeEnum.SERVICETYPE_QKD,
Loading