Develop services and applications in NSO.
Loading...
Loading...
Loading...
Learn service development in Java with Examples.
As using Java for service development may be somewhat more involved than Python, this section provides further examples and additional tips for setting up the development environment for Java.
The two examples, a simple VLAN service and a Layer 3 MPLS VPN service are more elaborate but show the same techniques as Implementing Services.
If you or your team primarily focuses on services implemented in Python, feel free to skip or only skim through this section.
In this example, you will create a simple VLAN service in Java. In order to illustrate the concepts, the device configuration is simplified from a networking perspective and only uses one single device type (Cisco IOS).
We will first look at the following preparatory steps:
Prepare a simulated environment of Cisco IOS devices: in this example, we start from scratch in order to illustrate the complete development process. We will not reuse any existing NSO examples.
Generate a template service skeleton package: use NSO tools to generate a Java-based service skeleton package.
Write and test the VLAN Service Model.
Analyze the VLAN service mapping to IOS configuration.
These steps are no different from defining services using templates. Next is to start playing with the Java Environment:
Configuring the start and stop of the Java VM.
First look at the Service Java Code: introduction to service mapping in Java.
Developing by tailing log files.
Developing using Eclipse.
We will start by setting up a run-time environment that includes simulated Cisco IOS devices and configuration data for NSO. Make sure you have sourced the ncsrc
file.
Create a new directory that will contain the files for this example, such as:
Now, let's create a simulated environment with 3 IOS devices and an NSO that is ready to run with this simulated network:
Start the simulator and NSO:
Use the Cisco CLI towards one of the devices:
Use the NSO CLI to get the configuration:
Finally, set VLAN information manually on a device to prepare for the mapping later.
In the run-time directory, you created:
Note the packages
directory, cd
to it:
Currently, there is only one package, the Cisco IOS NED.
We will now create a new package that will contain the VLAN service.
This creates a package with the following structure:
During the rest of this section, we will work with the vlan/src/yang/vlan.yang
and vlan/src/java/src/com/example/vlan/vlanRFS.java
files.
So, if a user wants to create a new VLAN in the network what should the parameters be? Edit the vlan/src/yang/vlan.yang
according to below:
This simple VLAN service model says:
We give a VLAN a name, for example net-1
.
The VLAN has an id from 1 to 4096.
The VLAN is attached to a list of devices and interfaces. In order to make this example as simple as possible the interface name is just a string. A more correct and useful example would specify this is a reference to an interface to the device, but for now it is better to keep the example simple.
The VLAN service list is augmented into the services tree in NSO. This specifies the path to reach VLANs in the CLI, REST, etc. There are no requirements on where the service shall be added into NCS, if you want VLANs to be at the top level, simply remove the augments statement.
Make sure you keep the lines generated by the ncs-make-package
:
The two lines tell NSO that this is a service. The first line expands to a YANG structure that is shared amongst all services. The second line connects the service to the Java callback.
To build this service model, cd
to packages/vlan/src
and type make
(assumes that you have the prerequisite make
build system installed).
We can now test the service model by requesting NSO to reload all packages:
You can also stop and start NSO, but then you have to pass the option --with-package-reload
when starting NSO. This is important, NSO does not by default take any changes in packages into account when restarting. When packages are reloaded the state/packages-in-use
is updated.
Now, create a VLAN service, (nothing will happen since we have not defined any mapping).
Now, let us move on and connect that to some device configuration using Java mapping. Note well that Java mapping is not needed, templates are more straightforward and recommended but we use this as a "Hello World" introduction to Java service programming in NSO. Also at the end, we will show how to combine Java and templates. Templates are used to define a vendor-independent way of mapping service attributes to device configuration and Java is used as a thin layer before the templates to do logic, call-outs to external systems, etc.
The default configuration of the Java VM is:
By default, NCS will start the Java VM by invoking the command $NCS_DIR/bin/ncs-start-java-vm
. That script will invoke
The class NcsJVMLauncher
contains the main()
method. The started Java VM will automatically retrieve and deploy all Java code for the packages defined in the load path of the ncs.conf
file. No other specification than the package-meta-data.xml
for each package is needed.
The verbosity of Java error messages can be controlled by:
For more details on the Java VM settings, see NSO Java VM.
The service model and the corresponding Java callback are bound by the servicepoint name. Look at the service model in packages/vlan/src/yang
:
The corresponding generated Java skeleton, (one print 'Hello World!' statement added):
Modify the generated code to include the print "Hello World!" statement in the same way. Re-build the package:
Whenever a package has changed, we need to tell NSO to reload the package. There are three ways:
Just reload the implementation of a specific package, will not load any model changes: admin@ncs# packages package vlan redeploy
.
Reload all packages including any model changes: admin@ncs# packages reload
.
Restart NSO with reload option: $ncs --with-package-reload
.
When that is done we can create a service (or modify an existing one) and the callback will be triggered:
Now, have a look at the logs/ncs-java-vm.log
:
Tailing the ncs-java-vm.log
is one way of developing. You can also start and stop the Java VM explicitly and see the trace in the shell. To do this, tell NSO not to start the VM by adding the following snippet to ncs.conf
:
Then, after restarting NSO or reloading the configuration, from the shell prompt:
So modifying or creating a VLAN service will now have the "Hello World!" string show up in the shell. You can modify the package, then reload/redeploy, and see the output.
To use a GUI-based IDE Eclipse, first generate an environment for Eclipse:
This will generate two files, .classpath
and .project
. If we add this directory to Eclipse as a File -> New -> Java Project, uncheck the Use default location and enter the directory where the .classpath
and .project
have been generated.
We are immediately ready to run this code in Eclipse.
All we need to do is choose the main()
routine in the NcsJVMLauncher
class. The Eclipse debugger works now as usual, and we can, at will, start and stop the Java code.
Timeouts
A caveat worth mentioning here is that there exist a few timeouts between NSO and the Java code that will trigger when we are in the debugger. While developing with the Eclipse debugger and breakpoints, we typically want to disable these timeouts.
First, we have the three timeouts in ncs.conf
that matter. Set the three values of /ncs-config/japi/new-session-timeout
, /ncs-config/japi/query-timeout
, and /ncs-config/japi/connect-timeout
to a large value (see man page ncs.conf(5) for a detailed description on what those values are). If these timeouts are triggered, NSO will close all sockets to the Java VM.
Edit the file and enter the following XML entry just after the Webui entry:
Now, restart ncs
, and from now on start it as:
You can verify that the Java VM is not running by checking the package status:
Create a new project and start the launcher main
in Eclipse:
You can start and stop the Java VM from Eclipse. Note well that this is not needed since the change cycle is: modify the Java code, make
in the src
directory, and then reload the package. All while NSO and the JVM are running.
Change the VLAN service and see the console output in Eclipse:
Another option is to have Eclipse connect to the running VM. Start the VM manually with the -d
option.
Then you can set up Eclipse to connect to the NSO Java VM:
In order for Eclipse to show the NSO code when debugging, add the NSO Source Jars (add external Jar in Eclipse):
Navigate to the service create
for the VLAN service and add a breakpoint:
Commit a change of a VLAN service instance and Eclipse will stop at the breakpoint:
So the problem at hand is that we have service parameters and a resulting device configuration. Previously, we showed how to do that with templates. The same principles apply in Java. The service model and the device models are YANG models in NSO irrespective of the underlying protocol. The Java mapping code transforms the service attributes to the corresponding configuration leafs in the device model.
The NAVU API lets the Java programmer navigate the service model and the device models as a DOM tree. Have a look at the create
signature:
Two NAVU nodes are passed: the actual service service
instance and the NSO root ncsRoot
.
We can have a first look at NAVU by analyzing the first try
statement:
NAVU is a lazy evaluated DOM tree that represents the instantiated YANG model. So knowing the NSO model: devices/device
, (container/list
) corresponds to the list of capabilities for a device, this can be retrieved by ncsRoot.container("devices").list("device")
.
The service
node can be used to fetch the values of the VLAN service instance:
vlan/name
vlan/vlan-id
vlan/device-if/device and vlan/device-if/interface
The first snippet that iterates the service model and prints to the console looks like below:
The com.tailf.conf
package contains Java Classes representing the YANG types like ConfUInt32
.
Try it out in the following sequence:
Rebuild the Java Code: In packages/vlan/src
type make
.
Reload the Package: In the NSO Cisco CLI, do admin@ncs# packages package vlan redeploy
.
Create or Modify a vlan
Service: In NSO CLI, do admin@ncs(config)# services vlan net-0 vlan-id 844 device-if c0 interface 1/0
, and commit.
Remember the service
attribute is passed as a parameter to the create method. As a starting point, look at the first three lines:
To reach a specific leaf in the model use the NAVU leaf method with the name of the leaf as a parameter. This leaf then has various methods like getting the value as a string.
service.leaf("vlan-id")
and service.leaf(vlan._vlan_id_)
are two ways of referring to the VLAN-id leaf of the service. The latter alternative uses symbols generated by the compilation steps. If this alternative is used, you get the benefit of compilation time checking. From this leaf you can get the value according to the type in the YANG model ConfUInt32
in this case.
Line 3 shows an example of casting between types. In this case, we prepare the VLAN ID as a 16 unsigned int for later use.
The next step is to iterate over the devices and interfaces. The NAVU elements()
returns the elements of a NAVU list.
In order to write the mapping code, make sure you have an understanding of the device model. One good way of doing that is to create a corresponding configuration on one device and then display that with the pipe target display xpath
. Below is a CLI output that shows the model paths for FastEthernet 1/0
:
Another useful tool is to render a tree view of the model:
This can then be opened in a Web browser and model paths are shown to the right:
Now, we replace the print statements with setting real configuration on the devices.
Let us walk through the above code line by line. The device-name
is a leafref
. The deref
method returns the object that the leafref
refers to. The getParent()
might surprise the reader. Look at the path for a leafref: /device/name/config/ios:interface/name
. The name
leafref is the key that identifies a specific interface. The deref
returns that key, while we want to have a reference to the interface, (/device/name/config/ios:interface
), that is the reason for the getParent()
.
The next line sets the VLAN list on the device. Note well that this follows the paths displayed earlier using the NSO CLI. The sharedCreate()
is important, it creates device configuration based on this service, and it says that other services might also create the same value, "shared". Shared create maintains reference counters for the created configuration in order for the service deletion to delete the configuration only when the last service is deleted. Finally, the interface name is used as a key to see if the interface exists, "containsNode()"
.
The last step is to update the VLAN list for each interface. The code below adds an element to the VLAN leaf-list
.
Note that the code uses the sharedCreate()
functions instead of create()
, as the shared variants are preferred and a best practice.
The above create
method is all that is needed for create, read, update, and delete. NSO will automatically handle any changes, like changing the VLAN ID, adding an interface to the VLAN service, and deleting the service. This is handled by the FASTMAP engine, it renders any change based on the single definition of the create method.
The mapping strategy using only Java is illustrated in the following figure.
This strategy has some drawbacks:
Managing different device vendors. If we would introduce more vendors in the network this would need to be handled by the Java code. Of course, this can be factored into separate classes in order to keep the general logic clean and just pass the device details to specific vendor classes, but this gets complex and will always require Java programmers to introduce new device types.
No clear separation of concerns, domain expertise. The general business logic for a service is one thing, detailed configuration knowledge of device types is something else. The latter requires network engineers and the first category is normally separated into a separate team that deals with OSS integration.
Java and templates can be combined:
In this model, the Java layer focuses on required logic, but it never touches concrete device models from various vendors. The vendor-specific details are abstracted away using feature templates. The templates take variables as input from the service logic, and the templates in turn transform these into concrete device configuration. The introduction of a new device type does not affect the Java mapping.
This approach has several benefits:
The service logic can be developed independently of device types.
New device types can be introduced at runtime without affecting service logic.
Separation of concerns: network engineers are comfortable with templates, they look like a configuration snippet. They have expertise in how configuration is applied to real devices. People defining the service logic often are more programmers, they need to interface with other systems, etc, this suites a Java layer.
Note that the logic layer does not understand the device types, the templates will dynamically apply the correct leg of the template depending on which device is touched.
From an abstraction point of view, we want a template that takes the following variables:
VLAN ID
Device and interface
So the mapping logic can just pass these variables to the feature template and it will apply it to a multi-vendor network.
Create a template as described before.
Create a concrete configuration on a device, or several devices of different type
Request NSO to display that as XML
Replace values with variables
This results in a feature template like below:
This template only maps to Cisco IOS devices (the xmlns="urn:ios"
namespace), but you can add "legs" for other device types at any point in time and reload the package.
Nodes set with a template variable evaluating to the empty string are ignored, e.g., the setting <some-tag>{$VAR}</some-tag> is ignored if the template variable $VAR evaluates to the empty string. However, this does not apply to XPath expressions evaluating to the empty string. A template variable can be surrounded by the XPath function string() if it is desirable to set a node to the empty string.
The Java mapping logic for applying the template is shown below:
Note that the Java code has no clue about the underlying device type, it just passes the feature variables to the template. At run-time, you can update the template with mapping to other device types. The Java code stays untouched, if you modify an existing VLAN service instance to refer to the new device type the commit
will generate the corresponding configuration for that device.
The smart reader will complain, "Why do we have the Java layer at all?", this could have been done as a pure template solution. That is true, but now this simple Java layer gives room for arbitrary complex service logic before applying the template.
The steps to build the solution described in this section are:
Create a run-time directory: $ mkdir ~/service-template; cd ~/service-template
.
Generate a netsim environment: $ ncs-netsim create-network $NCS_DIR/packages/neds/cisco-ios 3 c
.
Generate the NSO runtime environment: $ ncs-setup --netsim-dir ./netsim --dest ./
.
Create the VLAN package in the packages directory: $ cd packages; ncs-make-package --service-skeleton java vlan
.
Create a template directory in the VLAN package: $ cd vlan; mkdir templates
.
Save the above-described template in packages/vlan/templates
.
Create the YANG service model according to the above: packages/vlan/src/yang/vlan.yang
.
Update the Java code according to the above: packages/vlan/src/java/src/com/example/vlan/vlanRFS.java
.
Build the package: in packages/vlan/src
do make
.
Start NSO.
This service shows a more elaborate service mapping. It is based on the examples.ncs/service-provider/mpls-vpn
example.
MPLS VPNs are a type of Virtual Private Network (VPN) that achieves segmentation of network traffic using Multiprotocol Label Switching (MPLS), often found in Service Provider (SP) networks. The Layer 3 variant uses BGP to connect and distribute routes between sites of the VPN.
The figure below illustrates an example configuration for one leg of the VPN. Configuration items in bold are variables that are generated from the service inputs.
Sometimes the input parameters are enough to generate the corresponding device configurations. But in many cases, this is not enough. The service mapping logic may need to reach out to other data in order to generate the device configuration. This is common in the following scenarios:
Policies: it might make sense to define policies that can be shared between service instances. The policies, for example, QoS, have data models of their own (not service models) and the mapping code reads from that.
Topology Information: the service mapping might need to know connected devices, like which PE the CE is connected to.
Resources like VLAN IDs, and IP Addresses: these might not be given as input parameters. This can be modeled separately in NSO or fetched from an external system.
It is important to design the service model to consider the above examples: what is input? what is available from other sources? This example illustrates how to define QoS policies "on the side". A reference to an existing QoS policy is passed as input. This is a much better principle than giving all QoS parameters to every service instance. Note well that if you modify the QoS definitions that services are referring to, this will not change the existing services. In order to have the service to read the changed policies you need to perform a re-deploy on the service.
This example also uses a list that maps every CE to a PE. This list needs to be populated before any service is created. The service model only has the CE as input parameter, and the service mapping code performs a lookup in this list to get the PE. If the underlying topology changes a service re-deploy will adopt the service to the changed CE-PE links. See more on topology below.
NSO has a package to manage resources like VLAN and IP addresses as a pool within NSO. In this way the resources are managed within the transaction. The mapping code could also reach out externally to get resources. Nano services are recommended for this.
Using topology information in the instantiation of an NSO service is a common approach, but also an area with many misconceptions. Just like a service in NSO takes a black-box view of the configuration needed for that service in the network NSO treats topologies in the same way. It is of course common that you need to reference topology information in the service but it is highly desirable to have a decoupled and self-sufficient service that only uses the part of the topology that is interesting/needed for the specific service should be used.
Other parts of the topology could either be handled by other services or just let the network state sort it out - it does not necessarily relate to the configuration of the network. A routing protocol will for example handle the IP path through the network.
It is highly desirable to not introduce unneeded dependencies towards network topologies in your service.
To illustrate this, let's look at a Layer 3 MPLS VPN service. A logical overview of an MPLS VPN with three endpoints could look something like this. CE routers connecting to PE routers, that are connected to an MPLS core network. In the MPLS core network, there are a number of P routers.
In the service model, you only want to configure the CE devices to use as endpoints. In this case, topology information could be used to sort out what PE router each CE router is connected to. However, what type of topology do you need? Lets look at a more detailed picture of what the L1 and L2 topology could look like for one side of the picture above.
In pretty much all networks there is an access network between the CE and PE router. In the picture above the CE routers are connected to local Ethernet switches connected to a local Ethernet access network, connected through optical equipment. The local Ethernet access network is connected to a regional Ethernet access network, connected to the PE router. Most likely the physical connections between the devices in this picture have been simplified, in the real world redundant cabling would be used. The example above is of course only one example of how an access network could look like and it is very likely that a service provider have different access technologies. For example Ethernet, ATM, or a DSL-based access network.
Depending on how you design the L3VPN service, the physical cabling or the exact traffic path taken in the layer 2 Ethernet access network might not be that interesting, just like we don't make any assumptions or care about how traffic is transported over the MPLS core network. In both these cases we trust the underlying protocols handling state in the network, spanning tree in the Ethernet access network, and routing protocols like BGP in the MPLS cloud. Instead in this case, it could make more sense to have a separate NSO service for the access network, both so it can be reused for both for example L3VPNs and L2VPN but also to not tightly couple to the access network with the L3VPN service since it can be different (Ethernet or ATM etc.).
Looking at the topology again from the L3VPN service perspective, if services assume that the access network is already provisioned or taken care of by another service, it could look like this.
The information needed to sort out what PE router a CE router is connected to as well as configuring both CE and PE routers is:
Interface on the CE router that is connected to the PE router, and IP address of that interface.
Interface on the PE router that is connected to the CE router, and IP address to the interface.
This section describes the creation of an MPLS L3VPN service in a multi-vendor environment by applying the concepts described above. The example discussed can be found in examples.ncs/service-provider/mpls-vpn
. The example network consists of Cisco ASR 9k and Juniper core routers (P and PE) and Cisco IOS-based CE routers.
The goal of the NSO service is to set up an MPLS Layer3 VPN on a number of CE router endpoints using BGP as the CE-PE routing protocol. Connectivity between the CE and PE routers is done through a Layer2 Ethernet access network, which is out of the scope of this service. In a real-world scenario, the access network could for example be handled by another service.
In the example network, we can also assume that the MPLS core network already exists and is configured.
When designing service YANG models there are a number of things to take into consideration. The process usually involves the following steps:
Identify the resulting device configurations for a deployed service instance.
Identify what parameters from the device configurations are common and should be put in the service model.
Ensure that the scope of the service and the structure of the model work with the NSO architecture and service mapping concepts. For example, avoid unnecessary complexities in the code to work with the service parameters.
Ensure that the model is structured in a way so that integration with other systems north of NSO works well. For example, ensure that the parameters in the service model map to the needed parameters from an ordering system.
Steps 1 and 2: Device Configurations and Identifying Parameters:
Deploying an MPLS VPN in the network results in the following basic CE and PE configurations. The snippets below only include the Cisco IOS and Cisco IOS-XR configurations. In a real process, all applicable device vendor configurations should be analyzed.
The device configuration parameters that need to be uniquely configured for each VPN have been marked in bold.
Steps 3 and 4: Model Structure and Integration with other Systems:
When configuring a new MPLS l3vpn in the network we will have to configure all CE routers that should be interconnected by the VPN, as well as the PE routers they connect to.
However, when creating a new l3vpn service instance in NSO it would be ideal if only the endpoints (CE routers) are needed as parameters to avoid having knowledge about PE routers in a northbound order management system. This means a way to use topology information is needed to derive or compute what PE router a CE router is connected to. This makes the input parameters for a new service instance very simple. It also makes the entire service very flexible, since we can move CE and PE routers around, without modifying the service configuration.
Resulting YANG Service Model:
The snipped above contains the l3vpn service model. The structure of the model is very simple. Every VPN has a name, an as-number, and a list of all the endpoints in the VPN. Each endpoint has:
A unique ID.
A reference to a device (a CE router in our case).
A pointer to the LAN local interface on the CE router. This is kept as a string since we want this to work in a multi-vendor environment.
LAN private IP network.
Bandwidth on the VPN connection.
To be able to derive the CE to PE connections we use a very simple topology model. Notice that this YANG snippet does not contain any service point, which means that this is not a service model but rather just a YANG schema letting us store information in CDB.
The model basically contains a list of connections, where each connection points out the device, interface, and IP address in each of the connections.
Since we need to look up which PE routers to configure using the topology model in the mapping logic it is not possible to use a declarative configuration template-based mapping. Using Java and configuration templates together is the right approach.
The Java logic lets you set a list of parameters that can be consumed by the configuration templates. One huge benefit of this approach is that all the parameters set in the Java code are completely vendor-agnostic. When writing the code, there is no need for knowledge of what kind of devices or vendors exist in the network, thus creating an abstraction of vendor-specific configuration. This also means that in to create the configuration template there is no need to have knowledge of the service logic in the Java code. The configuration template can instead be created and maintained by subject matter experts, the network engineers.
With this service mapping approach, it makes sense to modularize the service mapping by creating configuration templates on a per-feature level, creating an abstraction for a feature in the network. In this example means, we will create the following templates:
CE router
PE router
This is both to make services easier to maintain and create but also to create components that are reusable from different services. This can of course be even more detailed with templates with for example BGP or interface configuration if needed.
Since the configuration templates are decoupled from the service logic it is also possible to create and add additional templates in a running NSO system. You can for example add a CE router from a new vendor to the layer3 VPN service by only creating a new configuration template, using the set of parameters from the service logic, to a running NSO system without changing anything in the other logical layers.
The Java code part for the service mapping is very simple and follows the following pseudo code steps:
This section will go through relevant parts of Java outlined by the pseudo-code above. The code starts with defining the configuration templates and reading the list of endpoints configured and the topology. The Navu API is used for navigating the data models.
The next step is iterating over the VPN endpoints configured in the service, finding out connected PE router using small helper methods navigating the configured topology.
The parameter dictionary is created from the TemplateVariables class and is populated with appropriate parameters.
The last step after all parameters have been set is applying the templates for the CE and PE routers for this VPN endpoint.
The configuration templates are XML templates based on the structure of device YANG models. There is a very easy way to create the configuration templates for the service mapping if NSO is connected to a device with the appropriate configuration on it, using the following steps.
Configure the device with the appropriate configuration.
Add the device to NSO
Sync the configuration to NSO.
Display the device configuration in XML format.
Save the XML output to a configuration template file and replace configured values with parameters
The commands in NSO give the following output. To make the example simpler, only the BGP part of the configuration is used:
The final configuration template with the replaced parameters marked in bold is shown below. If the parameter starts with a $
-sign, it's taken from the Java parameter dictionary; otherwise, it is a direct xpath reference to the value from the service instance.
Develop NSO services using Visual Studio (VS) Code extensions.
NSO Developer Studio provides an integrated framework for developing NSO services using Visual Studio (VS) Code extensions. The extensions come with a core feature set to help you create services and connect to running CDB instances from within the VS Code environment. The following extensions are available as part of the NSO Developer Studio:
NSO Developer Studio - Developer: Used for creating NSO services. Also referred to as NSO Developer extension in this guide.
NSO Developer Studio - Explorer: Used for connecting to and inspecting NSO instance. Also referred to as NSO Explorer extension in this guide.
Throughout this guide, references to the VS Code GUI elements are made. It is recommended that you understand the GUI terminology before proceeding. To familiarize yourself with the VS Code GUI terminology, refer to VS Code UX Guidelines.
CodeLens is a VS Code feature to facilitate performing inline contextual actions. See Extensions using CodeLens for more information.
Contribute
If you feel certain code snippets would be helpful or would like to help contribute to enhancing the extension, please get in touch: jwycoff@cisco.com.
This section describes the installation and functionality of the NSO Developer extension.
The purpose of the NSO Developer extension is to provide a base framework for developers to create their own NSO services. The focus of this guide is to manifest the creation of a simple NSO service package using the NSO Developer extension. At this time, reactive FastMAP and Nano services are not supported with this extension.
In terms of an NSO package, the extension supports YANG, XML, and Python to bring together various elements required to create a simple service.
After the installation, you can use the extension to create services and perform additional functions described below.
To get started with development using the NSO Developer extension, ensure that the following prerequisites are met on your system. The prerequisites are not a requirement to install the NSO Developer extension, but for NSO development after the extension is installed.
Visual Studio Code.
Java JDK 11 or higher.
Python 3.9 or higher (recommended).
Installation of the NSO Developer extension is done via the VS Code marketplace.
To install the NSO Developer extension in your VS Code environment:
Open VS Code and click the Extensions icon on the Activity Bar.
Search for the extension using the keywords "nso developer studio" in the Search Extensions in Marketplace field.
In the search results, locate the extension (NSO Developer Studio - Developer) and click Install.
Wait while the installation completes. A notification at the bottom-right corner indicates that the installation has finished. After the installation, an NSO icon is added to the Activity Bar.
Use the Make Package command in VS Code to create a new Python package. The purpose of this command is to provide functionality similar to the ncs-make-package
CLI command, that is, to create a basic structure for you to start developing a new Python service package. The ncs-make-package
command, however, comes with several additional options to create a package.
To make a new Python service package:
In the VS Code menu, go to View, and choose Command Palette.
In the Command Palette, type or pick the command NSO: Make Package. This brings up the Make Package dialog where you can configure package details.
In the Make Package dialog, specify the following package details:
Package Name: Name of the package.
Package Location: Destination folder where the package is to be created.
Namespace: Namespace of the YANG module, e.g. http://www.cisco.com/myModule
.
Prefix: The prefix to be given to the YANG module, e.g. msp
.
Yang Version: The YANG version that this module follows.
Click Create Package. This creates the required package and opens up a new instance of VS Code with the newly created NSO package.
If the Workspace Trust dialog is shown, click Yes, I Trust the Authors.
Use the Open Existing Package command to open an already existing package.
To open an existing package:
In the VS Code menu, go to View, then choose Command Palette.
In the Command Palette, type or pick the command NSO: Open Existing Package.
Browse for the package on your local disk and open it. This brings up a new instance of VS Code and opens the package in it.
Opening a YANG file for edit results in VS Code detecting syntax errors in the YANG file. The errors show up due to missing path to YANG files and can be resolved using the following procedure.
Add YANG models for Yangster
For YANG support, a third-party extension called Yangster is used. Yangster is able to resolve imports for core NSO models but requires additional configuration.
To add YANG models for Yangster:
Create a new file named yang.settings
by right-clicking in the blank area of the Explorer view and choosing New File from the pop-up.
Locate the NSO source YANG files on your local disk and copy the path.
In the file yang.settings
, enter the path in the JSON format: { "yangPath": "<path to Yang files >" }
, for example, { "yangPath": /home/my-user-name/nso-6.0/src/ncs/yang}
. On Microsoft Windows, make sure that the backslash (\
) is escaped, e.g., "C:\\user\\folder\\src\\yang
".
Save the file.
Wait while the Yangster extension indexes and parses the YANG file to resolve NSO imports. After the parsing is finished, errors in the YANG file will disappear.
YANG diagram is a feature provided by the Yangster extension.
To view the YANG diagram:
Update the YANG file. (Pressing Ctrl+space brings up auto-completion where applicable.)
Right-click anywhere in the VS Code Editor area and select Open in Diagram in the pop-up.
To add a new YANG module:
In the Explorer view, navigate to the yang folder and select it.
Right-click on the yang folder and select NSO: Add Yang Module from the pop-up menu. This brings up the Create Yang Module dialog where you can configure module details.
In the Create Yang Module dialog, fill in the following details:
Module Name: Name of the module.
Namespace: Namespace of the module, e.g., http://www.cisco.com/myModule
.
Prefix: Prefix for the YANG module.
Yang Version: Version of YANG for this module.
Click Finish. This creates and opens up the newly created module.
Often while working on a package, there is a requirement to create a new service. This usually involves adding a service point. Adding a service point also requires other parts of the files to be updated, for example, Python.
Service points are usually added to lists.
To add a service point:
Update your YANG model as required. The extension automatically detects the list elements and displays a CodeLens called Add Service Point. An example is shown below.
Click the Add Service Point CodeLens. This brings up the Add Service Point dialog.
Fill in the Service Point ID that is used to identify the service point, for example, mySimpleService
.
Next, in the Python Details section, select using the Python Module field if you want to create a new Python module or use an existing one.
If you opt to create a new Python file, relevant sections are automatically updated in package-meta-data.xml
.
If you select an existing Python module from the list, it is assumed that you are selecting the correct module and that, it has been created correctly, i.e., the package-meta-data.xml
file is updated with the component definition.
Enter the Service CB Class, for example, SimpleServiceCB
.
Finish creating the service by clicking Add Service Point.
All action points in a YANG model must be registered in NSO. Registering an action point also requires other parts of the files to be updated, for example, Python (register_action
), and update package-meta-data
if needed.
Action points are usually defined to lists or containers.
To register an action point:
Update your YANG model as required. The extension automatically detects the action point elements in YANG and displays a CodeLens called Add Action Point. An example is shown below.
Note that it is mandatory to specify tailf:actionpoint <actionpointname>
under tailf:action <actionname>
. This is a known limitation.
The action point CodeLens at this time only works for the tailf:action
statement, and not for the YANG rpc
or YANG 1.1 action
statements.
Click the Add Action Point CodeLens. This brings up the Register Action Point dialog.
Next, in the Python Details section, select using the Python Module field if you want to create a new Python module or use an existing one.
If you opt to create a new Python file, relevant sections are automatically updated in package-meta-data.xml
.
If you select an existing Python module from the list, it is assumed that you are selecting the correct module, and that it has been created correctly, i.e., the package-meta-data.xml
file is updated with the component definition.
Enter the action class name in the Main Class name used as entry point field, for example, MyAction
.
Finish by clicking Register Action Point.
Opening a Python file uses the Microsoft Pylance extension. This extension provides syntax highlighting and other features such as code completion.
To resolve NCS import errors with the Pylance extension, you need to configure the path to NSO Python API in VS Code settings. To do this, go to VS Code Preferences > Settings and type python.analysis.extraPaths
in the Search settings field. Next, click Add Item, and enter the path to NSO Python API, for example, /home/my-user-name/nso-6.0/src/ncs/pyapi
. Press OK when done.
To add a new Python module:
In the Primary Sidebar, Explorer view, right-click on the python
folder.
Select NSO: Add Python Module from the pop-up. This brings up the Create Python Module dialog.
In the Create Python Module dialog, fill in the following details:
Module Name: Name of the module, for example, MyServicePackage.service
.
Component Name: Name of the component that will be used to identify this module, for example, service
.
Class Name: Name of the class to be invoked, for example, Main
.
Click Finish.
Pre-defined snippets in VS Code allow for NSO Python code completion.
To use a Python code completion snippet:
Open a Python file for editing.
Type in one of the following pre-defined texts to display snippet options:
maapi
: to view options for creating a maapi
write transaction.
ncs
: to view options for snippet for ncs
template and variables.
Select a snippet from the pop-up to insert its code. This also highlights config items that can be changed. Press the Tab key to cycle through each value.
The final part of a typical service development is creating and editing the XML configuration template.
Add a New XML Template
To add a new XML template:
In the Primary Sidebar, Explorer view, right-click on the templates folder.
Select NSO: Add XML Template from the pop-up. This brings up the Add XML Template dialog.
In the Add XML Template dialog, fill in the XML Template name, for example, mspSimpleService
.
Click Finish.
Use XML Code Completion Snippets
Pre-defined snippets in VS Code allow for NSO XML code completion of processing instructions and variables.
To use an XML code completion snippet:
Open an XML file for editing.
Type in one of the following pre-defined texts to display snippet options:
For processing instructions: <?
followed by a character, for example <?i
to view snippets for an if
statement. All supported processing instructions are available as snippets.
For variables: $
followed by a character(s) matching the variable name, for example, $VA
to view the variable snippet. Variables defined in the XML template via the <?set
processing instruction or defined in Python code are displayed.
Note: Auto-completion can also be triggered by pressing the Ctrl+Space keys.
Select an option from the pop-up to insert the relevant XML processing instruction or variable. Items that require further configuration are highlighted. Press the Tab key to cycle through the items.
XML Code Validation
The NSO Developer extension also performs code validation wherever possible. The following warning and error messages are shown if the extension is unable to validate the code:
A warning is shown if a user enters a variable in an XML template that is not detected by the NSO Developer extension.
An error message is shown if the ending tags in a processing instruction do not match.
The extension provides help on a best-effort basis by showing error messages and warnings wherever possible. Still, in certain situations, code validation is not possible. An example of such a limitation is when the extension is not able to detect a template variable that is defined elsewhere and passed indirectly (i.e., the variable is not directly called).
Consider the following code for example, where the extension will successfully detect that a template variable IP_ADDRESS
has been set.
vars.add('IP_ADDRESS','192.168.0.1')
Now consider the following code. While it serves the same purpose, it will fail to be detected.
ip_add_var_name = 'IP_ADDRESS' vars.add(ip_add_var_name, '192.168.0.1')
This section describes the installation and functionality of the NSO Explorer extension.
The purpose of the NSO Explorer extension is to allow the user to connect to a running instance of NSO and navigate the CDB from within VS Code.
To get started with the NSO Explorer extension, ensure that the following prerequisites are met on your system. The prerequisites are not a requirement to install the NSO Explorer extension, but for NSO development after the extension is installed.
Visual Studio Code.
Java JDK 11 or higher.
Python 3.9 or higher (recommended).
Installation of the NSO Explorer extension is done via the VS Code marketplace.
To install the NSO Explorer extension in your VS Code environment:
Open VS Code and click the Extensions icon on the Activity Bar.
Search for the extension using the keywords "nso developer studio" in the Search Extensions in Marketplace field.
In the search results, locate the extension (NSO Developer Studio - Explorer) and click Install.
Wait while the installation completes. A notification at the bottom-right corner indicates that the installation has finished. After the installation, an NSO icon is added to the Activity Bar.
The NSO Explorer extension allows you to connect to and inspect a live NSO instance from within the VS Code. This procedure assumes that you have not previously connected to an NSO instance.
To connect to an NSO instance:
In the Activity Bar, click the NSO icon to open NSO Explorer.
If no NSO instance is already configured, a welcome screen is displayed with an option to add a new NSO instance.
Click the Add NSO Instance button to open the Settings editor.
In the Settings editor, click the link Edit in settings.json. This opens the settings.json
file for editing.
Next, edit the settings.json
file as shown below:
Save the file when done.
If settings have been configured correctly, NSO Explorer will attempt to connect to the running NSO instance and display the NSO configuration.
Once the NSO Explorer extension is configured, the user can inspect the CDB tree.
To inspect the CDB tree, use the following functions:
Get Element Info: Click the i (info) icon on the Explorer bar, or alternatively inline next to an element in the Explorer view.
Copy KeyPath: Click the {KP}
icon to copy the keypath for the selected node.
Copy XPath: Click the {XP}
icon to copy the XPath for the selected node.
Get XML Config: Click the XML
icon to retrieve the XML configuration for the selected node and copy it to the clipboard.
If data has changed in NSO, click the refresh button at the top of the Explorer pane to fetch it.
Deep dive into service implementation.
Before you Proceed
This section discusses the implementation details of services in NSO. The reader should already be familiar with the concepts described in the introductory sections and Implementing Services.
For an introduction to services, see Develop a Simple Service instead.
Each service type in NSO extends a part of the data model (a list or a container) with the ncs:servicepoint
statement and the ncs:service-data
grouping. This is what defines an NSO service.
The service point instructs NSO to involve the service machinery (Service Manager) for management of that part of the data tree and the ncs:service-data
grouping contains definitions common to all services in NSO. Defined in tailf-ncs-services.yang
, ncs:service-data
includes parts that are required for the proper operation of FASTMAP and the Service Manager. Every service must therefore use this grouping as part of its data model.
In addition, ncs:service-data
provides a common service interface to the users, consisting of:
While not part of ncs:service-data
as such, you may consider the service-commit-queue-event
notification part of the core service interface. The notification provides information about the state of the service when the service uses the commit queue. As an example, an event-driven application uses this notification to find out when a service instance has been deployed to the devices. See the showcase_rc.py
script in examples.ncs/development-guide/concurrency-model/perf-stack/
for sample Python code, leveraging the notification. See tailf-ncs-services.yang
for the full definition of the notification.
NSO Service Manager is responsible for providing the functionality of the common service interface, requiring no additional user code. This interface is the same for classic and nano services, whereas nano services further extend the model.
NSO calls into Service Manager when accessing actions and operational data under the common service interface, or when the service instance configuration data (the data under the service point) changes. NSO being a transactional system, configuration data changes happen in a transaction.
When applied, a transaction goes through multiple stages, as shown by the progress trace (e.g. using commit | details
in the CLI). The detailed output breaks up the transaction into four distinct phases:
validation
write-start
prepare
commit
These phases deal with how the network-wide transactions work:
The validation phase prepares and validates the new configuration (including NSO copy of device configurations), then the CDB processes the changes and prepares them for local storage in the write-start phase.
The prepare stage sends out the changes to the network through the Device Manager and the HA system. The changes are staged (e.g. in the candidate data store) and validated if the device supports it, otherwise, the changes are activated immediately.
If all systems took the new configuration successfully, enter the commit phase, marking the new NSO configuration as active and activating or committing the staged configuration on remote devices. Otherwise, enter the abort phase, discarding changes, and ask NEDs to revert activated changes on devices that do not support transactions (e.g. without candidate data store).
There are also two types of locks involved with the transaction that are of interest to the service developer; the service write lock and the transaction lock. The latter is a global lock, required to serialize transactions, while the former is a per-service-type lock for serializing services that cannot be run in parallel. See Scaling and Performance Optimization for more details and their impact on performance.
The first phase, historically called validation, does more than just validate data and is the phase a service deals with the most. The other three support the NSO service framework but a service developer rarely interacts with directly.
We can further break down the first phase into the following stages:
rollback creation
pre-transform validation
transforms
full data validation
conflict check and transaction lock
When the transaction starts applying, NSO captures the initial intent and creates a rollback file, which allows one to reverse or roll back the intent. For example, the rollback file might contain the information that you changed a service instance parameter but it would not contain the service-produced device changes.
Then the first, partial validation takes place. It ensures the service input parameters are valid according to the service YANG model, so the service code can safely use provided parameter values.
Next, NSO runs transaction hooks and performs the necessary transforms, which alter the data before it is saved, for example encrypting passwords. This is also where the Service Manager invokes FASTMAP and service mapping callbacks, recording the resulting changes. NSO takes service write locks in this stage, too.
After transforms, there are no more changes to the configuration data, and the full validation starts, including YANG model constraints over the complete configuration, custom validation through validation points, and configuration policies (see Policies in Operation and Usage).
Throughout the phase, the transaction engine makes checkpoints, so it can restart the transaction faster in case of concurrency conflicts. The check for conflicts happens at the end of this first phase when NSO also takes the global transaction lock. Concurrency is further discussed in NSO Concurrency Model.
The main callback associated with a service point is the create callback, designed to produce the required (new) configuration, while FASTMAP takes care of the other operations, such as update and delete.
NSO implements two additional, optional callbacks for scenarios where create is insufficient. These are pre- and post-modification callbacks that NSO invokes before (pre) or after (post) create. These callbacks work outside of the scope tracked by FASTMAP. That is, changes done in pre- and post-modification do not automatically get removed during the update or delete of the service instance.
For example, you can use the pre-modification callback to check the service prerequisites (pre-check) or make changes that you want persisted even after the service is removed, such as enabling some global device feature. The latter may be required when NSO is not the only system managing the device and removing the feature configuration would break non-NSO managed services.
Similarly, you might use post-modification to reset the configuration to some default after the service is removed. Say the service configures an interface on a router for customer VPN. However, when the service is deprovisioned (removed), you don't want to simply erase the interface configuration. Instead, you want to put it in shutdown and configure it for a special, unused VLAN. The post-modification callback allows you to achieve this goal.
The main difference from create callback is that pre- and post-modification are called on update and delete, as well as service create. Since the service data node may no longer exist in case of delete, the API for these callbacks does not supply the service
object. Instead, the callback receives the operation and key path to the service instance. See the following API signatures for details.
The Python callbacks use the following function arguments:
tctx
: A TransCtxRef object containing transaction data, such as user session and transaction handle information.
op
: Integer representing operation: create (ncs.dp.NCS_SERVICE_CREATE
), update (ncs.dp.NCS_SERVICE_UPDATE
), or delete (ncs.dp.NCS_SERVICE_DELETE
) of the service instance.
kp
: A HKeypathRef object with a key path of the affected service instance, such as /svc:my-service{instance1}
.
root
: A Maagic node for the root of the data model.
service
: A Maagic node for the service instance.
proplist
: Opaque service properties, see Persistent Opaque Data.
The Java callbacks use the following function arguments:
context
: A ServiceContext object for accessing root and service instance NavuNode in the current transaction.
operation
: ServiceOperationType enum representing operation: CREATE
, UPDATE
, DELETE
of the service instance.
path
: A ConfPath object with a key path of the affected service instance, such as /svc:my-service{instance1}
.
ncsRoot
: A NavuNode for the root of the ncs
data model.
service
: A NavuNode for the service instance.
opaque
: Opaque service properties, see Persistent Opaque Data.
See examples.ncs/development-guide/services/post-modification-py
and examples.ncs/development-guide/services/post-modification-java
examples for a sample implementation of the post-modification callback.
Additionally, you may implement these callbacks with templates. Refer to Service Callpoints and Templates for details.
FASTMAP greatly simplifies service code, so it usually only needs to deal with the initial mapping. NSO achieves this by first discarding all the configuration performed during the create callback of the previous run. In other words, the service create code always starts anew, with a blank slate.
If you need to keep some private service data across runs of the create callback, or pass data between callbacks, such as pre- and post-modification, you can use opaque properties.
The opaque object is available in the service callbacks as an argument, typically named proplist
(Python) or opaque
(Java). It contains a set of named properties with their corresponding values.
If you wish to use the opaque properties, it is crucial that your code returns the properties object from the create call, otherwise, the service machinery will not save the new version.
Compared to pre- and post-modification callbacks, which also persist data outside of FASTMAP, NSO deletes the opaque data when the service instance is deleted, unlike with the pre- and post-modification data.
The examples.ncs/development-guide/services/post-modification-py
and examples.ncs/development-guide/services/post-modification-java
examples showcase the use of opaque properties.
NSO by default enables concurrent scheduling and execution of services to maximize throughput. However, concurrent execution can be problematic for non-thread-safe services or services that are known to always conflict with themselves or other services, such as when they read and write the same shared data. See NSO Concurrency Model for details.
To prevent NSO from scheduling a service instance together with an instance of another service, declare a static conflict in the service model, using the ncs:conflicts-with
extension. The following example shows a service with two declared static conflicts, one with itself and one with another service, named other-service
.
This means each service instance will wait for other service instances that have started sooner than this one (and are of example-service or other-service type) to finish before proceeding.
FASTMAP knows that a particular piece of configuration belongs to a service instance, allowing NSO to revert the change as needed. But what happens when several service instances share a resource that may or may not exist before the first service instance is created? If the service implementation naively checks for existence and creates the resource when it is missing, then the resource will be tracked with the first service instance only. If, later on, this first instance is removed, then the shared resource is also removed, affecting all other instances.
A well-known solution to this kind of problem is reference counting. NSO uses reference counting by default with the XML templates and Python Maagic API, while in Java Maapi and Navu APIs, the sharedCreate()
, sharedSet()
, and sharedSetValues()
functions need to be used.
When enabled, the reference counter allows FASTMAP algorithm to keep track of the usage and only delete data when the last service instance referring to this data is removed.
Furthermore, containers and list items created using the sharedCreate()
and sharedSetValues()
functions also get an additional attribute called backpointer
. (But this functionality is currently not available for individual leafs.)
backpointer
points back to the service instance that created the entity in the first place. This makes it possible to look at part of the configuration, say under /devices
tree, and answer the question: which parts of the device configuration were created by which service?
To see reference counting in action, start the examples.ncs/implement-a-service/iface-v3
example with make demo
and configure a service instance.
Then configure another service instance with the same parameters and use the display service-meta-data
pipe to show the reference counts and backpointers:
Notice how commit dry-run
produces no new device configuration but the system still tracks the changes. If you wish, remove the first instance and verify the GigabitEthernet 0/1
configuration is still there, but is gone when you also remove the second one.
But what happens if the two services produce different configurations for the same node? Say, one sets the IP address to 10.1.2.3
and the other to 10.1.2.4
. Conceptually, these two services are incompatible, and instantiating both at the same time produces a broken configuration (instantiating the second service instance breaks the configuration for the first). What is worse is that the current configuration depends on the order the services were deployed or re-deployed. For example, re-deploying the first service will change the configuration from 10.1.2.4
back to 10.1.2.3
and vice versa. Such inconsistencies break the declarative configuration model and really should be avoided.
In practice, however, NSO does not prevent services from producing such configuration. But note that we strongly recommend against it and that there are associated limitations, such as service un-deploy not reverting configuration to that produced by the other instance (but when all services are removed, the original configuration is still restored).
The commit | debug
service pipe command warns about any such conflict that it finds but may miss conflicts on individual leafs. The best practice is to use integration tests in the service development life cycle to ensure there are no conflicts, especially when multiple teams develop their own set of services that are to be deployed on the same NSO instance.
Much like a service in NSO can provision device configurations, it can also provision other, non-device data, as well as other services. We call the approach of services provisioning other services 'service stacking' and the services that are involved — 'stacked'.
Service stacking concepts usually come into play for bigger, more complex services. There are a number of reasons why you might prefer stacked services to a single monolithic one:
Smaller, more manageable services with simpler logic.
Separation of concerns and responsibility.
Clearer ownership across teams for (parts of) overall service.
Smaller services reusable as components across the solution.
Avoiding overlapping configuration between service instances causing conflicts, such as using one service instance per device (see examples in Designing for Maximal Transaction Throughput).
Stacked services are also the basis for LSA, which takes this concept even further. See Layered Service Architecture for details.
The standard naming convention with stacked services distinguishes between a Resource-Facing Service (RFS), that directly configures one or more devices, and a Customer-Facing Service (CFS), that is the top-level service, configuring only other services, not devices. There can be more than two layers of services in the stack, too.
While NSO does not prevent a single service from configuring devices as well as services, in the majority of cases this results in a less clean design and is best avoided.
Overall, creating stacked services is very similar to the non-stacked approach. First, you can design the RFS services as usual. Actually, you might take existing services and reuse those. These then become your lower-level services, since they are lower in the stack.
Then you create a higher-level service, say a CFS, that configures another service, or a few, instead of a device. You can even use a template-only service to do that, such as:
The preceding example references an existing iface
service, such as the one in the examples.ncs/implement-a-service/iface-v3
example. The output shows hard-coded values but you can change those as you would for any other service.
In practice, you might find it beneficial to modularize your data model and potentially reuse parts in both, the lower- and higher-level service. This avoids duplication while still allowing you to directly expose some of the lower-level service functionality through the higher-level model.
The most important principle to keep in mind is that the data created by any service is owned by that service, regardless of how the mapping is done (through code or templates). If the user deletes a service instance, FASTMAP will automatically delete whatever the service created, including any other services. Likewise, if the operator directly manipulates service data that is created by another service, the higher-level service becomes out of sync. The check-sync service action checks this for services as well as devices.
In stacked service design, the lower-level service data is under the control of the higher-level service and must not be directly manipulated. Only the higher-level service may manipulate that data. However, two higher-level services may manipulate the same structures, since NSO performs reference counting (see Reference Counting Overlapping Configuration).
This section lists some specific advice for implementing services, as well as any known limitations you might run into.
You may also obtain some useful information by using the debug service
commit pipe command, such as commit dry-run | debug service
. The command display the net effect of the service create code, as well as issue warnings about potentially problematic usage of overlapping shared data.
Service callbacks must be deterministic: NSO invokes service callbacks in a number of situations, such as for dry-run, check sync, and actual provisioning. If a service does not create the same configuration from the same inputs, NSO sees it as being out of sync, resulting in a lot of configuration churn and making it incompatible with many NSO features. If you need to introduce some randomness or rely on some other nondeterministic source of data, make sure to cache the values across callback invocations, such as by using opaque properties (see Persistent Opaque Data) or persistent operational data (see Operational Data) populated in a pre-modification callback.
Never overwrite service inputs: Service input parameters capture client intent and a service should never change its own configuration. Such behavior not only muddles the intent but is also temporary when done in the create callback, as the changes are reverted on the next invocation.
If you need to keep some additional data that cannot be easily computed each time, consider using opaque properties (see Persistent Opaque Data) or persistent operational data (see Operational Data) populated in a pre-modification callback.
No service ordering in a transaction: NSO is a transactional system and as such does not have the concept of order inside a single transaction. That means NSO does not guarantee any specific order in which the service mapping code executes if the same transaction touches multiple service instances. Likewise, your code should not make any assumptions about running before or after other service code.
Return value of create callback: The create callback is not the exclusive user of the opaque object; the object can be chained in several different callbacks, such as pre- and post-modification. Therefore, returning None/null
from create callback is not a good practice. Instead, always return the opaque object even if the create callback does not use it.
Avoid delete in service create: Unlike creation, deleting configuration does not support reference counting, as there is no data left to reference count. This means the deleted elements are tied to the service instance that deleted them.
Additionally, FASTMAP must store the entire deleted tree and restore it on every service change or re-deploy, only to be deleted again. Depending on the amount of deleted data, this is potentially an expensive operation.
So, a general rule of thumb is to never use delete in service create code. If an explicit delete is used, debug service
may display the following warning:\
However, the service may also delete data implicitly, through when
and choice
statements in the YANG data model. If a when
statement evaluates to false, the configuration tree below that node is deleted. Likewise, if a case
is set in a choice
statement, the previously set case
is deleted. This has the same limitations as an explicit delete.
To avoid these issues, create a separate service, that only handles deletion, and use it in the main service through the stacked service design (see Stacked Services). This approach allows you to reference count the deletion operation and contains the effect of restoring deleted data through a small, rarely-changing helper service. See examples.ncs/development-guide/services/shared-delete
for an example.
Alternatively, you might consider pre- and post-modification callbacks for some specific cases.
Prefer shared*()
functions: Non-shared create and set operations in the Java and Python low-level API do not add reference counts or backpointer information to changed elements. In case there is overlap with another service, unwanted removal can occur. See Reference Counting Overlapping Configuration for details.
In general, you should prefer sharedCreate()
, sharedSet()
, and sharedSetValues()
. If non-shared variants are used in a shared context, service debug
displays a warning, such as:\
Likewise, do not use MAAPI load_config
variants from the service code. Use the sharedSetValues()
function to load XML data from a file or a string.
Reordering ordered-by-user lists: If the service code rearranges an ordered-by-user list with items that were created by another service, that other service becomes out of sync. In some cases, you might be able to avoid out-of-sync scenarios by leveraging special XML template syntax (see Operations on ordered lists and leaf-lists) or using service stacking with a helper service.
In general, however, you should reconsider your design and try to avoid such scenarios.
Automatic upgrade of keys for existing services is unsupported: Service backpointers, described in Reference Counting Overlapping Configuration, rely on the keys that the service model defines to identify individual service instances. If you update the model by adding, removing, or changing the type of leafs used in the service list key, while there are deployed service instances, the backpointers will not be automatically updated. Therefore, it is best to not change the service list key.
A workaround, if the service key absolutely must change, is to first perform a no-networking undeploy of the affected service instances, then upgrade the model, and finally no-networking re-deploy the previously un-deployed services.
Avoid conflicting intents: Consider that a service is executed as part of a transaction. If, in the same transaction, the service gets conflicting intents, for example, it gets modified and deleted, the transaction is aborted. You must decide which intent has higher priority and design your services to avoid such situations.
A very common situation, when NSO is deployed in an existing network, is that the network already has services implemented. These services may have been deployed manually or through an older provisioning system. To take full advantage of the new system, you should consider importing the existing services into NSO. The goal is to use NSO to manage existing service instances, along with adding new ones in the future.
The process of identifying services and importing them into NSO is called Service Discovery and can be broken down into the following high-level parts:
Implementing the service to match existing device configuration.
Enumerating service instances and their parameters.
Amend the service metadata references with reconciliation.
Ultimately, the problem that service discovery addresses is one of referencing or linking configuration to services. Since the network already contains target configuration, a new service instance in NSO produces no changes in the network. This means the new service in NSO by default does not own the network configuration. One side effect is that removing a service will not remove the corresponding device configuration, which is likely to interfere with service modification as well.
Some of the steps in the process can be automated, while others are mostly manual. The amount of work differs a lot depending on how structured and consistent the original deployment is.
A prerequisite (or possibly the product in an iterative approach) is an NSO service that supports all the different variants of the configuration for the service that are used in the network. This usually means there will be a few additional parameters in the service model that allow selecting the variant of device configuration produced, as well as some covering other non-standard configurations (if such configuration is present).
In the simplest case, there is only one variant and that is the one that the service needs to produce. Let's take the examples.ncs/implement-a-service/iface-v2-py
example and consider what happens when a device already has an existing interface configuration.
Configuring a new service instance does not produce any new device configuration (notice that device c1 has no changes).
However, when committed, NSO records the changes, just like in the case of overlapping configuration (see Reference Counting Overlapping Configuration). The main difference is that there is only a single backpointer, to a newly configured service, but the refcount
is 2. The other item, that contributes to the refcount
, is the original device configuration. Which is why the configuration is not deleted when the service instance is.
A prerequisite for service discovery to work is that it is possible to construct a list of the already existing services. Such a list may exist in an inventory system, an external database, or perhaps just an Excel spreadsheet.
You can import the list of services in a number of ways. If you are reading it in from a spreadsheet, a Python script using NSO API directly (Basic Automation with Python) and a module to read Excel files is likely a good choice.
Or, you might generate an XML data file to import using the ncs_load
command; use display xml
filter to help you create a template:
Regardless of the way you implement the data import, you can run into two kinds of problems.
On one hand, the service list data may be incomplete. Suppose that the earliest service instances deployed did not take the network mask as a parameter. Moreover, for some specific reasons, a number of interfaces had to deviate from the default of 28 and that information was never populated back in the inventory for old services after the netmask parameter was added.
Now the only place where that information is still kept may be the actual device configuration. Fortunately, you can access it through NSO, which may allow you to extract the missing data automatically, for example:
On the other hand, some parameters may be NSO specific, such as those controlling which variant of configuration to produce. Again, you might be able to use a script to find this information, or it could turn out that the configuration is too complex to make such a script feasible.
In general, this can be the most tricky part of the service discovery process, making it very hard to automate. It all comes down to how good the existing data is. Keep in mind that this exercise is typically also a cleanup exercise, and every network will be different.
The last step is updating the metadata, telling NSO that a given service controls (owns) the device configuration that was already present when the NSO service was configured. This is called reconciliation and you achieve it using a special re-deploy reconcile
action for the service.
Let's examine the effects of this action on the following data:
Having run the action, NSO has updated the refcount
to remove the reference to the original device configuration:
What is more, the reconcile algorithm works even if multiple service instances share configuration. What if you had two instances of the iface
service, instead of one?
Before reconciliation, the device configuration would show a refcount of three.
Invoking re-deploy reconcile
on either one or both of the instances makes the services sole owners of the configuration.
This means the device configuration is removed only when you remove both service instances.
The reconcile operation only removes the references to the original configuration (without the service backpointer), so you can execute it as many times as you wish. Just note that it is part of a service re-deploy, with all the implications that brings, such as potentially deploying new configuration to devices when you change the service template.
As an alternative to the re-deploy reconcile
, you can initially add the service configuration with a commit reconcile
variant, performing reconciliation right away.
It is hard to design a service in one go when you wish to cover existing configurations that are exceedingly complex or have a lot of variance. In such cases, many prefer an iterative approach, where you tackle the problem piece-by-piece.
Suppose there are two variants of the service configured in the network; iface-v2-py
and the newer iface-v3
, which produces a slightly different configuration. This is a typical scenario when a different (non-NSO) automation system is used and the service gradually evolves over time. Or, when a Method of Procedure (MOP) is updated if manual provisioning is used.
We will tackle this scenario to show how you might perform service discovery in an iterative fashion. We shall start with the iface-v2-py
as the first iteration of the iface
service, which represents what configuration the service should produce to the best of our current knowledge.
There are configurations for two service instances in the network already: for interfaces 0/1 and 0/2 on the c1
device. So, configure the two corresponding iface
instances.
You can also use the commit no-deploy
variant to add service parameters when a normal commit would produce device changes, which you do not want.
Then use the re-deploy reconcile { discard-non-service-config } dry-run
command to observe the difference between the service-produced configuration and the one present in the network.
For instance1
, the config is the same, so you can safely reconcile it already.
But interface 0/2 (instance2
), which you suspect was initially provisioned with the newer version of the service, produces the following:
The output tells you that the service is missing the ip dhcp snooping trust
part of the interface configuration. Since the service does not generate this part of the configuration yet, running re-deploy reconcile { discard-non-service-config }
(without dry-run) would remove the DHCP trust setting. This is not what we want.
One option, and this is the default reconcile mode, would be to use keep-non-service-config
instead of discard-non-service-config
. But that would result in the service taking ownership of only part of the interface configuration (the IP address).
Instead, the right approach is to add the missing part to the service template. There is, however, a little problem. Adding the DHCP snooping trust configuration unconditionally to the template can interfere with the other service instance, instance1
.
In some cases, upgrading the old configuration to the new variant is viable, but in most situations, you likely want to avoid all device configuration changes. For the latter case, you need to add another parameter to the service model that selects the configuration variant. You must update the template too, producing the second iteration of the service.
With the updated configuration, you can now safely reconcile the service2
service instance:
Nevertheless, keep in mind that the discard-non-service-config reconcile operation only considers parts of the device configuration under nodes that are created with the service mapping. Even if all data there is covered in the mapping, there could still be other parts that belong to the service but reside in an entirely different section of the device configuration (say DNS configuration under ip name-server
, which is outside the interface GigabitEthernet
part) or even a different device. That kind of configuration the discard-non-service-config
option cannot find on its own and you must add manually.
You can find the complete iface
service as part of the examples.ncs/development-guide/services/discovery
example.
Since there were only two service instances to reconcile, the process is now complete. In practice, you are likely to encounter multiple variants and many more service instances, requiring you to make additional iterations. But you can follow the iterative process shown here.
In some cases a service may need to rely on the actual device configurations to compute the changeset. It is often a requirement to pull the current device configurations from the network before executing such service. Doing a full sync-from
on a number of devices is an expensive task, especially if it needs to be performed often. The alternative way in this case is using partial-sync-from
.
In cases where a multitude of service instances touch a device that is not entirely orchestrated using NSO, i.e. relying on the partial-sync-from
feature described above, and the device needs to be replaced then all services need to be re-deployed. This can be expensive depending on the number of service instances. Partial-sync-to
enables the replacement of devices in a more efficient fashion.
Partial-sync-from
and partial-sync-to
actions allow to specify certain portions of the device's configuration to be pulled or pushed from or to the network, respectively, rather than the full config. These are more efficient operations on NETCONF devices and NEDs that support the partial-show feature. NEDs that do not support the partial-show feature will fall back to pulling or pushing the whole configuration.
Even though partial-sync-from
and partial-sync-to
allows to pull or push only a part of the device's configuration, the actions are not allowed to break the consistency of configuration in CDB or on the device as defined by the YANG model. Hence, extra consideration needs to be given to dependencies inside the device model. If some configuration item A depends on configuration item B in the device's configuration, pulling only A may fail due to unsatisfied dependency on B. In this case, both A and B need to be pulled, even if the service is only interested in the value of A.
It is important to note that partial-sync-from
and partial-sync-to
clear the transaction ID of the device in NSO unless the whole configuration has been selected (e.g. /ncs:devices/ncs:device[ncs:name='ex0']/ncs:config
). This ensures NSO does not miss any changes to other parts of the device configuration but it does make the device out of sync.
sync-from
Pulling the configuration from the network needs to be initiated outside the service code. At the same time, the list of configuration subtrees required by a certain service should be maintained by the service developer. Hence it is a good practice for such a service to implement a wrapper action that invokes the generic /devices/partial-sync-from
action with the correct list of paths. The user or application that manages the service would only need to invoke the wrapper action without needing to know which parts of the configuration the service is interested in.
The snippet in the example below (Example of running partial-sync-from action via Java API) gives an example of running partial-sync-from action via Java, using router
device from examples.ncs/getting-started/developing-with-ncs/0-router-network
.