September 16, 2019

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 Main setup 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

Does this all sound amazing? Want to know more about how Network to Code can help you do this, reach out to our sales team. If you want to help make this a reality for our clients, check out our careers page.