Cisco NSO Development Environment in Docker

Blog Detail

Developing services for Cisco NSO requires a development environment, which can be set up locally on your laptop or on a dedicated server. When a developer starts working on an NSO service, it has to install a correct NSO version, all required NEDs, and dependencies. This is where Docker can come in handy. It is a tool to make packaging easy, which means that you can pre-build a Docker image, which contains NSO, NEDs, and all dependencies.

In this blog post, I will show you how to create an NSO Docker image that can be used for development. There are a couple of advantages of using Docker containers for development. The most important is that everything can be packaged together in a single unit.

To build a Docker image, Docker requires a Dockerfile, where you put build instructions to create an image. I can create Dockerfile from scratch. But there is a better way. NSO in Docker from Cisco is a repository that contains scripts and files to create a base NSO Docker image. Using the NSO in Docker project will produce a base image, which can be extended with additional packages.

This blog post is divided in three sections:

  • Prepare a base image
  • Extend the base image
  • Run a Docker container

Prepare a Base Image

The first step in the process is to build a base image, which is a bare NSO installation that contains NSO only. The NSO in Docker project is used in this step.

Clone the Repository

The NSO in Docker repository should be cloned first.

$ git clone git@github.com:NSO-developer/nso-docker.git
Cloning into 'nso-docker'...
remote: Enumerating objects: 4597, done.
remote: Counting objects: 100% (1579/1579), done.
remote: Compressing objects: 100% (635/635), done.
remote: Total 4597 (delta 1043), reused 1395 (delta 871), pack-reused 3018
Receiving objects: 100% (4597/4597), 1.36 MiB | 2.08 MiB/s, done.
Resolving deltas: 100% (2932/2932), done.
$
$ cd nso-docker/
nso-docker$

Download Installation Files

To install NSO, you need the installation file, which is not included in the NSO in Docker project. You can download the installation file from the Cisco DevNet page. The installation file is an executable script, which verifies a signature and creates the installer file when executed.

You should put the file into the ./nso-install-files directory, which is a default path, where the build script will take a look for the NSO installer.

nso-docker$ cd nso-install-files
nso-install-files$ ls
nso-5.5.linux.x86_64.signed.bin

The script should be executed to extract the installer file.

nso-install-files$ sh nso-5.5.linux.x86_64.signed.bin
Unpacking...
Verifying signature...
Retrieving CA certificate from http://www.cisco.com/security/pki/certs/crcam2.cer ...
Successfully retrieved and verified crcam2.cer.
Retrieving SubCA certificate from http://www.cisco.com/security/pki/certs/innerspace.cer ...
Successfully retrieved and verified innerspace.cer.
Successfully verified root, subca and end-entity certificate chain.
Successfully fetched a public key from tailf.cer.
Successfully verified the signature of nso-5.5.linux.x86_64.installer.bin using tailf.cer

This produces multiple files. All these files should be removed and you should only keep the actual installer in the directory.

nso-install-files$ ls
nso-5.5.linux.x86_64.installer.bin

Build the Base Image

Now that the installer is in the ./nso-install-files you are ready to build the base image. You need to run the make command in the root of the project.

nso-install-files$ cd ..
nso-docker$ make
...
OMITTED FOR BREVITY
...

This creates a base image. It is as simple as that.

nso-docker$ docker image ls
REPOSITORY            TAG               IMAGE ID       CREATED       SIZE
cisco-nso-base        5.5-foobar        856ffdeef133   6 days ago    564MB
cisco-nso-dev         5.5-foobar        04f1f5da57b3   6 days ago    1.4GB

The make command creates two images:

  • a production image, which contains only required packages
  • a development image, which also contains documentation, compilers, and other tools

The make command adds the username to the image tag (5.5-foobar), but you should add another tag that only includes the NSO version.

Add a New Tag to the Base Image

The Makefile comes with another directive (tag-release), which will be used to tag the image.

