diff --git a/src/device/service/drivers/openconfig/OpenConfigDriver.py b/src/device/service/drivers/openconfig/OpenConfigDriver.py index ec49765a125e7965500a28d47a1d4da109b09e5d..b0d856426c02f85ed070c6e6affb322d7dee0101 100644 --- a/src/device/service/drivers/openconfig/OpenConfigDriver.py +++ b/src/device/service/drivers/openconfig/OpenConfigDriver.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import anytree, copy, logging, pytz, queue, re, threading #import lxml.etree as ET from datetime import datetime, timedelta @@ -28,7 +29,7 @@ from device.service.driver_api.Exceptions import UnsupportedResourceKeyException from device.service.driver_api._Driver import _Driver from device.service.driver_api.AnyTreeTools import TreeNode, get_subnode, set_subnode_value #dump_subtree #from .Tools import xml_pretty_print, xml_to_dict, xml_to_file -from .templates import ALL_RESOURCE_KEYS, EMPTY_CONFIG, compose_config, get_filter, parse +from .templates import ALL_RESOURCE_KEYS, EMPTY_CONFIG, compose_config, get_filter, parse, cli_compose_config from .RetryDecorator import retry DEBUG_MODE = False @@ -206,42 +207,48 @@ def edit_config( format='xml' # pylint: disable=redefined-builtin ): str_method = 'DeleteConfig' if delete else 'SetConfig' - logger.debug('[{:s}] resources = {:s}'.format(str_method, str(resources))) - results = [None for _ in resources] - 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 - results[i] = 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 - - 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 + if "L2VSI" in resources[0][1] and netconf_handler.vendor == "CISCO": + #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) + results = [] + 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 + results[i] = 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 + + 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 DRIVER_NAME = 'openconfig' diff --git a/src/device/service/drivers/openconfig/templates/Tools.py b/src/device/service/drivers/openconfig/templates/Tools.py index 387cb628b4441fd0546791e9a6be5886b1ffd029..f5250d65540eb0b99b7b7079b9b9dee647b5123a 100644 --- a/src/device/service/drivers/openconfig/templates/Tools.py +++ b/src/device/service/drivers/openconfig/templates/Tools.py @@ -30,6 +30,25 @@ def add_value_from_collection(target : Dict, field_name: str, field_value : Coll if field_value is None or len(field_value) == 0: return target[field_name] = field_value +""" +# Method Name: generate_templates + +# 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. + +# Functionality: + This method generates the template to configure the equipment using pyangbind. + To generate the template the following steps are performed: + 1) Get the first parameter of the variable "resource_key" to identify the main path of the rule. + 2) Search for the specific configuration path + 3) Call the method with the configuration parameters (resource_data variable). + +# Return: + [dict] Set of templates generated according to the configuration rule +""" def generate_templates(resource_key: str, resource_value: str, delete: bool,vendor:str) -> str: # template management to be configured result_templates = [] diff --git a/src/device/service/drivers/openconfig/templates/VPN/Interfaces_multivendor.py b/src/device/service/drivers/openconfig/templates/VPN/Interfaces_multivendor.py index 6cfe525d77ec28a5714248ef5cf481a60f460d9b..b75fc9000dbdfb18628ef02fe7fc95abe75c6bdb 100644 --- a/src/device/service/drivers/openconfig/templates/VPN/Interfaces_multivendor.py +++ b/src/device/service/drivers/openconfig/templates/VPN/Interfaces_multivendor.py @@ -1,8 +1,22 @@ from .openconfig_interfaces import openconfig_interfaces from pyangbind.lib.serialise import pybindIETFXMLEncoder +""" +# Method Name: set_vlan + +# Parameters: + - vendor: [str] Variable to set the name of the vendor of the device to be configured. Depending on the vendor, the generated template may vary. + - vlan_id: [int] Variable to set the value of the parameter "vlan id". +# Functionality: + This is an auxiliary method that helps in the creation of the Interface template. This method generates the correspondent configuration of the vlan for the interface. + The method first checks if the parameters vendor and vlan_id are defined. If the vendor is ADVA and vlan_id = 0, an special configuration line is created. + Based on the values of the given parameters, the method generates the vlan configuration string. + +# Return: + [str] The method returns the generated vlan configuration string, that can be later used in the generation of the Interface template. +""" def set_vlan(OptionalParams): #[L2/L3] Sets a VLANID and a VENDOR that will be requested for executing the following methods - verify = str(OptionalParams) #Verify transforms the received parameters into a string format for later making verifications and modifications + verify = str(OptionalParams) #Verify transforms the received parameters into a string format for later making verifications and modifications #If the Vendor parameter is defined [OPTIONAL-PARAMETER] if verify.find('vendor')>0: @@ -17,8 +31,21 @@ def set_vlan(OptionalParams): #[L2/L3] Sets a else: vlan = '</subinterface>\n </config>' return vlan -def set_ip(OptionalParams): #[L3] Sets a IPAddress that will be requested for executing the following L3VPN methods - verify = str(OptionalParams) # Verify transforms the received parameters into a string format for later making verifications and modifications +""" +# Method Name: set_ip + +# Parameters: + - address_ip: [str] Variable that sets the value of the ip address. + - address_prefix: [int] Variable that specifies the prefix of the given ip address. +# Functionality: + This is an auxiliary method that helps in the creation of the Interface template. This method generates the correspondent configuration of the ip address for the interface. + The method first checks if the parameter address_ip is defined. If it is defined, then it creates the configuration string that will be used later in the Interface template. + +# Return: + [str] The method returns the generated ip configuration string, that can be later used in the generation of the Interface template. +""" +def set_ip(OptionalParams): #[L3] Sets a IPAddress that will be requested for executing the following L3VPN methods + verify = str(OptionalParams) # Verify transforms the received parameters into a string format for later making verifications and modifications #If the Address_ip parameter is defined [OPTIONAL-PARAMETER] if verify.find('address_ip')>0: @@ -29,7 +56,28 @@ def set_ip(OptionalParams): #[L3] Sets a address ='</subinterface>' return address -def create_If_SubIf(parameters): # [L2/L3] Creates a Interface with a Subinterface as described in /interface[{:s}]/subinterface[{:d}] +""" +# Method Name: create_If_SubIf + +# Parameters: + - Interface_name: [str] Variable to set the name of the Interface that will be configured. [Mandatory parameter in all cases]. + - DEL: [bool] Variable that determines if the template will be for creating (DEL = False) or for deleting (DEL = True) a configuration [Mandatory parameter in all cases]. + - Interface_type: [str] Variable that specifies the type of interface, can take the value "l2vlan" or "l3ipvlan" [Only mandatory if DEL = False]. + - SubInterface_Index: [int] Variable to set the index of the subinterface.[Only mandatory if DEL = False]. + - Description: [str] Variable for adding a description to the Interface [Only mandatory if DEL = False]. + +# Functionality: + This method generates the template of an Interface with subinterface, used both for L2 and L3 VPNs. + This template will be generated for configuring a device, making use of pyangbind. + To generate the template the following steps are performed: + 1) Checks if the DEL variable is true (Template for deleting a existent Interface or) or false (Template for creating a new Interface with Subinterface). + 2) Create the template correspondent in each case, assigning the correspondent parameters with their value. + 3) Make the correspondent replaces for the unssuported configurations by pyangbind. + +# Return: + [str] The newly generated template according to the specified parameters. +""" +def create_If_SubIf(parameters): #[L2/L3] Creates a Interface with a Subinterface as described in /interface[{:s}]/subinterface[{:d}] Interface_name = parameters['name'] DEL = parameters['DEL'] # If the parameters DEL is set to "TRUE" that will mean that is for making a DELETE, ELSE is for creating verify = str(parameters) # Verify transforms the received parameters into a string format for later making verifications and modifications diff --git a/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py b/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py index aaedd4b920dd6ad181c8dbfc7b7c0dcffa76a17a..f4830b85ee8da9dba6f94c0fddb885c5e23c004b 100644 --- a/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py +++ b/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py @@ -1,7 +1,28 @@ from .openconfig_network_instance import openconfig_network_instance from pyangbind.lib.serialise import pybindIETFXMLEncoder -def create_network_instance(parameters,vendor): #[L2/L3] Creates a Network Instance as described in: /network_instance[{:s}] +""" +# Method Name: create_network_instance + +# Parameters: + - NetInstance_name: [str] Variable to set the name of the Network Instance . [Mandatory parameter in all cases]. + - DEL: [bool]Variable that determines if the template will be for creating (DEL = False) or for deleting (DEL = True) a configuration [Mandatory parameter in all cases]. + - NetInstance_type: [str] Variable that sets the type of the Network Instance, it can take the value L2VSI for L2VPN or L3VRF for L3VPN . + - NetInstance_description [int] Variable for adding a description to the Network Instance . + - NetInstance_MTU [str] Variable that sets the value of the MTU for the network instance. [L2VPN] + - NetInstance_Route_disting [str] Variable to set the route distinguisher value . [L3VPN] + +# Functionality: + This method generates the template for creating a Network Instance. This template will be generated for being configured in a device, making use of pyangbind. + To generate the template the following steps are performed: + 1) Checks if the DEL variable is true (Template for deleting a existent Network Instance) or false (Template for creating a new Network Instance). + 2) Create the template correspondent in each case, assigning the correspondent parameters with their value. + 3) Make the correspondent replaces for the unssuported configurations by pyangbind. + +# Return: + [str] The newly generated template according to the specified parameters. +""" +def create_network_instance(parameters,vendor): #[L2/L3] Creates a Network Instance as described in: /network_instance[{:s}] NetInstance_name = parameters['name'] #Retrieves the Name parameter of the NetInstance DEL = parameters['DEL'] #If the parameter DEL is set to "TRUE" that will mean that is for making a DELETE, ELSE is for creating verify = str(parameters) #Verify transforms the received parameters into a string format for later making verifications and modifications @@ -55,7 +76,7 @@ def create_network_instance(parameters,vendor): #[L2/L3] Creat #Configuration for L3VRF elif "L3VRF" in NetInstance_type: - NetInstance_Route_disting = parameters['route_distinguisher'] #Retrieves the Route-Distinguisher parameter [Obligatory for L2VSI] + NetInstance_Route_disting = parameters['route_distinguisher'] #Retrieves the Route-Distinguisher parameter [Obligatory for L3VRF] NetInstance_set.config.route_distinguisher = NetInstance_Route_disting #If the router-id parameter is defined [OPTIONAL-PARAMETER] @@ -79,7 +100,25 @@ def create_network_instance(parameters,vendor): #[L2/L3] Creat return (NetInstance_set) -def associate_If_to_NI(parameters): #[L2/L3] Associates an Interface to a Network Instance as described in: /network_instance[{:s}]/interface[{:s}] +""" +# Method Name: associate_If_to_NI + +# Parameters: + - NetInstance_name: [str] Variable that specifies the name of Network Instance that is going to be used. + - NetInstance_ID: [str] Variable to set the ID of the Interface that is going to be associated to the Network Instance. + - NetInstance_Interface: [str] Variable that specifies the name of the Interface that is going to be associated to the Network Instance. + - NetInstance_SubInterface: [int] Variable that specifies the index of the subinterface that is going to be associated to the Network Instance. + +# Functionality: + This method generates the template for associating an Interface to an existent Network Instance. This template will be generated for being configured in a device, making use of pyangbind. + To generate the template the following steps are performed: + 1) Create the template correspondent in each case, assigning the correspondent parameters with their value. + 2) Make the correspondent replaces for the unssuported configurations by pyangbind. + +# Return: + [str] The newly generated template according to the specified parameters. +""" +def associate_If_to_NI(parameters): #[L2/L3] Associates an Interface to a Network Instance as described in: /network_instance[{:s}]/interface[{:s}] NetInstance_name = parameters['name'] NetInstance_ID = parameters['id'] NetInstance_Interface = parameters['interface'] @@ -103,7 +142,29 @@ def associate_If_to_NI(parameters): #[L2/L3] Associates a NetInstance_set = NetInstance_set.replace('</openconfig-network-instance>','') return (NetInstance_set) -def add_protocol_NI(parameters): #[L3] Adds a Protocol to a Network Instance as described in: /network_instance[{:s}]/protocols +""" +# Method Name: add_protocol_NI [Only for L3-VPN] + +# Parameters: + - NetInstance_name: [str] Variable that specifies the name of Network Instance that is going to be used. + - DEL: [bool]Variable that determines if the template will be for creating (DEL = False) or for deleting (DEL = True) a configuration [Mandatory parameter in all cases]. + - Protocol_name: [str] Variable that sets the type of protocol that is going to be added to the NI. It can be STATIC, DIRECTLY_CONNECTED or BGP. + - Identifier: [str] Variable that sets the identifier of the protocol that will be added to the NI. It can be STATIC, DIRECTLY_CONNECTED or BGP. + - AS: [int] Variable that specifies the AS (Autonomous System) parameter. To be defined only in case the protocol used is BGP + - Router_ID: [int] Variable that specifies the identifier of the router to be configured. To be defined only in case the protocol used is BGP + +# Functionality: + This method generates the template that associates a routing protocol with a Network instance. + This template will be generated for being configured in a device, making use of pyangbind. + To generate the template the following steps are performed: + 1) Checks if the DEL variable is true (Template for deleting a routing policy defined set) or false (Template for creating a routing policy defined set). + 2) Create the template correspondent in each case, assigning the correspondent parameters with their value. + 3) Make the correspondent replaces for the unssuported configurations by pyangbind. + +# Return: + [str] The newly generated template according to the specified parameters. +""" +def add_protocol_NI(parameters): #[L3] Adds a Protocol to a Network Instance as described in: /network_instance[{:s}]/protocols NetInstance_name = parameters['name'] Protocol_name = parameters['protocol_name'] #Protocol can be [STATIC], [DIRECTLY_CONNECTED] or [BGP] Identifier = parameters['identifier'] #Identifier can be [STATIC], [DIRECTLY_CONNECTED] or [BGP] @@ -161,7 +222,27 @@ def add_protocol_NI(parameters): #[L3] Adds a Proto return (NetInstance_set) -def associate_virtual_circuit(parameters): #[L2] Associates a Virtual Circuit as described in: /network_instance[{:s}]/connection_point[VC-1] +""" +# Method Name: associate_virtual_circuit [Only for L2-VPN] + +# Parameters: + - NetInstance_name: [str] Variable that specifies the name of Network Instance that is going to be used. + - ConnectionPoint_ID: [str] Variable that defines the Identifier of the Connection Point of within the Network Instance . + - VirtualCircuit_ID: [int] Variable that sets the Identifier of the Virtual Circuit (VC_ID). + - RemoteSystem: [str] Variable to specify the remote system (device) in which the virtual circuit is created. It should be an IP address. + +# Functionality: + This method will generate the template to associate a virtual circuit, used for L2VPN, with a Network Instance. + This template will be generated for being configured in a device, making use of pyangbind. + To generate the template the following steps are performed: + 1) Checks if the DEL variable is true (Template for deleting a Virtual Circuit from the NI) or false (Template for associating a Virtual Circuit to the NI). + 2) Create the template correspondent in each case, assigning the correspondent parameters with their value. + 3) Make the correspondent replaces for the unssuported configurations by pyangbind. + +# Return: + [str] The newly generated template according to the specified parameters. +""" +def associate_virtual_circuit(parameters): #[L2] Associates a Virtual Circuit as described in: /network_instance[{:s}]/connection_point[VC-1] NetInstance_name = parameters['name'] ConnectionPoint_ID = parameters['connection_point'] VirtualCircuit_ID = parameters['VC_ID'] @@ -188,7 +269,26 @@ def associate_virtual_circuit(parameters): #[L2] Associates a NetInstance_set = NetInstance_set.replace('</openconfig-network-instance>','') return (NetInstance_set) -def associate_RP_to_NI(parameters): #[L3] Associates a Routing Policy to a Network Instance as described in: /network_instance[{:s}]/inter_instance_policies[{:s}] +""" +# Method Name: associate_RP_to_NI [Only for L3-VPN] + +# Parameters: + - NetInstance_name: [str] Variable that specifies the name of Network Instance that is going to be used. + - Import_policy: [str] Variable that specifies the name of the Import Routing Policy to be set. + - Export_policy: [str] Variable that specifies the name of the Export Routing Policy to be set. + +# Functionality: + This method generates the template to associate a Routing Policy (Import or Export) to an existent Network Instance. + This template will be generated for being configured in a device, making use of pyangbind. + To generate the template the following steps are performed: + 1) Checks if the DEL variable is true (Template for deleting a RP from a Network Instance) or false (Template for associating a RP to a Network Instance). + 2) Create the template correspondent in each case, assigning the correspondent parameters with their value. + 3) Make the correspondent replaces for the unssuported configurations by pyangbind. + +# Return: + [str] The newly generated template according to the specified parameters. +""" +def associate_RP_to_NI(parameters): #[L3] Associates a Routing Policy to a Network Instance as described in: /network_instance[{:s}]/inter_instance_policies[{:s}] NetInstance_name = parameters['name'] verify = str(parameters) #Verify transforms the received parameters into a string format for later making verifications and modifications @@ -217,7 +317,29 @@ def associate_RP_to_NI(parameters): #[L3] Associates a NetInstance_set = NetInstance_set.replace('</openconfig-network-instance>','') return (NetInstance_set) -def create_table_conns(parameters): #[L3] Creates Table Connections as described in: /network_instance[{:s}]/table_connections +""" +# Method Name: create_table_conns [Only for L3-VPN] + +# Parameters: + - NetInstance_name: [str] Variable that specifies the name of Network Instance that is going to be used. + - DEL: [bool] Variable that determines if the template will be for creating (DEL = False) or for deleting (DEL = True) a configuration [Mandatory parameter in all cases]. + - SourceProtocol: [str] Variable to specify the protocol used in the Source for the table connection. + - DestProtocol [str] Variable to specify the protocol used in the Destination for the table connection.. + - AddrFamily [str] Variable to specify the Address Family that is going to be used for the table connection. It can take the value 'IPV4'or 'IPV6' + - Def_ImportPolicy [str] Variable to specify a Routing Policy, that will be used as Default for the table connections. + +# Functionality: + This method generates the template for creating (or deleting) a table connection. + This template will be generated for being configured in a device, making use of pyangbind. + To generate the template the following steps are performed: + 1) Checks if the DEL variable is true (Template for deleting a table connection) or false (Template for creating a table connection). + 2) Create the template correspondent in each case, assigning the correspondent parameters with their value. + 3) Make the correspondent replaces for the unssuported configurations by pyangbind. + +# Return: + [str] The newly generated template according to the specified parameters. +""" +def create_table_conns(parameters): #[L3] Creates Table Connections as described in: /network_instance[{:s}]/table_connections NetInstance_name = parameters['name'] SourceProtocol = parameters['src_protocol'] DestProtocol = parameters['dst_protocol'] diff --git a/src/device/service/drivers/openconfig/templates/VPN/Routing_policy.py b/src/device/service/drivers/openconfig/templates/VPN/Routing_policy.py index 895385a65a3f71cd1e3daa42b56d32fbd7ceec0b..541c1b46b2d0734990db81658aceb7b2893acd7e 100644 --- a/src/device/service/drivers/openconfig/templates/VPN/Routing_policy.py +++ b/src/device/service/drivers/openconfig/templates/VPN/Routing_policy.py @@ -1,7 +1,27 @@ from .openconfig_routing_policy import openconfig_routing_policy from pyangbind.lib.serialise import pybindIETFXMLEncoder -def create_rp_statement(parameters): #[L3] Creates a Routing Policy Statement +""" +# Method Name: create_rp_statement + +# Parameters: + - Policy_Name: [str] Variable that determines the name of the Routing Policy [Mandatory parameter in all cases]. + - DEL: [bool] Variable that determines if the template will be for creating (DEL = False) or for deleting (DEL = True) a configuration [Mandatory parameter in all cases]. + - Statement_Name: [str] Variable that determines the name of the Routing Policy Statement, which is a unique statement within the policy [Only mandatory if DEL = False]. + - Policy_Result: [str] Variable to set if the policy is for accepting (ACCEPT ROUTE) or rejecting (REJECT ROUTE). [Only mandatory if DEL = False]. + - ExtCommSetName: [str] Variable to set the name of the extended community set in the context of BGP policy conditions. [Only mandatory if DEL = False]. + +# Functionality: + This method generates the template of a routing policy statement to configure in a device, making use of pyangbind. + To generate the template the following steps are performed: + 1) Checks if the DEL variable is true (Template for deleting a policy statement) or false (Template for creating a policy statement). + 2) Create the template correspondent in each case, assigning the correspondent parameters with their value. + 3) Make the correspondent replaces for the unssuported configurations by pyangbind. + +# Return: + [str] The newly generated template according to the specified parameters. +""" +def create_rp_statement(parameters): #[L3] Creates a Routing Policy Statement Policy_Name = parameters['policy_name'] DEL = parameters['DEL'] #If the parameter DEL is set to "TRUE" that will mean that is for making a DELETE, ELSE is for creating @@ -53,7 +73,26 @@ def create_rp_statement(parameters): #[L3] Creates a Routing return (RoutingInstance_set) -def create_rp_def(parameters): #[L3] Creates a Routing Policy - Defined Sets [ '/routing_policy/bgp_defined_set[{:s}_rt_export][{:s}]' ] +""" +# Method Name: create_rp_def + +# Parameters: + - ExtCommSetName: [str] Variable to set the name of the extended community set in the context of BGP policy conditions. [Mandatory parameter in all cases]. + - DEL: [bool] Variable that determines if the template will be for creating (DEL = False) or for deleting (DEL = True) a configuration [Mandatory parameter in all cases]. + - ExtCommMember: [str] Variable that represents an individual member or value within an Extended Community [Only mandatory if DEL = False]. + +# Functionality: + This method generates the template of a routing policy defined sets, which are objects defined and used within a routing policy statement. + This template will be generated for being configured in a device, making use of pyangbind. + To generate the template the following steps are performed: + 1) Checks if the DEL variable is true (Template for deleting a routing policy defined set) or false (Template for creating a routing policy defined set). + 2) Create the template correspondent in each case, assigning the correspondent parameters with their value. + 3) Make the correspondent replaces for the unssuported configurations by pyangbind. + +# Return: + [str] The newly generated template according to the specified parameters. +""" +def create_rp_def(parameters): #[L3] Creates a Routing Policy - Defined Sets [ '/routing_policy/bgp_defined_set[{:s}_rt_export][{:s}]' ] ExtCommSetName = parameters['ext_community_set_name'] DEL = parameters['DEL'] #If the parameter DEL is set to "TRUE" that will mean that is for making a DELETE, ELSE is for creating @@ -86,4 +125,3 @@ def create_rp_def(parameters): #[L3] Creates a Routing </config> \n\t </ext-community-set> \n\t </ext-community-sets> \n </bgp-defined-sets> \n </defined-sets> \n </routing-policy>') return (RoutingInstance_set) - diff --git a/src/device/service/drivers/openconfig/templates/__init__.py b/src/device/service/drivers/openconfig/templates/__init__.py index efeff2a12461e2c0d3f99ca32f5a34e5794efd11..89784c2a4ad935f3b5241a76e18c29ca0261b894 100644 --- a/src/device/service/drivers/openconfig/templates/__init__.py +++ b/src/device/service/drivers/openconfig/templates/__init__.py @@ -12,9 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ast import List, Tuple 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 from .ACL.ACL_multivendor import acl_mgmt from device.service.driver_api._Driver import ( @@ -72,14 +75,28 @@ def parse(resource_key : str, xml_data : ET.Element): resource_key = RE_REMOVE_FILTERS.sub('', resource_key) resource_key = RE_REMOVE_FILTERS_2.sub('/', resource_key) resource_key = resource_key.replace('//', '') - #resource_key_parts = resource_key.split('/') - #if len(resource_key_parts) > 1: resource_key_parts = resource_key_parts[:-1] - #resource_key = '/'.join(resource_key_parts) - #resource_key = RESOURCE_KEY_MAPPINGS.get(resource_key, resource_key) parser = RESOURCE_PARSERS.get(resource_key) if parser is None: return [(resource_key, xml_data)] return parser(xml_data) +""" +# 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: @@ -96,13 +113,137 @@ def compose_config( # template generation template = JINJA_ENV.get_template(template_name) if "acl_ruleset" in resource_key: # MANAGING ACLs - template.extend(acl_mgmt(resource_value,vendor)) # MANAGING ACLs + templates =[] + templates.append(JINJA_ENV.get_template(template_name)) + templates.append(JINJA_ENV.get_template(template_name)) data : Dict[str, Any] = json.loads(resource_value) operation = 'delete' if delete else 'merge' - LOGGER.info('Template={:s}'.format('<config>{:s}</config>'.format(template.render(**data, operation=operation, vendor=vendor).strip()))) - return ['<config>{:s}</config>'.format(template.render(**data, operation=operation, vendor=vendor).strip())] - + 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() \ No newline at end of file diff --git a/src/service/service/service_handlers/l2nm_openconfig/ConfigRules.py b/src/service/service/service_handlers/l2nm_openconfig/ConfigRules.py index 69354ff759338a03f52587ff2abb5da4ea3ab232..f33a68fc41f06a796559f08e007036636233f868 100644 --- a/src/service/service/service_handlers/l2nm_openconfig/ConfigRules.py +++ b/src/service/service/service_handlers/l2nm_openconfig/ConfigRules.py @@ -42,17 +42,17 @@ def setup_config_rules( #address_ip = json_endpoint_settings.get('address_ip', '0.0.0.0') # '2.2.2.1' #address_prefix = json_endpoint_settings.get('address_prefix', 24 ) # 30 remote_router = json_endpoint_settings.get('remote_router', '5.5.5.5') # '5.5.5.5' - circuit_id = json_endpoint_settings.get('circuit_id', '111' ) # '111' - network_instance_name = json_endpoint_settings.get('ni_name', 'ELAN-AC:{:s}'.format(str(circuit_id))) #ELAN-AC:1 - + network_instance_name = json_endpoint_settings.get('ni_name', 'ELAN-AC:{:s}'.format(str(vlan_id))) #ELAN-AC:1 + virtual_circuit_id = json_endpoint_settings.get('vc_id', '111' ) # '111' + connection_point = json_endpoint_settings.get('conn_point', '1' ) # '111' #network_interface_desc = '{:s}-NetIf'.format(service_uuid) network_interface_desc = json_endpoint_settings.get('ni_description','') #network_subinterface_desc = '{:s}-NetSubIf'.format(service_uuid) network_subinterface_desc = json_endpoint_settings.get('subif_description','') if_cirid_name = '{:s}.{:s}'.format(endpoint_name, vlan_id) - #connection_point_id = 'VC-{:s}'.format(str(circuit_id)) #Provisionalmente comentado, en principio se deberia usar asi - connection_point_id = 'VC-1' #Uso provisional + connection_point_id = 'VC-{:s}'.format(str(connection_point)) #Provisionalmente comentado, en principio se deberia usar asi + #connection_point_id = 'VC-1' #Uso provisional json_config_rules = [ @@ -61,7 +61,7 @@ def setup_config_rules( {'name': network_instance_name, 'type': 'L2VSI'}), json_config_rule_set( - '/interface[{:s}]/subinterface[{:d}]'.format(if_cirid_name, sub_interface_index), + '/interface[{:s}]/subinterface[{:s}]'.format(if_cirid_name, sub_interface_index), {'name': if_cirid_name, 'type': 'l2vlan', 'index': sub_interface_index, 'vlan_id': vlan_id}), json_config_rule_set( @@ -71,7 +71,7 @@ def setup_config_rules( json_config_rule_set( '/network_instance[{:s}]/connection_point[{:s}]'.format(network_instance_name, connection_point_id), - {'name': network_instance_name, 'connection_point': connection_point_id, 'VC_ID': circuit_id, + {'name': network_instance_name, 'connection_point': connection_point_id, 'VC_ID': virtual_circuit_id, 'remote_system': remote_router}), ] for res_key, res_value in endpoint_acls: @@ -125,7 +125,7 @@ def teardown_config_rules( {'name': network_instance_name}), json_config_rule_delete( - '/interface[{:s}]/subinterface[{:d}]'.format(if_cirid_name, sub_interface_index), + '/interface[{:s}]/subinterface[{:s}]'.format(if_cirid_name, sub_interface_index), {'name': if_cirid_name, 'index': sub_interface_index}), ] diff --git a/src/webui/service/service/forms.py b/src/webui/service/service/forms.py index f3c5e3d17abebe23e8df56aa23f1452cdc12d056..ea192566560169d5e5904fe4362cc1efff42cdde 100644 --- a/src/webui/service/service/forms.py +++ b/src/webui/service/service/forms.py @@ -161,14 +161,14 @@ class AddServiceForm_L2VPN(FlaskForm): #L2VPN - Formulary Fields NI_description = StringField('NI Description', validators=[Optional()]) #OPTIONAL PARAMETER #Device_1 specific #Device_1_NI_VC_ID = IntegerField('Device_1 NI VC_ID', validators=[CustomInputRequired(), NumberRange(min=0, message="VC can't be negative"), validator_ADVA]) #MANDATORY PARAMETER - Device_1_NI_VC_ID = IntegerField('Device_1 NI VC_ID', validators=[CustomInputRequired(), NumberRange(min=0, message="VC can't be negative")]) #MANDATORY PARAMETER - Device_1_NI_remote_system = StringField('Device_1 NI remote_system', validators=[CustomInputRequired(),validate_ipv4_address]) #MANDATORY PARAMETER - Device_1_NI_connection_point = StringField('Device_1 NI conn_point', validators=[CustomInputRequired()]) #MANDATORY PARAMETER + Device_1_NI_remote_system = StringField('Device_1 NI Remote System', validators=[CustomInputRequired(),validate_ipv4_address]) #MANDATORY PARAMETER + Device_1_NI_VC_ID = IntegerField('Device_1 NI VC ID', validators=[CustomInputRequired(), NumberRange(min=0, message="VC can't be negative")]) #MANDATORY PARAMETER + Device_1_NI_connection_point = StringField('Device_1 NI Conection Point', validators=[CustomInputRequired()]) #MANDATORY PARAMETER #Device_2 specific #Device_2_NI_VC_ID = IntegerField('Device_2 NI VC_ID', validators=[CustomInputRequired(), NumberRange(min=0, message="VC can't be negative"), validator_ADVA]) #MANDATORY PARAMETER - Device_2_NI_VC_ID = IntegerField('Device_2 NI VC_ID', validators=[CustomInputRequired(), NumberRange(min=0, message="VC can't be negative")]) #MANDATORY PARAMETER - Device_2_NI_remote_system = StringField ('Device_2 NI remote_system', validators=[CustomInputRequired(),validate_ipv4_address]) #MANDATORY PARAMETER - Device_2_NI_connection_point = StringField ('Device_2 NI conn_point', validators=[CustomInputRequired()]) #MANDATORY PARAMETER + Device_2_NI_remote_system = StringField ('Device_2 NI Remote System', validators=[CustomInputRequired(),validate_ipv4_address]) #MANDATORY PARAMETER + Device_2_NI_VC_ID = IntegerField('Device_2 NI VC ID', validators=[CustomInputRequired(), NumberRange(min=0, message="VC can't be negative")]) #MANDATORY PARAMETER + Device_2_NI_connection_point = StringField ('Device_2 NI Conection Point', validators=[CustomInputRequired()]) #MANDATORY PARAMETER #Interface parameters (DEVICE SPECIFIC) #Device-1 diff --git a/src/webui/service/service/routes.py b/src/webui/service/service/routes.py index a24e6169c603dbbcd4b11a21f512cae03b0f1729..85bd11cb420f4a9f951058e700c8ada7efda10d5 100644 --- a/src/webui/service/service/routes.py +++ b/src/webui/service/service/routes.py @@ -55,6 +55,7 @@ type = ["ACL_UNDEFINED", "ACL_IPV4","ACL_IPV6","ACL_L2","ACL_MPLS","ACL_MIXE f_action = ["UNDEFINED", "DROP","ACCEPT","REJECT"] l_action = ["UNDEFINED", "LOG_NONE","LOG_SYSLOG"] +''' @service.get('/') #Route for the homepage of the created "service" blueprint @contextmanager def connected_client(c): @@ -63,7 +64,7 @@ def connected_client(c): yield c finally: c.close() - +''' # Context client must be in connected state when calling this function def get_device_drivers_in_use(topology_uuid: str, context_uuid: str) -> Set[str]: active_drivers = set() @@ -320,8 +321,8 @@ def delete(service_uuid: str): return redirect(url_for('service.home')) #Added routes for creating a new Service -@service.route('add', methods=['GET', 'POST']) #Route for adding a new service [Selecting the type of operation to be performed - First Form] -def add(): +@service.route('add/configure', methods=['GET', 'POST']) #Route for adding a new service [Selecting the type of operation to be performed - First Form] +def add_configure(): form_1 = AddServiceForm_1() if form_1.validate_on_submit(): #store the selected service type in session @@ -611,7 +612,8 @@ def get_device_params(form, device_num, form_type): 'sub_interface_index': str(getattr(form, f'Device_{device_num}_IF_index').data), 'vlan_id': str(getattr(form, f'Device_{device_num}_IF_vlan_id').data), 'remote_router': str(getattr(form, f'Device_{device_num}_NI_remote_system').data), - 'circuit_id': str(getattr(form, f'Device_{device_num}_NI_VC_ID').data), + 'vc_id': str(getattr(form, f'Device_{device_num}_NI_VC_ID').data), + 'conn_point': str(getattr(form, f'Device_{device_num}_NI_connection_point').data), 'mtu': str(getattr(form, f'Device_{device_num}_IF_mtu').data), 'ni_description': str(getattr(form, 'NI_description').data), 'subif_description': str(getattr(form, f'Device_{device_num}_IF_description').data), diff --git a/src/webui/service/templates/service/add.html b/src/webui/service/templates/service/add.html index 4f9aaa699fcef32a22ae2048163dff1c54198b57..2b03ebcbf05a759606861a675b1a9e76e12df47b 100644 --- a/src/webui/service/templates/service/add.html +++ b/src/webui/service/templates/service/add.html @@ -17,7 +17,7 @@ {% block content %} <h1>Add New Service</h1> -<form method="POST" action="{{ url_for('service.add') }}"> +<form method="POST" action="{{ url_for('service.add_configure') }}"> <fieldset> <div class="row mb-3"> diff --git a/src/webui/service/templates/service/configure_L2VPN.html b/src/webui/service/templates/service/configure_L2VPN.html index a0039704bcb09140d5cd150a7c8f5628fe266381..c443a024d3f00d70c29b70e394a67c595b27cc69 100644 --- a/src/webui/service/templates/service/configure_L2VPN.html +++ b/src/webui/service/templates/service/configure_L2VPN.html @@ -222,58 +222,58 @@ </div> </div> <div class="row mb-3"> - {{ form_l2vpn.Device_1_NI_VC_ID.label(class="col-sm-2 col-form-label") }} + {{ form_l2vpn.Device_1_NI_remote_system.label(class="col-sm-2 col-form-label") }} <div class="col-sm-4"> - {% if form_l2vpn.Device_1_NI_VC_ID.errors %} - {{ form_l2vpn.Device_1_NI_VC_ID(class="form-control is-invalid", placeholder="Mandatory") }} + {% if form_l2vpn.Device_1_NI_remote_system.errors %} + {{ form_l2vpn.Device_1_NI_remote_system(class="form-control is-invalid", placeholder="Mandatory") }} <div class="invalid-feedback"> - {% for error in form_l2vpn.Device_1_NI_VC_ID.errors %} + {% for error in form_l2vpn.Device_1_NI_remote_system.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} - {{ form_l2vpn.Device_1_NI_VC_ID(class="form-control", placeholder="Mandatory") }} + {{ form_l2vpn.Device_1_NI_remote_system(class="form-control", placeholder="Mandatory") }} {% endif %} </div> - {{ form_l2vpn.Device_2_NI_VC_ID.label(class="col-sm-2 col-form-label") }} + {{ form_l2vpn.Device_2_NI_remote_system.label(class="col-sm-2 col-form-label") }} <div class="col-sm-4"> - {% if form_l2vpn.Device_2_NI_VC_ID.errors %} - {{ form_l2vpn.Device_2_NI_VC_ID(class="form-control is-invalid", placeholder="Mandatory") }} + {% if form_l2vpn.Device_2_NI_remote_system.errors %} + {{ form_l2vpn.Device_2_NI_remote_system(class="form-control is-invalid", placeholder="Mandatory") }} <div class="invalid-feedback"> - {% for error in form_l2vpn.Device_2_NI_VC_ID.errors %} + {% for error in form_l2vpn.Device_2_NI_remote_system.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} - {{ form_l2vpn.Device_2_NI_VC_ID(class="form-control", placeholder="Mandatory") }} + {{ form_l2vpn.Device_2_NI_remote_system(class="form-control", placeholder="Mandatory") }} {% endif %} </div> </div> <div class="row mb-3"> - {{ form_l2vpn.Device_1_NI_remote_system.label(class="col-sm-2 col-form-label") }} + {{ form_l2vpn.Device_1_NI_VC_ID.label(class="col-sm-2 col-form-label") }} <div class="col-sm-4"> - {% if form_l2vpn.Device_1_NI_remote_system.errors %} - {{ form_l2vpn.Device_1_NI_remote_system(class="form-control is-invalid", placeholder="Mandatory") }} + {% if form_l2vpn.Device_1_NI_VC_ID.errors %} + {{ form_l2vpn.Device_1_NI_VC_ID(class="form-control is-invalid", placeholder="Mandatory") }} <div class="invalid-feedback"> - {% for error in form_l2vpn.Device_1_NI_remote_system.errors %} + {% for error in form_l2vpn.Device_1_NI_VC_ID.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} - {{ form_l2vpn.Device_1_NI_remote_system(class="form-control", placeholder="Mandatory") }} + {{ form_l2vpn.Device_1_NI_VC_ID(class="form-control", placeholder="Mandatory") }} {% endif %} </div> - {{ form_l2vpn.Device_2_NI_remote_system.label(class="col-sm-2 col-form-label") }} + {{ form_l2vpn.Device_2_NI_VC_ID.label(class="col-sm-2 col-form-label") }} <div class="col-sm-4"> - {% if form_l2vpn.Device_2_NI_remote_system.errors %} - {{ form_l2vpn.Device_2_NI_remote_system(class="form-control is-invalid", placeholder="Mandatory") }} + {% if form_l2vpn.Device_2_NI_VC_ID.errors %} + {{ form_l2vpn.Device_2_NI_VC_ID(class="form-control is-invalid", placeholder="Mandatory") }} <div class="invalid-feedback"> - {% for error in form_l2vpn.Device_2_NI_remote_system.errors %} + {% for error in form_l2vpn.Device_2_NI_VC_ID.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} - {{ form_l2vpn.Device_2_NI_remote_system(class="form-control", placeholder="Mandatory") }} + {{ form_l2vpn.Device_2_NI_VC_ID(class="form-control", placeholder="Mandatory") }} {% endif %} </div> </div> diff --git a/src/webui/service/templates/service/home.html b/src/webui/service/templates/service/home.html index 4e4c1f82f33113402bdc3948b1073a579e2922a3..a079dbd28b91a89210072f0c0df56b20d948ddb2 100644 --- a/src/webui/service/templates/service/home.html +++ b/src/webui/service/templates/service/home.html @@ -21,11 +21,11 @@ <div class="row"> <!-- Button for adding a New Service --> <div class="col"> - <a href="{{ url_for('service.add') }}" class="btn btn-primary" style="margin-bottom: 10px;"> + <a href="{{ url_for('service.add_configure') }}" class="btn btn-primary" style="margin-bottom: 10px;"> <i class="bi bi-plus"></i> Add New Service </a> - </div> --> + </div> <!-- Only display XR service addition button if there are XR constellations. Otherwise it might confuse user, as other service types do not have GUI to add service yet. -->