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:
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:actionpointncsinternal{tailf:internal;}input{containerdry-run{presence"";leafoutformat{typeoutformat2;description"Report what would be done towards CDB, withoutactuallydoinganything.";}}useswait-for-lock;}output{usessync-result;}}
You may notice additional data points in there, but it follows the same general pattern:
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:
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:
modulentc-action-example{namespace"http://example.com/ntc-action-example";prefixntc-action-example;importietf-inet-types{prefixinet;}importtailf-common{prefixtailf;}importtailf-ncs{prefixncs;}description"Bla bla...";revision 2016-01-01 {description "Initialrevision.";}containeraction{tailf:actiondouble {tailf:actionpointntc-action-example-action;input {leafnumber {typeuint8;} }output{leafresult {typeuint16;} } } }listntc-action-example{description "ThisisanRFSskeletonservice";keyname;leafname {tailf:info "Uniqueserviceid";tailf:cli-allow-range;typestring;}usesncs:service-data;ncs:servicepointntc-action-example-servicepoint;// may replace this with other ways of refering to the devices.leaf-listdevice{typeleafref {path "/ncs:devices/ncs:device/ncs:name";} }// replace with your own stuff hereleafdummy{typeinet: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:
By default NSO creates the following Python file for all the action and service tie ins:
# -*-mode: python; python-indent:4-*-import ncsfrom ncs.application import Servicefrom 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 ncsfrom 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 srcsrc$ lsMakefile yangsrc$ makemkdir -p ../load-dirmkdir -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.yangpackages$ ncs_cli -C -u adminadmin connected from 127.0.0.1using console on ntc-jasonbelk-macbook-pro.localadmin@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 {packagentc-action-exampleresulttrue}admin@ncs# confEntering configuration mode terminaladmin@ncs(config)# action double ?Possible completions: number <cr>admin@ncs(config)# action double number 22result 44admin@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:
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.
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.
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.
admin@ncs# packages reloadreload-result {packagentc-action-exampleresulttrue}admin@ncs# confEntering configuration mode terminaladmin@ncs(config)# action random-norris-joke number-of-jokes 2resultChuck 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 6resultChuck 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.
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.
We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept”, you consent to the use of ALL the cookies. In case of sale of your personal information, you may opt out by using the link Do not sell my personal information. Privacy | Cookies
This website uses cookies to improve your experience while you navigate through the website. Out of these cookies, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may have an effect on your browsing experience.
Necessary cookies are absolutely essential for the website to function properly. These cookies ensure basic functionalities and security features of the website, anonymously.
Cookie
Duration
Description
__hssc
30 minutes
HubSpot sets this cookie to keep track of sessions and to determine if HubSpot should increment the session number and timestamps in the __hstc cookie.
__hssrc
session
This cookie is set by Hubspot whenever it changes the session cookie. The __hssrc cookie set to 1 indicates that the user has restarted the browser, and if the cookie does not exist, it is assumed to be a new session.
cookielawinfo-checkbox-advertisement
1 year
Set by the GDPR Cookie Consent plugin, this cookie records the user consent for the cookies in the "Advertisement" category.
cookielawinfo-checkbox-analytics
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics".
cookielawinfo-checkbox-functional
11 months
The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional".
cookielawinfo-checkbox-necessary
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary".
cookielawinfo-checkbox-others
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other.
cookielawinfo-checkbox-performance
11 months
This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance".
CookieLawInfoConsent
1 year
CookieYes sets this cookie to record the default button state of the corresponding category and the status of CCPA. It works only in coordination with the primary cookie.
viewed_cookie_policy
11 months
The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data.
Functional cookies help to perform certain functionalities like sharing the content of the website on social media platforms, collect feedbacks, and other third-party features.
Cookie
Duration
Description
__cf_bm
30 minutes
Cloudflare set the cookie to support Cloudflare Bot Management.
li_gc
5 months 27 days
Linkedin set this cookie for storing visitor's consent regarding using cookies for non-essential purposes.
lidc
1 day
LinkedIn sets the lidc cookie to facilitate data center selection.
UserMatchHistory
1 month
LinkedIn sets this cookie for LinkedIn Ads ID syncing.
Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.
Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics the number of visitors, bounce rate, traffic source, etc.
Cookie
Duration
Description
__hstc
5 months 27 days
Hubspot set this main cookie for tracking visitors. It contains the domain, initial timestamp (first visit), last timestamp (last visit), current timestamp (this visit), and session number (increments for each subsequent session).
_ga
1 year 1 month 4 days
Google Analytics sets this cookie to calculate visitor, session and campaign data and track site usage for the site's analytics report. The cookie stores information anonymously and assigns a randomly generated number to recognise unique visitors.
_gat_gtag_UA_*
1 minute
Google Analytics sets this cookie to store a unique user ID.
_gid
1 day
Google Analytics sets this cookie to store information on how visitors use a website while also creating an analytics report of the website's performance. Some of the collected data includes the number of visitors, their source, and the pages they visit anonymously.
AnalyticsSyncHistory
1 month
Linkedin set this cookie to store information about the time a sync took place with the lms_analytics cookie.
CONSENT
2 years
YouTube sets this cookie via embedded YouTube videos and registers anonymous statistical data.
hubspotutk
5 months 27 days
HubSpot sets this cookie to keep track of the visitors to the website. This cookie is passed to HubSpot on form submission and used when deduplicating contacts.
ln_or
1 day
Linkedin sets this cookie to registers statistical data on users' behaviour on the website for internal analytics.
Advertisement cookies are used to provide visitors with relevant ads and marketing campaigns. These cookies track visitors across websites and collect information to provide customized ads.
Cookie
Duration
Description
bcookie
1 year
LinkedIn sets this cookie from LinkedIn share buttons and ad tags to recognize browser IDs.
bscookie
1 year
LinkedIn sets this cookie to store performed actions on the website.
li_sugr
3 months
LinkedIn sets this cookie to collect user behaviour data to optimise the website and make advertisements on the website more relevant.
VISITOR_INFO1_LIVE
5 months 27 days
YouTube sets this cookie to measure bandwidth, determining whether the user gets the new or old player interface.
YSC
session
Youtube sets this cookie to track the views of embedded videos on Youtube pages.
yt-remote-connected-devices
never
YouTube sets this cookie to store the user's video preferences using embedded YouTube videos.
yt-remote-device-id
never
YouTube sets this cookie to store the user's video preferences using embedded YouTube videos.
yt.innertube::nextId
never
YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen.
yt.innertube::requests
never
YouTube sets this cookie to register a unique ID to store data on what videos from YouTube the user has seen.