nso-docker$ make NSO_VERSION=5.5 tag-release
docker tag cisco-nso-dev:5.5-foobar cisco-nso-dev:5.5
docker tag cisco-nso-base:5.5-foobar cisco-nso-base:5.5
nso-docker$ docker image ls
REPOSITORY            TAG               IMAGE ID       CREATED       SIZE
cisco-nso-base        5.5               856ffdeef133   6 days ago    564MB
cisco-nso-base        5.5-foobar        856ffdeef133   6 days ago    564MB
cisco-nso-dev         5.5               04f1f5da57b3   6 days ago    1.4GB
cisco-nso-dev         5.5-foobar        04f1f5da57b3   6 days ago    1.4GB

The base image is now ready.

Extend the Base Image

The base image will be extended with additional packages such as NEDs. The cisco-nso-base image will be used as a base image. All instructions to extend the base image will be added into the Dockerfile.

Create Dockerfile

Let’s first create Dockerfile. The following Docker image is pretty simple, but it can be more complex than that. The file contains the instructions to extend the cisco-nso-dev:5.5 base image. In the build process, Docker will create a directory and copy NED files from local neds directory to /var/opt/ncs/packages/ inside the container.

nso-docker$ cd ../nso-dev-image
nso-dev-image$ cat Dockerfile
FROM cisco-nso-dev:5.5

