This pySROS-based script solves this problem by sending the MPLS tunnel ID to the tunnel name lookup and including the tunnel name in the output.
Element | Version |
---|---|
CentOS Workstation/Server | CentOS Linux release 7.9.2009 (Core) |
SR OS device running in Model-Driven mode | 22.5.R2 |
Python (on the CentOS machine) | Python 3.6.8 |
In the following sections, each functional component of the script is briefly discussed. At the end, the script is presented in its entirety to promote reusability.
Every pySROS script must set up a connection to a device before it can perform any interactions with a device. The generally accepted way of implementing this behavior is to use a get_connection function.
def get_connection(host=None, username=None, password=None, port=830):
"""Function definition to obtain a Connection object to a specific
SR OS device and access the model-driven information.
:parameter host: The hostname or IP address of the SR OS node.
:type host: str
:paramater credentials: The username and password to connect
to the SR OS node.
:type credentials: dict
:parameter port: The TCP port for the connection to the SR OS node.
:type port: int
:returns: Connection object for the SR OS node.
:rtype: :py:class:`pysros.management.Connection`
"""
try:
connection_object = connect(
host=host,
username=username,
password=password,
port=port,
)
except RuntimeError as error1:
print("Failed to connect. Error:", error1)
sys.exit(-1)
return connection_object
The table.print function used in this function is shipped with the pySROS library and allows printing an SR OS-style table according to specified column names and widths.
def print_route_table_v4(rows):
"""Function Definition to print the SROS style table."""
# Define the columns that will be used in the table. Each list item
# is a tuple of (column width, heading).
cols = [
(25, "Dest Prefix"),
(15, "Protocol"),
(20, "Age"),
(15, "Preference"),
(80, "Next Hop"),
]
# Initalize the Table object with the heading and columns.
table = Table("Service Route Table", cols)
# Print the output passing the data for the rows as
# an argument to the function.
table.print(rows)
In classic CLI, it is common to use a numerical service ID as the unique reference for a service. In MD-CLI, service names are used as the service identifier. Nonetheless, numerical service IDs still exist. The get_service_name function checks whether the provided service identifier is a number. If it is a number, it is assumed that it is the service ID, which is then used to retrieve the actual service name. The service name is needed throughout the script for data retrieval from the network element.
def get_service_name(input_parameter, vprn_services):
"""Function definition to resolve the service name.
The information collection is based on the service name.
If the user provides the service ID as the input parameter
instead, this function resolves the corresponsing
service name.
:parameter input_parameter: accessing provided input values
:type input_parameter: dict
:parameter vprn_services: Contains all VPRN services of a NE.
:type vprn_services: dict
:returns: The service name of the service.
:rtype: str
"""
if input_parameter[1].isdigit():
service_id = input_parameter[1]
for service in vprn_services.keys():
if int(vprn_services[service]["oper-service-id"].data) == int(
service_id
):
service_name = service
else:
service_name = input_parameter[1]
return service_name
For local routes in an L3VPN, the data set provides an interface index number. This interface index is resolved to the interface name in this function.
def get_local_itf_name(service_routes, route, interface_list):
"""Function definition to resolve local interface name.
For local interfaces in the L3VPN/L3EVPN the respective
interface name is resolved.
:parameter service_routes: Contains all routes of a given L3 service
:type service_routes: dict
:parameter route: A specific route to be checked
:type route: str
:parameter interface_list: Contains all interfaces on a given L3 service
:type interface_list: dict
:returns: The interface name.
:rtype: str
"""
if_index = service_routes[route]["nexthop"][0]["if-index"].data
for interface in interface_list.keys():
if interface_list[interface]["if-index"].data == if_index:
next_hop_interface = interface
next_hop = next_hop_interface + " (local interface)"
return next_hop
This function provides the most notable enhancement compared to the built-in SR OS L3VPN route-table view. If L3VPN routes are being tunneled through MPLS tunnels, the built-in SR OS L3VPN route-table view only provides an internal tunnel ID. A second command is needed to resolve that tunnel ID to a meaningful tunnel name. This get_next_hop_tunnel function performs the bespoke tunnel name resolution.
def get_next_hop_tunnel(service_routes, route, lsp_list, tun_table):
"""Function definition to resolve tunnel names.
For tunneled routes in the L3VPN/L3EVPN the respective
tunnel name is resolved.
:parameter service_routes: Contains all routes of a given L3 service
:type service_routes: dict
:parameter route: A specific route to be checked
:type route: str
:parameter lsp_list: Contains all LSPs of the NE
:type lsp_list: dict
:parameter tun_table: Contains all tunnels of the NE
:type tun_table: dict
:returns: The tunnel name.
:rtype: str
"""
next_hop_path = service_routes[route]["nexthop"][0]["resolving-nexthop"][0]
if "nexthop-tunnel-id" in next_hop_path.keys():
tun_id = next_hop_path["nexthop-tunnel-id"].data
# if "nexthop-tunnel-id" key does not exist,
# tun_id is set to empty string to avoid errors
else:
tun_id = ""
tun_type = next_hop_path["nexthop-tunnel-type"].data
for tunnel in tun_table.keys():
if tun_table[tunnel]["id"].data == tun_id:
next_hop_ip = tunnel[0]
next_hop_tun_proto = tun_table[tunnel]["protocol"].data
else:
next_hop_ip = next_hop_path["nexthop-ip"].data
next_hop_tun_proto = tun_type
for lsp in lsp_list.keys():
if lsp_list[lsp]["ttm-tunnel-id"].data == tun_id:
next_hop_lsp = " | lsp:" + lsp
break
# if an LSP with given Tunnel ID is found we break
# the for loop
# otherwise it would loop through all LSPs and the else
# condition at the end would most likely be met
# handling case in which Tunnel ID does not exist
if tun_id == "":
next_hop_lsp = ""
# handling case in which LSP Name is not resolvable
# in that case TTM Tunnel ID is displayed
else:
next_hop_lsp = " | ttmId:" + str(tun_id)
next_hop = (
next_hop_ip
+ " "
+ "(tunneled:"
+ next_hop_tun_proto
+ next_hop_lsp
+ ")"
)
return next_hop
This function obtains data from the SR OS device.
def get_data():
"""
Dedicated function to retrieve required data.
:returns: A tuple holding the required data.
:rtype: tuple
"""
# Calling the get_connection() function to establish
# the connection to the NE.
connection_object = get_connection()
# List of VPRN services is needed to resolve service Id
# to service name
vprn_services = connection_object.running.get("/state/service/vprn")
service_name = get_service_name(sys.argv, vprn_services)
# All routes for a given service are retrieved
path_service_routes = (
"/state/service/vprn[service-name="
+ service_name
+ "]/route-table/unicast/ipv4/route"
)
service_routes = connection_object.running.get(path_service_routes)
# All interfaces for a given are retrieved
# Needed to resolve the interface name
path_interface_list = (
"/state/service/vprn[service-name=" + service_name + "]/interface"
)
interface_list = connection_object.running.get(path_interface_list)
# Tunnel Table and LSP List are retrieved
# Together they allow tunneled route resolution from TTM Id to tunnel name
tun_table = connection_object.running.get(
"/state/router[router-name=Base]/tunnel-table/ipv4/tunnel"
)
lsp_list = connection_object.running.get(
"/state/router[router-name=Base]/mpls/lsp"
)
# Returning all needed data back to the main() function
return service_routes, interface_list, tun_table, lsp_list
Finally, using the building blocks shown above, the main function is created. As the main part of the script, it controls the entire flow. It loops through all the L3VPN routes and calls the respective resolution functions. At the end, the SR OS-style table is printed.
def main():
"""
Main procedure to get all L3VPN/L3EVPN routes for a given service.
It takes the input service ID or service name and collects required
information from the SROS device to list all L3VPN/L3EVPN routes and
resolve tunneled routes in an SROS style table
"""
(
service_routes,
interface_list,
tun_table,
lsp_list,
) = get_data()
svc_route_table_v4 = []
for route in service_routes.keys():
protocol = service_routes[route]["protocol"].data
# converting Age into human readable format
# consisting of "x day, hh:mm:ss"
age = timedelta(seconds=service_routes[route]["age"].data)
preference = service_routes[route]["preference"].data
# This block does interface name resolution based
# on if-index from route output
if protocol == "local":
next_hop = get_local_itf_name(
service_routes, route, interface_list
)
# The function call below handles tunnel name resolution
else:
next_hop = get_next_hop_tunnel(
service_routes, route, lsp_list, tun_table
)
svc_route_table_v4.append([route, protocol, age, preference, next_hop])
print_route_table_v4(svc_route_table_v4)
#!/usr/bin/env python3
# show_route_table_enhanced.py
# Copyright 2022 Nokia
"""
Enhancement of SROS embedded show router [Id] route-table command.
Tested on: SR OS 22.5.R1
"""
import sys
from datetime import timedelta
from pysros.management import connect
from pysros.pprint import Table
def get_connection(host=None, username=None, password=None, port=830):
"""
Function definition to obtain a Connection object to a specific
SR OS device and access the model-driven information.
This function also checks whether the script is being executed
locally on a pySROS capable SROS device or on a remote machine.
:parameter host: The hostname or IP address of the SR OS node.
:type host: str
:paramater credentials: The username and password to connect
to the SR OS node.
:type credentials: dict
:parameter port: The TCP port for the connection to the SR OS node.
:type port: int
:returns: Connection object for the SR OS node.
:rtype: :py:class:`pysros.management.Connection`
"""
try:
connection_object = connect(
host=host,
username=username,
password=password,
port=port,
)
except RuntimeError as error1:
print("Failed to connect. Error:", error1)
sys.exit(-1)
return connection_object
def print_route_table_v4(rows):
"""Function Definition to print the SROS style table."""
# Define the columns that will be used in the table. Each list item
# is a tuple of (column width, heading).
cols = [
(25, "Dest Prefix"),
(15, "Protocol"),
(20, "Age"),
(15, "Preference"),
(80, "Next Hop"),
]
# Initalize the Table object with the heading and columns.
table = Table("Service Route Table", cols)
# Print the output passing the data for the rows as
# an argument to the function.
table.print(rows)
def get_service_name(input_parameter, vprn_services):
"""Function definition to resolve the service name.
The information collection is based on the service name.
If the user provides the service ID as the input parameter
instead, this function resolves the corresponsing
service name.
:parameter input_parameter: accessing provided input values
:type input_parameter: dict
:parameter vprn_services: Contains all VPRN services of a NE.
:type vprn_services: dict
:returns: The service name of the service.
:rtype: str
"""
if input_parameter[1].isdigit():
service_id = input_parameter[1]
for service in vprn_services.keys():
if int(vprn_services[service]["oper-service-id"].data) == int(
service_id
):
service_name = service
else:
service_name = input_parameter[1]
return service_name
def get_local_itf_name(service_routes, route, interface_list):
"""Function definition to resolve local interface name.
For local interfaces in the L3VPN/L3EVPN the respective
interface name is resolved.
:parameter service_routes: Contains all routes of a given L3 service
:type service_routes: dict
:parameter route: A specific route to be checked
:type route: str
:parameter interface_list: Contains all interfaces on a given L3 service
:type interface_list: dict
:returns: The interface name.
:rtype: str
"""
if_index = service_routes[route]["nexthop"][0]["if-index"].data
for interface in interface_list.keys():
if interface_list[interface]["if-index"].data == if_index:
next_hop_interface = interface
next_hop = next_hop_interface + " (local interface)"
return next_hop
def get_next_hop_tunnel(service_routes, route, lsp_list, tun_table):
"""Function definition to resolve tunnel names.
For tunneled routes in the L3VPN/L3EVPN the respective
tunnel name is resolved.
:parameter service_routes: Contains all routes of a given L3 service
:type service_routes: dict
:parameter route: A specific route to be checked
:type route: str
:parameter lsp_list: Contains all LSPs of the NE
:type lsp_list: dict
:parameter tun_table: Contains all tunnels of the NE
:type tun_table: dict
:returns: The tunnel name.
:rtype: str
"""
next_hop_path = service_routes[route]["nexthop"][0]["resolving-nexthop"][0]
if "nexthop-tunnel-id" in next_hop_path.keys():
tun_id = next_hop_path["nexthop-tunnel-id"].data
# if "nexthop-tunnel-id" key does not exist,
# tun_id is set to empty string to avoid errors
else:
tun_id = ""
tun_type = next_hop_path["nexthop-tunnel-type"].data
for tunnel in tun_table.keys():
if tun_table[tunnel]["id"].data == tun_id:
next_hop_ip = tunnel[0]
next_hop_tun_proto = tun_table[tunnel]["protocol"].data
else:
next_hop_ip = next_hop_path["nexthop-ip"].data
next_hop_tun_proto = tun_type
for lsp in lsp_list.keys():
if lsp_list[lsp]["ttm-tunnel-id"].data == tun_id:
next_hop_lsp = " | lsp:" + lsp
break
# if an LSP with given Tunnel ID is found we break
# the for loop
# otherwise it would loop through all LSPs and the else
# condition at the end would most likely be met
# handling case in which Tunnel ID does not exist
if tun_id == "":
next_hop_lsp = ""
# handling case in which LSP Name is not resolvable
# in that case TTM Tunnel ID is displayed
else:
next_hop_lsp = " | ttmId:" + str(tun_id)
next_hop = (
next_hop_ip
+ " "
+ "(tunneled:"
+ next_hop_tun_proto
+ next_hop_lsp
+ ")"
)
return next_hop
def get_data():
"""
Dedicated function to retrieve required data.
:returns: A tuple holding the required data.
:rtype: tuple
"""
# Calling the get_connection() function to establish
# the connection to the NE.
connection_object = get_connection()
# List of VPRN services is needed to resolve service Id
# to service name
vprn_services = connection_object.running.get("/state/service/vprn")
service_name = get_service_name(sys.argv, vprn_services)
# All routes for a given service are retrieved
path_service_routes = (
"/state/service/vprn[service-name="
+ service_name
+ "]/route-table/unicast/ipv4/route"
)
service_routes = connection_object.running.get(path_service_routes)
# All interfaces for a given are retrieved
# Needed to resolve the interface name
path_interface_list = (
"/state/service/vprn[service-name=" + service_name + "]/interface"
)
interface_list = connection_object.running.get(path_interface_list)
# Tunnel Table and LSP List are retrieved
# Together they allow tunneled route resolution from TTM Id to tunnel name
tun_table = connection_object.running.get(
"/state/router[router-name=Base]/tunnel-table/ipv4/tunnel"
)
lsp_list = connection_object.running.get(
"/state/router[router-name=Base]/mpls/lsp"
)
# Returning all needed data back to the main() function
return service_routes, interface_list, tun_table, lsp_list
def main():
"""
Main procedure to get all L3VPN/L3EVPN routes for a given service.
It takes the input service ID or service name and collects required
information from the SROS device to list all L3VPN/L3EVPN routes and
resolve tunneled routes in an SROS style table
"""
(
service_routes,
interface_list,
tun_table,
lsp_list,
) = get_data()
svc_route_table_v4 = []
for route in service_routes.keys():
protocol = service_routes[route]["protocol"].data
# converting Age into human readable format
# consisting of "x day, hh:mm:ss"
age = timedelta(seconds=service_routes[route]["age"].data)
preference = service_routes[route]["preference"].data
# This block does interface name resolution based
# on if-index from route output
if protocol == "local":
next_hop = get_local_itf_name(
service_routes, route, interface_list
)
# The function call below handles tunnel name resolution
else:
next_hop = get_next_hop_tunnel(
service_routes, route, lsp_list, tun_table
)
svc_route_table_v4.append([route, protocol, age, preference, next_hop])
print_route_table_v4(svc_route_table_v4)
if __name__ == "__main__":
main()
This section shows some brief CLI examples of using the script.
The following CLI output shows the built-in SR OS L3VPN route-table view, including a tunneled route:
A:admin@sros1# show router 320 route-table
===============================================================================
Route Table (Service: 320)
===============================================================================
Dest Prefix[Flags] Type Proto Age Pref
Next Hop[Interface Name] Metric
-------------------------------------------------------------------------------
20.0.0.1/32 Local Local 05d19h04m 0
sros1-vpn320 0
20.0.0.2/32 Remote EVPN-IFL 03d12h49m 170
200.0.0.2 (tunneled:SR-TE:655365) 10
-------------------------------------------------------------------------------
No. of Routes: 2
Flags: n = Number of times nexthop is repeated
B = BGP backup route available
L = LFA nexthop available
S = Sticky ECMP requested
===============================================================================
The following output shows the enhanced pySROS script-based L3VPN route-table view, demonstrating that the script expects either the service-id or the service-name as an input variable. It works in both cases:
######
# Using service-id as input variable
######
[/]
A:admin@sros1# pyexec cf3:/show_RouteTable_enhanced.py 320
===============================================================================
Service Route Table
===============================================================================
Dest Prefix Protocol Age Preference
Next Hop
-------------------------------------------------------------------------------
20.0.0.1/32 local 68 days, 18:54:53 0
sros1-vpn320 (local interface)
20.0.0.2/32 evpn-ifl 0:39:28 170
200.0.0.2 (tunneled:sr-te | lsp:l_sros2-srte_pcc)
===============================================================================
[/]
######
# Using service-name as input variable
######
[/]
A:admin@sros1# pyexec cf3:/show_RouteTable_enhanced.py L3-EVPN-Test
===============================================================================
Service Route Table
===============================================================================
Dest Prefix Protocol Age Preference
Next Hop
-------------------------------------------------------------------------------
20.0.0.1/32 local 68 days, 19:23:52 0
sros1-vpn320 (local interface)
20.0.0.2/32 evpn-ifl 0:07:10 170
200.0.0.2 (tunneled:sr-te | lsp:l_sros2-srte_pcc)
===============================================================================
[/]
This script has been tested for the following types of tunneled L3VPN routes: