NorthStar offers a number of south bound (SB) interfaces for traditional protocols such as ISIS, OSPFv2, BGP-LS and PCEP.
NorthStar also offers a single north bound (NB) interface for retrieving, subscribing to and modifying data in the form of
a REST interface. We tend to think of the NB interface as a means for external applications to interface with NorthStar, and
the SB interface serves to interface with networking equipment such as routers and switches:
One of the values of NorthStar, as a centralized PCE, is to provide new methods for routing LSPs across a network, which can
be viewed as constraints. Currently, the platform supports several new constraints, including diverse-paths, minimal delay,
and maximally protected.
One limitation of the current PCE architecture is that there is no way of conveying a routing constraint using PCEP. As such,
if a customer has deployed LSPs with a constraint, such as a diversity association with another LSP, using manually configuring
their ingress routers then the moment orthstar takes control, through LSP delegation, the notion of that constraint is lost.
A user can, manually, reprovision LSPs with the constraint using the NorthStar GUI or NB REST API, although this might be
difficult for a large deployment.
This example demonstrates writing a new application for Junos, using the JET framework, that sends routing constraints directly
to NorthStar:
A new Junos daemon, in the form of a JET application, subscribes to MGD configuration change events through the JET Services
Daemon (JSD). If the protocols mpls label-switched-path
hierarchy is modified the daemon determines if a new routing constraint was added, deleted, or modified and makes a REST
call to the NB interface adding the routing constraint to the LSP properties on PCS:
This a application applies to these scenarios:
The following routing constraints are supported:
The Junos CLI for adding a routing constraint to an LSP is:
Main JET Application Python File: northstard
The northstard.py
Python file is the main JET application file that interacts with specific events and JET sessions, instantiating NorthStar
Python library objects. It is typically invoked in the Junos OS config as the representative file for the JET application
and gathers input attributes from the router configuration for object instantiation.
The file has the following structural elements, which are described below.
Argument Parsing and JET Session Handling Main Function
The main()
function handles initial arguments for customization. Some attributes are required, while others are optional:
parser = argparse.ArgumentParser(prog=os.path.basename(__file__), description='Northstar JET application')
# Compulsory attributes identified with required=True
parser.add_argument("--northstar_address", nargs='?', help="Address of the Northstar server.", type=str, required=True)
parser.add_argument("--northstar_username", nargs='?', help="Northstar server username.", type=str, required=True)
parser.add_argument("--northstar_password", nargs='?', help="Northstar server password.", type=str, required=True)
parser.add_argument("--router_username", nargs='?', help="Router username.", type=str, required=True)
parser.add_argument("--router_password", nargs='?', help="Router password.", type=str, required=True)
parser.add_argument("--network_igp", nargs='?', help="IGP used in the network, used for data extraction from TED.", type=str, required=True)
# Optional attributes, inheriting default values otherwise
parser.add_argument("--northstar_grant_type", nargs='?', help="Authentication Grant method for Northstar server. (default: %(default)s)", type=str, default=NORTHSTAR_GRANT_TYPE)
parser.add_argument("--northstar_port", nargs='?', help="Port for Northstar server REST API. (default: %(default)s)", type=str, default=NORTHSTAR_PORT)
parser.add_argument("--northstar_tenant_id", nargs='?', help="Tenant Id used in Northstar server. (default: %(default)s)", type=str, default=NORTHSTAR_TENANT_ID)
parser.add_argument("--output", nargs='?', help="Traceoptions output file. (default: %(default)s)", type=str, default=DEFAULT_OUTPUT)
parser.add_argument("--log_level", nargs='?', help="Traceoptions logging level. (default: %(default)s)", type=str, default=DEFAULT_LOGGING)
After successfully processing arguments, the function opens notification and request-response JET sessions. The notification
session is required for commit notification changes and, therefore, subscribes to commit updates. The request-response session
is required for interactive configuration gathering from the router. To optimize resource consumption, a basic multithreading
scheme is set up to set the notification session to listen mode:
# Main threading Event
calling_thread_event = threading.Event()
global client
client = JetHandler()
evHandle = client.OpenNotificationSession(device="localhost",user=args.router_username,password=args.router_password, port=1883)
client.OpenRequestResponseSession(device="localhost",user=args.router_username,password=args.router_password,port=9090)
cutopic = evHandle.CreateConfigUpdateTopic()
logger.debug("Subscription for commit notifications")
evHandle.Subscribe(cutopic,handleCommitNotifications)
logger.debug("Thread waiting for the events")
calling_thread_event.wait()
# This part will never get executed unless an event is triggered by using calling_thread_event.set()
calling_thread_event.clear()
logger.debug("Thread finished")
# Unsubscribe events
evHandle.Unsubscribe()
logger.debug("Unsubscription for commit notifications")
client.CloseRequestResponseSession()
client.CloseNotificationSession()
Because the notification session subscribes to configuration updates, a subsequent commit notification handling session is
invoked on these events.
Commit Notification Handling Session
On every configuration update, this function is called to evaluate the changed hierarchy. If the hierarchy [edit protocols mpls]
was notified, the request-response session is triggered to gather the specific LSP stanzas. Otherwise, the change is ignored.
def handleCommitNotifications(message):
logger.debug('Handling commit notifications for change: ' + str(message))
try:
if message['commit-patch']['configuration']['protocols']['mpls']:
logger.debug("MPLS LSP config changes: " + str(message))
extractLSPconfig()
except:
logger.debug("Config changes not applying to MPLS LSPs")
return
LSP Configuration Extraction
The request-response session gathers the specific LSP configuration:
def extractLSPconfig():
mgmt_handle = client.GetManagementService()
conf_query = """
<get-configuration>
<configuration junos>
<protocols> <mpls></mpls>
</protocols>
</configuration>
</get-configuration>
"""
op_command =
OperationCommand(conf_query,OperationFormatType.OPERATION_FORMAT_XML,OperationFormatType.OPERATION_FORMAT_JSON)
result = mgmt_handle.ExecuteOpCommand(op_command)
parsed_json = json.loads(result.response)
[...]
After the LSP configuration is gathered, the stanza is parsed using jsonpath
regular expressions to extract the LSP description:
# Check and extract all descriptions from LSP configs using jsonpath
expr_description = "$..description"
# Obtain a list of available descriptions
lsp_descriptions = jsonpath.jsonpath(parsed_json,expr_description)
Alternatively, the apply-macro pce-routing-attributes
stanza is processed:
# Check and extract all apply_macros from LSP configs using jsonpath
expr_apply_macro = "$..apply-macro"
# Obtain a list of available apply-macros
lsp_macros = jsonpath.jsonpath(parsed_json,expr_apply_macro)
In both cases, the configuration elements are parsed to detect keywords representing attributes. If [protocols mpls label-switched-hath]
was modified, the daemonized JET App determines a new routing constraint has been added, deleted or modified, and makes a
REST API call for:
Constraint coded using LSP description:
protocols mpls { label-switched-path <name> {
description diversityGroup:<dg>;diversityLevel:<dl>;... [...] }}
or, apply-macro:
protocols mpls {
label-switched-path <name> {
apply-macro pce-routing-attributes {
diversity-group "<value>";
diversity-constraint "<site> | <link> | <srlg>";
symmetry-group "<value>";
routing-method "<adminWeight>"" | <delay> | <constant> | <distance> | <maxProtect>";
max-delay "<value>";
max-hop "<value>";
max-cost "<value>";
If there is a match in any of these elements, a subsequent function is invoked to start element extraction and NorthStar API
calls.
NorthStar API Calls Driven by Routing Attributes in LSP Description
In this case, the NorthStar class is instantiated in an object with the original input attributes, because there is guaranteed
to be a minimum of one LSP Update through the NorthStar API:
def UpdateLSPsInNorthStarUsingDescription(lsp_json):
# Create northstar handle
ns =
northstar.northstar(grant_type=args.northstar_grant_type,username=args.northstar_username,password=args.northstar_password,api_server=args.northstar_address,api_port=args.northstar_port,igp=args.network_igp,tenant_id=args.northstar_tenant_id)
# Authenticate Northstar
ns.get_token()
# Get default topology index
ns.get_topology_index()
After successful authentication and topology index is extracted, this function traverses the LSP descriptions, searching for
specific keywords:
# Key_list
key_list =
['diversityGroup','diversityLevel','routingMethod','maxDelay','maxHop','maxCost']
#Extract LSPs from json file
for lsp_config in lsp_json['configuration']['protocols']['mpls']['labelswitched-path']:
[…]
When a match is found, this constructs a dictionary of all key-value pairs and leverages the existing function in the main
NorthStar Python library to issue an update API request:
# Parse LSP description with ';' as delimiter
for item in str(lsp_config['description']).split(';'):
logger.debug('Extracting LSP description item: ' + item)
fields = item.split(':')
if fields[0] == 'diversityGroup':
lsp_pce_attrib['diversityGroup'] = fields[1]
if fields[0] == 'diversityLevel':
lsp_pce_attrib['diversityLevel'] = fields[1]
if fields[0] == 'routingMethod':
lsp_pce_attrib['routingMethod'] = fields[1]
if fields[0] == 'maxDelay':
lsp_pce_attrib['maxDelay'] = fields[1]
if fields[0] == 'maxHop':
lsp_pce_attrib['maxHop'] = fields[1]
if fields[0] == 'maxCost':
lsp_pce_attrib['maxCost'] = fields[1]
logger.debug('Updating LSP ' + lsp_config['name'] + ' with PCE routing attributes: ' + str(lsp_pce_attrib))
# Update LSP with PCE routing attributes
ns.update_lsp(old_lsp,**lsp_pce_attrib)
NorthStar REST API Calls Driven by Routing Attributes in apply-macro
As in API calls driven by routing attributes in the LSP, the NorthStar class is instantiated in an object with the original
input attributes, because there is guaranteed to be a minimum of one LSP update through the API:
def UpdateLSPsInNorthstarUsingMacros(lsp_json):
# Create northstar handle
ns =
northstar.northstar(grant_type=args.northstar_grant_type,username=args.northstar_username,password=args.northstar_password,api_server=args.northstar_address,api_port=args.northstar_port,igp=args.network_igp,tenant_id=args.northstar_tenant_id)
# Authenticate Northstar
ns.get_token()
# Get default topology index
ns.get_topology_index()
After successful authentication and the topology index is extracted, this function traverses the LSP configuration, searching
for the specific apply-macro
:
#Extract LSPs from json file
for lsp_config in lsp_json['configuration']['protocols']['mpls']['labelswitched-path']:
When a match is found on the apply-macro name, this constructs a dictionary from all key-value pairs and leverages the existing
function in the main NorthStar Python library to issue an update API request:
for item in lsp_macro['data']:
logger.debug('Extracting LSP apply-macro routing-constraint: ' + str(item))
if str(item['name']) == 'diversity-group':
lsp_pce_attrib['diversityGroup'] = item['value']
if str(item['name']) == 'diversity-constraint':
lsp_pce_attrib['diversityLevel'] = item['value']
if str(item['name']) == 'routing-method':
lsp_pce_attrib['routingMethod'] = item['value']
if str(item['name']) == 'max-delay':
lsp_pce_attrib['maxDelay'] = item['value']
if str(item['name']) == 'max-hop':
lsp_pce_attrib['maxHop'] = item['value']
if str(item['name']) == 'max-cost':
lsp_pce_attrib['maxCost'] = item['value']
logger.debug('Updating LSP ' + lsp_config['name'] + ' with PCE routing attributes: ' + str(lsp_pce_attrib))
# Update LSP with PCE routing attributes
ns.update_lsp(old_lsp,**lsp_pce_attrib)
Router Baseline Configuration
The required router configuration stanzas include:
-
Python as scripting language
[edit system]
scripts {
language python;
}
-
Extension services for request-response (grpc and/or thrift) and notification JET sessions (JET traceoptions enabled for troubleshooting)
[edit system]
services {
extension-service {
request-response {
grpc {
clear-text;
}
thrift {
clear-text {
port 9090;
}
}
}
notification {
port 1883;
max-connections 20;
allow-clients {
address 0.0.0.0/0;
}
}
traceoptions {
file jet.log size 1g;
flag all;
}
}
}
-
Commit notification enabling
[edit system]
commit {
notification;
}
-
JET App extension with concrete input attributes
[edit system]
extensions {
extension-service {
application {
file northstard.py {
arguments " --router_username regress --router_password MaRtInI
--northstar_address 192.168.33.4 --northstar_username admin --
northstar_password adminadmin --network_igp ISIS --output
/var/log/northstar_jet.log";
daemonize;
username root;
}
}
}
}
The JET notification session is required to listen to configuration change commit events and evaluate if a change has occurred
within the [edit protocols mpls]
stanza.
The JET request-response session is subsequently required to gather details under [edit protocols mpls]
to evaluate PCE routing attributes under a label-switched-path
and eventually trigger an update API request with the details. This separate session is required because all PCE routing
attributes might not be in the previous commit notification message. The notification message acts as a trigger, if impacting
[edit protocols mpls]
, for the request-response session to evaluate the configuration stanza and eventually issue an API request.
The JET application is enabled as an extension service in daemonized mode under the root
user, so it is continuously running in the background. Input attributes are constant and could, conceptually, be common across
the network, and cover mandatory arguments, such as local router username and password, NorthStar credentials, NorthStar PCS
host IPv4 address, the network IGP (to properly extract details from the right TED) and a local log file in the router where
information is traced (a-latraceoptions
: with different logging levels, according to the standard Python logging library).
Note that there are more considerations for the package implementation in the router:
-
The requests library is a directory, not a standalone file. This library is referrenced and used by the NorthStar library
to handle API call details. Currently, even though it is compiled out of the same package, it is a requirement that you explicitly
download the Python requests library and upload it to the right /var/db/scripts/jet/
directory:
root@R4> file list /var/db/scripts/jet/
/var/db/scripts/jet/:
jsonpath.py@ -> /packages/mnt/northstar/var/db/scripts/jet/jsonpath.py
northstar.py@ -> /packages/mnt/northstar/var/db/scripts/jet/northstar.py
northstard.py*
requests/
server-extension/
urllib2.py@ -> /packages/mnt/northstar/var/db/scripts/jet/urllib2.py
-
If no certificates are used, it is a requirement that you delete the symlink for the main application file, northstard.py
, and copy the file from /packages/mnt/northstar/var/db/scripts/jet/
to /var/db/scripts/jet/ directory
:
root@R4> file list /var/db/scripts/jet/
/var/db/scripts/jet/:
jsonpath.py@ -> /packages/mnt/northstar/var/db/scripts/jet/jsonpath.py
northstar.py@ -> /packages/mnt/northstar/var/db/scripts/jet/northstar.py
northstard.py*
requests/
server-extension/
urllib2.py@ -> /packages/mnt/northstar/var/db/scripts/jet/urllib2.py
JET Application REST Call Examples
Diverse LSP Pair
PE1 configuration for a diversely paired LSP:
protocols {
mpls {
label-switched-path pe1-to-pe2 {
to 1.1.1.2;
apply-macro pce-routing-attributes {
diversity-group 101;
diversity-constraint srlg;
}
lsp-external-controller pccd;
REST call JSON data:
{
"name": ”pe1-to-pe2",
"from": {"topoObjectType": "ipv4","address": ”1.1.1.1"},
"to": {"topoObjectType": "ipv4","address": ”1.1.1.2"},
"design": {
"diversityLevel":”srlg",
"diversityGroup":”101”
PE3 configuration for a diversely paired LSP:
protocols {
mpls {
label-switched-path pe3-to-pe4 {
to 1.1.1.4;
apply-macro pce-routing-attributes {
diversity-group 101;
diversity-constraint srlg;
}
lsp-external-controller pccd;
REST call JSON data:
{
"name": ”pe1-to-pe2",
"from": {"topoObjectType": "ipv4","address": ”1.1.1.3"},
"to": {"topoObjectType": "ipv4","address": ”1.1.1.4"},
"design": {
"diversityLevel":”srlg",
"diversityGroup":”101”
maxDelay Constraint
PE1 configuration for a LSP with a maximum delay constraint:
protocols {
mpls {
label-switched-path pe1-to-pe2 {
to 1.1.1.2;
apply-macro pce-routing-attributes {
max-delay 100;
}
lsp-external-controller pccd;
REST call JSON data:
{
"name": ”pe1-to-pe2",
"from": {"topoObjectType": "ipv4","address": ”1.1.1.1"},
"to": {"topoObjectType": "ipv4","address": ”1.1.1.2"},
"design": {
"maxDelay":”100",
Bidirectional Pair of LSPs
PE1 configuration:
protocols {
mpls {
label-switched-path pe1-to-pe2 {
to 1.1.1.2;
apply-macro pce-routing-attributes {
symmetry-group 102;
}
lsp-external-controller pccd;
REST call JSON data:
{
"name": ”pe1-to-pe2",
"from": {"topoObjectType": "ipv4","address": ”1.1.1.1"},
"to": {"topoObjectType": "ipv4","address": ”1.1.1.2"},
"design": {
"coroutedPairId":”102",
PE2 configuration:
protocols {
mpls {
label-switched-path pe2-to-pe1 {
to 1.1.1.1;
apply-macro pce-routing-attributes {
symmetry-group 102;
}
lsp-external-controller pccd;
REST call JSON data:
{
"name": ”pe2-to-pe1",
"from": {"topoObjectType": "ipv4","address": ”1.1.1.2"},
"to": {"topoObjectType": "ipv4","address": ”1.1.1.1"},
19
"design": {
"coroutedPairId":”102",
Routing Methods
PE1 configuration for a LSP with a delay based routing method:
protocols {
mpls {
label-switched-path pe1-to-pe2 {
to 1.1.1.2;
apply-macro pce-routing-attributes {
routing-method delay;
}
lsp-external-controller pccd;
REST call JSON data:
{
"name": ”pe1-to-pe2",
"from": {"topoObjectType": "ipv4","address": ”1.1.1.1"},
"to": {"topoObjectType": "ipv4","address": ”1.1.1.2"},
"design": {
"routingMethod":”delay",