RUN mkdir -p /var/opt/ncs/packages/
COPY neds/*.tar.gz /var/opt/ncs/packages/

Download NEDs

You can download NEDs from DevNet and add these as compressed .tar.gz files to the neds directory.

nso-dev-image$ ls neds
cisco-ios-cli-6.69.tar.gz cisco-nx-cli-5.21.tar.gz

Build Docker Image

Now all components are prepared to build an NSO Docker image.

nso-dev-image$ docker build -t nso:5.5-neds .
[+] Building 2.2s (8/8) FINISHED
 => [internal] load build definition from Dockerfile                                                                        0.0s
 => => transferring dockerfile: 144B                                                                                        0.0s
 => [internal] load .dockerignore                                                                                           0.0s
 => => transferring context: 2B                                                                                             0.0s
 => [internal] load metadata for docker.io/library/cisco-nso-dev:5.5                                                        0.0s
 => [1/3] FROM docker.io/library/cisco-nso-dev:5.5                                                                          0.1s
 => [internal] load build context                                                                                           1.4s
 => => transferring context: 83.84MB                                                                                        1.4s
 => [2/3] RUN mkdir -p /var/opt/ncs/packages/                                                                               0.7s
 => [3/3] COPY neds/*.tar.gz /var/opt/ncs/packages/                                                                         0.2s
 => exporting to image                                                                                                      0.4s
 => => exporting layers                                                                                                     0.4s
 => => writing image sha256:2ee95dcc017a77e3725f984de62c9f896f5c73d8fc00130fd43ca6c0fc2aee6e                                0.0s
 => => naming to docker.io/library/nso:5.5-neds                                                                             0.0s

After a couple of moments the image is ready. The name of the image is nso and it is tagged as 5.5-neds.

nso-dev-image$ docker image ls
REPOSITORY            TAG               IMAGE ID       CREATED          SIZE
nso                   5.5-neds          2ee95dcc017a   30 seconds ago   1.48GB
cisco-nso-base        5.5               856ffdeef133   6 days ago       564MB
cisco-nso-base        5.5-foobar        856ffdeef133   6 days ago       564MB
cisco-nso-dev         5.5               04f1f5da57b3   6 days ago       1.4GB
cisco-nso-dev         5.5-foobar        04f1f5da57b3   6 days ago       1.4GB

Push the Image to the Registry

In order to allow other developers to use the same image, the image can be pushed to a registry, such as Docker Hub. To do that, another tag must be added to the image.

nso-dev-image$ docker tag nso:5.5-neds networktocode/nso:5.5-neds
nso-dev-image$ docker image ls
REPOSITORY            TAG               IMAGE ID       CREATED         SIZE
nso                   5.5-neds          2ee95dcc017a   3 minutes ago   1.48GB
networktocode/nso     5.5-neds          2ee95dcc017a   3 minutes ago   1.48GB
cisco-nso-base        5.5               856ffdeef133   6 days ago      564MB
cisco-nso-base        5.5-foobar        856ffdeef133   6 days ago      564MB
cisco-nso-dev         5.5               04f1f5da57b3   6 days ago      1.4GB
cisco-nso-dev         5.5-foobar        04f1f5da57b3   6 days ago      1.4GB

Once the image is tagged, it can be pushed to the registry.

nso-dev-image$ docker push networktocode/nso:5.5-neds

From this point, the image is available to anyone that has access to the registry.

Run a Docker Container

To make everything easier and repeatable, you can create a Makefile, which contains commands to start and purge NSO containers.

Create Makefile

Let’s first create a Makefile. There are two directives we’ll add to the Makefile. The first one starts a container, while the other is used to stop and remove the container.

There are a couple of options in the run directive. The one that should be pointed out is the -v option. This option maps a directory on a local filesystem to a directory in a container. You can map an existing git repository with NSO services to the directory inside the container. NSO loads services from that directory. Because files are originally located on a local filesystem, you can use preferred tools for development, while using Docker as a runtime environment. In this example the services directory is mapped.

nso-dev-image$ cat Makefile

.PHONY: run
run:
	docker run -itd --name nso \
                   -v ${PWD}/../services:/nso/run/packages \
                   -e ADMIN_PASSWORD=admin \
                   -p 2024:22 \
                   networktocode/nso:5.5-neds

.PHONY: purge
purge:
	docker stop nso
	docker rm nso

Start a Container

You can now start a container using the make run command.

nso-dev-image$ make run

And voila, the container with NSO is running. After a couple of moments the container is ready to use.

nso-dev-image$ docker ps
CONTAINER ID   IMAGE                        COMMAND         CREATED         STATUS                   PORTS                                                      NAMES
3623e63ab2a6   networktocode/nso:5.5-neds   "/run-nso.sh"   4 minutes ago   Up 4 minutes (healthy)   80/tcp, 443/tcp, 830/tcp, 4334/tcp, 0.0.0.0:2024->22/tcp   nso

Connect to NSO

You can now use ssh to connect to the NSO instance. The port 22 in the container is mapped to the port 2024 on the host. Let’s open an ssh session to port 2024.

nso-dev-image$ ssh admin@localhost -p 2024
admin@localhost's password:

User admin last logged in 2021-04-26T21:31:21.009543+00:00, to 3623e63ab2a6, from 172.17.0.1 using cli-ssh
admin connected from 172.17.0.1 using ssh on 3623e63ab2a6
admin@ncs>

Verify Packages

You can now work with NSO as you would with any other instance running natively on the system or running on a dedicated server. The following example displays the packages that are deployed in NSO.

admin@ncs> switch cli
admin@ncs# show packages package oper-status
                                                                                                      PACKAGE
                        PROGRAM                                                                       META     FILE
                        CODE     JAVA           PYTHON         BAD NCS  PACKAGE  PACKAGE  CIRCULAR    DATA     LOAD   ERROR
NAME                UP  ERROR    UNINITIALIZED  UNINITIALIZED  VERSION  NAME     VERSION  DEPENDENCY  ERROR    ERROR  INFO
-----------------------------------------------------------------------------------------------------------------------------
cisco-ios-cli-6.69  X   -        -              -              -        -        -        -           -        -      -
cisco-nx-cli-5.21   X   -        -              -              -        -        -        -           -        -      -
l3vpn               X   -        -              -              -        -        -        -           -        -      -

There are two NEDs that were added during the build process. Another package called l3vpn is included in my services directory, which is mapped to the container. You can make changes to the l3vpn packages locally on the host, and when you are ready, you can reload packages in NSO and test the new functionality.

Cleanup

It is also quite simple to remove the development environment. Since you have the purge directive in the Makefile, you can simply run the make purge command to stop and remove the container.

nso-dev-image$ make purge
docker stop nso
nso
docker rm nso
nso

Conclusion

Using Docker for NSO development brings many advantages. It is easy to set up an environment. Developers only need to run a Docker container, while an image is automatically pulled from a Docker registry. The same image can be used later in test and development environments, and you can be pretty sure that all packages and dependencies are installed. You can avoid “it works on my laptop” issues, because the same runtime environment can be used in development and in production.

The build process requires more steps, but it can be easily automated. Typically, the process will be running as a job in a CI/CD pipeline.

Hopefully this post shows you how to build an NSO development environment using Docker containers.

-Uros

References



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

NSO Tips and Tricks – NSO Actions

Blog Detail

I recently brought up the usefulness of custom NSO Actions in a Twitter discussion with several Cisco friends, and promised to provide a brief demo to illustrate how to use them. This is not the first time I have had this type of discussion. People who have been exposed to NSO’s functionality often gravitate towards and focus on the service mechanics, and rightly so. They are incredibly powerful and a core feature for the product. However, the flexible utility of using NSO actions is an underrated feature of NSO, which I want to educate others about.

NSO Actions

What is an NSO Action?

It is effectively an RPC call used by NSO to execute some arbitrary set of code associated with that Yang NSO Action. For example, the common action of sync-from is a built in NSO action which does not store any data on its own, but when triggered, causes NSO to execute code behind the scenes to log into a network device and sync the data from the device into NSO’s CDB.

The NSO Action is a combination of a Yang model, defining the constraints / data model of the inputs expected or outputs expected for the action, and then some code linked to it when the action is triggered. When a NSO action is created, just like any custom package in NSO, the Yang propagates the action to be available across all of the NSO APIs (CLI, GUI, REST/RESTCONF, Python, etc). This means with a few lines of Yang and a few lines of Python, you very quickly have an API interface exposed to any user who has access to NSO.

Custom NSO Actions

This blog is not meant to be a deep dive on the nitty gritty details of custom NSO actions, but rather a quick primer on what they are and how to use them. Also, for the sake of simplicity, I will focus on Python NSO actions, rather than Java, etc.

A NSO custom action is defined in a package’s yang file with the following syntax:

    tailf:action double {
      tailf:actionpoint ACTION-NAME-action;
      input {
        leaf some-input-name {
          type string;
        }
      }
      output {
        leaf result {
          type string;
        }
      }
    }

and just for comparison, looking at the NSO source Yang files(tailf-ncs-devices.yang), you can see for example, the NSO sync-from action Yang which is actually used by the NSO:

tailf:action sync-from {
        description
          "Synchronize the configuration by pulling from the device.";
        tailf:info "Synchronize the config by pulling from the device";
        tailf:actionpoint ncsinternal {
          tailf:internal;
        }
        input {
          container dry-run {
            presence "";
            leaf outformat {
              type outformat2;
              description
                "Report what would be done towards CDB, without
                 actually doing anything.";
            }
          }
          uses wait-for-lock;
        }
        output {
          uses sync-result;
        }
      }

You may notice additional data points in there, but it follows the same general pattern:

tailf:action NAME-OF-ACTION
{
  tailf:actionpoint NAME-WHERE-CODE-LINKS-TO-IT
  input
  {
    YANG-INPUT-NODES
  }
  output
  {
    YANG-OUTPUT-NODES
  }
}

Basically what is going on there–and this will be more evident when you see a live example with everything working–is the Yang model tells the NSO application, “Hey! I have a custom set of code I want to execute, and here is the name I want the action to be called in the NSO application, and here is the Yang model constraints on the input and output”.

NSO Action Example

The easiest way to get started with an NSO action is to use the bash command flag in ncs-make-package to give you a dummy example: ncs-make-package --service-skeleton python --action-example ntc-action-example

This command should be run in your nso-run/packages folder, or wherever you keep your other packages and NEDs. It will create the following folders and files:

packages$ tree ntc-action-example/
ntc-action-example/
├── README
├── package-meta-data.xml
├── python
│   └── ntc-action-example
│       ├── __init__.py
│       └── main.py
├── src
│   ├── Makefile
│   └── yang
│       └── ntc-action-example.yang
├── templates
└── test
    ├── Makefile
    └── internal
        ├── Makefile
        └── lux
            ├── Makefile
            ├── action
            │   ├── Makefile
            │   └── run.lux
            └── service
                ├── Makefile
                ├── dummy-device.xml
                ├── dummy-service.xml
                ├── pyvm.xml
                └── run.lux

10 directories, 16 files

The most relevant ones for this example will be the Yang file: ntc-action-example/src/yang/ntc-action-example.yang and the Python file: ntc-action-example/python/ntc_action_example/main.py.

Trimming Down the Yang file

First, by default this is what the Yang file will look like:

module ntc-action-example {

  namespace "http://example.com/ntc-action-example";
  prefix ntc-action-example;

  import ietf-inet-types {
    prefix inet;
  }
  import tailf-common {
    prefix tailf;
  }
  import tailf-ncs {
    prefix ncs;
  }

  description
    "Bla bla...";

  revision 2016-01-01 {
    description
      "Initial revision.";
  }

  container action {
    tailf:action double {
      tailf:actionpoint ntc-action-example-action;
      input {
        leaf number {
          type uint8;
        }
      }
      output {
        leaf result {
          type uint16;
        }
      }
    }
  }
  list ntc-action-example {
    description "This is an RFS skeleton service";

    key name;
    leaf name {
      tailf:info "Unique service id";
      tailf:cli-allow-range;
      type string;
    }

    uses ncs:service-data;
    ncs:servicepoint ntc-action-example-servicepoint;

    // may replace this with other ways of refering to the devices.
    leaf-list device {
      type leafref {
        path "/ncs:devices/ncs:device/ncs:name";
      }
    }

    // replace with your own stuff here
    leaf dummy {
      type inet:ipv4-address;
    }
  }
}

Since I used the service skeleton bash flag, we see the dummy list ntc-action-example and since I did an action example it added the container action part in the Yang file. In this example, I am not going to use the service mechanics, so I will remove it and other unused part of the Yang file just to keep it simple, so the new yang file is:

module ntc-action-example {

  namespace "http://networktocode.com/ntc-action-example";
  prefix ntc-action-example;

  import tailf-common {
    prefix tailf;
  }
  import tailf-ncs {
    prefix ncs;
  }

  description
    "An action example";

  revision 2019-09-12 {
    description
      "Giving some details for an action example.";
  }

  container action {
    tailf:action double {
      tailf:actionpoint ntc-action-example-action;
      input {
        leaf number {
          type uint8;
        }
      }
      output {
        leaf result {
          type uint16;
        }
      }
    }
  }
}

Trimming down the Python File

By default NSO creates the following Python file for all the action and service tie ins:

# -*- mode: python; python-indent: 4 -*-
import ncs
from ncs.application import Service
from ncs.dp import Action


# ---------------
# ACTIONS EXAMPLE
# ---------------
class DoubleAction(Action):
    @Action.action
    def cb_action(self, uinfo, name, kp, input, output, trans):
        self.log.info('action name: ', name)
        self.log.info('action input.number: ', input.number)

        # Updating the output data structure will result in a response
        # being returned to the caller.
        output.result = input.number * 2


# ------------------------
# SERVICE CALLBACK EXAMPLE
# ------------------------
class ServiceCallbacks(Service):

    # The create() callback is invoked inside NCS FASTMAP and
    # must always exist.
    @Service.create
    def cb_create(self, tctx, root, service, proplist):
        self.log.info('Service create(service=', service._path, ')')


    # The pre_modification() and post_modification() callbacks are optional,
    # and are invoked outside FASTMAP. pre_modification() is invoked before
    # create, update, or delete of the service, as indicated by the enum
    # ncs_service_operation op parameter. Conversely
    # post_modification() is invoked after create, update, or delete
    # of the service. These functions can be useful e.g. for
    # allocations that should be stored and existing also when the
    # service instance is removed.

    # @Service.pre_lock_create
    # def cb_pre_lock_create(self, tctx, root, service, proplist):
    #     self.log.info('Service plcreate(service=', service._path, ')')

    # @Service.pre_modification
    # def cb_pre_modification(self, tctx, op, kp, root, proplist):
    #     self.log.info('Service premod(service=', kp, ')')

    # @Service.post_modification
    # def cb_post_modification(self, tctx, op, kp, root, proplist):
    #     self.log.info('Service premod(service=', kp, ')')


# ---------------------------------------------
# COMPONENT THREAD THAT WILL BE STARTED BY NCS.
# ---------------------------------------------
class Main(ncs.application.Application):
    def setup(self):
        # The application class sets up logging for us. It is accessible
        # through 'self.log' and is a ncs.log.Log instance.
        self.log.info('Main RUNNING')

        # Service callbacks require a registration for a 'service point',
        # as specified in the corresponding data model.
        #
        self.register_service('ntc-action-example-servicepoint', ServiceCallbacks)

        # When using actions, this is how we register them:
        #
        self.register_action('ntc-action-example-action', DoubleAction)

        # If we registered any callback(s) above, the Application class
        # took care of creating a daemon (related to the service/action point).

        # When this setup method is finished, all registrations are
        # considered done and the application is 'started'.

    def teardown(self):
        # When the application is finished (which would happen if NCS went
        # down, packages were reloaded or some error occurred) this teardown
        # method will be called.

        self.log.info('Main FINISHED')

Since I am focusing just on the action, I can reduce the Python file to simply this:

# -*- mode: python; python-indent: 4 -*-
import ncs
from ncs.dp import Action


# ---------------
# ACTIONS EXAMPLE
# ---------------
class DoubleAction(Action):
    @Action.action
    def cb_action(self, uinfo, name, kp, input, output, trans):
        self.log.info('action name: ', name)
        self.log.info('action input.number: ', input.number)

        # Updating the output data structure will result in a response
        # being returned to the caller.
        output.result = input.number * 2


# ---------------------------------------------
# COMPONENT THREAD THAT WILL BE STARTED BY NCS.
# ---------------------------------------------
class Main(ncs.application.Application):
    def setup(self):
        # The application class sets up logging for us. It is accessible
        # through 'self.log' and is a ncs.log.Log instance.
        self.log.info('Main RUNNING')

        # When using actions, this is how we register them:
        #
        self.register_action('ntc-action-example-action', DoubleAction)

        # If we registered any callback(s) above, the Application class
        # took care of creating a daemon (related to the service/action point).

        # When this setup method is finished, all registrations are
        # considered done and the application is 'started'.

    def teardown(self):
        # When the application is finished (which would happen if NCS went
        # down, packages were reloaded or some error occurred) this teardown
        # method will be called.

        self.log.info('Main FINISHED')

Loading in the Package and Using the Action

As with any package, the Yang module needs to be compiled and the NSO packages need to be reloaded:

packages$ cd ntc-action-example/
ntc-action-example$ cd src
src$ ls
Makefile	yang
src$ make
mkdir -p ../load-dir
mkdir -p java/src
/Users/jabelk/ncs-all/nso-5-install/bin/ncsc  `ls ntc-action-example-ann.yang  > /dev/null 2>&1 && echo "-a ntc-action-example-ann.yang"` \
              -c -o ../load-dir/ntc-action-example.fxs yang/ntc-action-example.yang
packages$ ncs_cli -C -u admin

admin connected from 127.0.0.1 using console on ntc-jasonbelk-macbook-pro.local
admin@ncs# packages reload force

>>> System upgrade is starting.
>>> Sessions in configure mode must exit to operational mode.
>>> No configuration changes can be performed until upgrade has completed.
>>> System upgrade has completed successfully.
reload-result {
    package ntc-action-example
    result true
}
admin@ncs# conf
Entering configuration mode terminal
admin@ncs(config)# action double ?
Possible completions:
  number  <cr>
admin@ncs(config)# action double number 22
result 44
admin@ncs(config)# action double QQ
---------------------------------^
syntax error: expecting
  number -
admin@ncs(config)# 

We can see the ntc-action-example shows up in our packages, and it executes the Python, doubling whatever integer we give it. Since it has a Yang model enforcing the inputs, we are unable to give it the invalid QQ value, thus protecting the Python code from executing a doubling action on a string.

How did that work?

From the Yang side, the key connecting statement to note is tailf:actionpoint ntc-action-example-action;, and also the names of the leafs used (in this case just number under input).

Then in the Python file note at the bottom of the Python code, under the Main class in the setup function: self.register_action('ntc-action-example-action', DoubleAction), is registering the action ntc-action-example-action to be associated with the Python class defined in that file DoubleAction.

From the Python DoubleAction Class the key lines to note are:

        self.log.info('action name: ', name)
        self.log.info('action input.number: ', input.number)
        output.result = input.number * 2

Where we use the syntax input.LEAFNAME, in this case input.number to access the value passed in by the action input, and then store the result in the output.result leaf.

Modifying the NSO Custom Action

Let’s make a custom action which gives us random chuck norris jokes! This is inspired by Hank Preston’s demos using Norris to demo REST APIs.

Basically, I am taking the DoubleAction example, and tweaking the class names, editing the leafs a bit (and namespace to NTC!), and adding in a Python requests call in the action. The reason I am showing requests is because something you might want to have available in an NSO package is a requests call to some other application (like Netbox, ServiceNow, etc) to grab some data and then use it to make a decision, or store it in the NSO CDB.

The Yang File

In this Yang file we have an input of number-of-jokes which is expecting an integer of how many jokes we want, and then a result leaf which is the string of all the jokes we got.

module ntc-action-example {

  namespace "http://networktocode.com/ntc-action-example";
  prefix ntc-action-example;

  import tailf-common {
    prefix tailf;
  }
  import tailf-ncs {
    prefix ncs;
  }

  description
    "An action example";

  revision 2019-09-12 {
    description
      "Giving some details for an action example.";
  }

  container action {
    tailf:action random-norris-joke {
      tailf:actionpoint ntc-action-example-action;
      input {
        leaf number-of-jokes {
          type uint8;
        }
      }
      output {
        leaf result {
          type string;
        }
      }
    }
  }
}

The Python File

In this updated main.py, we added an import for requests, changed the DoubleAction in the class name and in the Mainsetup function. The Python code will take the integer input, call the API, extract the data from the nested data structure given from the API, and then store it in the result leaf with some newline padding for readability.

# -*- mode: python; python-indent: 4 -*-
import ncs
from ncs.dp import Action
import requests
class NorrisAction(Action):
    @Action.action
    def cb_action(self, uinfo, name, kp, input, output, trans):
        self.log.info('action name: ', name)
        self.log.info('action input.number: ', input.number_of_jokes)
        url = "http://api.icndb.com/jokes/random/"+str(input.number_of_jokes)
        resp = requests.get(url)
        data = resp.json()
        jokes = data["value"]
        joke_response = "\n\n"
        for joke in jokes:
            joke_response = joke_response + joke["joke"]
            joke_response = joke_response + "\n\n"
        output.result = joke_response
class Main(ncs.application.Application):
    def setup(self):
        self.log.info('Main RUNNING')
        self.register_action('ntc-action-example-action', NorrisAction)
    def teardown(self):
        self.log.info('Main FINISHED')

Seeing is Believing

admin@ncs# packages reload
reload-result {
    package ntc-action-example
    result true
}
admin@ncs# conf
Entering configuration mode terminal
admin@ncs(config)# action random-norris-joke number-of-jokes 2
result

Chuck Norris has banned rainbows from the state of North Dakota.

When you play Monopoly with Chuck Norris, you do not pass go, and you do not collect two hundred dollars. You will be lucky if you make it out alive.


admin@ncs(config)# action random-norris-joke number-of-jokes 6
result

Chuck Norris once roundhouse kicked someone so hard that his foot broke the speed of light, went back in time, and killed Amelia Earhart while she was flying over the Pacific Ocean.

Chuck Norris doesnt shave; he kicks himself in the face. The only thing that can cut Chuck Norris is Chuck Norris.

Chuck Norris is the only man to ever defeat a brick wall in a game of tennis.

Chuck Norris doesn't throw up if he drinks too much. Chuck Norris throws down!

Newton's Third Law is wrong: Although it states that for each action, there is an equal and opposite reaction, there is no force equal in reaction to a Chuck Norris roundhouse kick.

Chuck Norris causes the Windows Blue Screen of Death.


admin@ncs(config)#

Conclusion

One other thing to note, is that NSO actions are incredibly powerful since they also can access the NSO CDB. Since the NSO CDB has a parsed representation of the snapshot of the entire network device inventory, this means you can leverage any data easily using the NSO Python ncs library to apply CRUD operations on any data within the CDB.

If you need a primer on the NSO Python API, check out my tutorial published on DevNet here.

The final package code can be viewed here.

-JB



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!