LAB 2: Create SVI Service Using pre_modification Service Callback
LAB 2: Create SVI Service Using pre_modification Service Callback
In this lab, you will use advanced YANG modeling techniques and implement a pre_modification
service callback.
The service will need a YANG data model, XML templates, and some Python code. Your new service package will be based on the existing model, which is provided to you in a separate folder.
Because your service is intended for switches, you will put a constraint in your data model. The YANG modeling language allows creating constraints with a must statement.
The pre_modification
callback will be implemented in Python and will provide the next available vlan-id to the upgraded svi service. The callback will be executed when you commit the configuration. Therefore, your service can keep track of every vlan-id in use without the need for a separate database.
Task 1: Upgrade the YANG Service Model
In this task, you will upgrade the svi YANG model. Files from the old service have been provided for you in the $HOME/packages/svi
directory.
The must
YANG statement requires an argument in the form of a string. The string must be a valid XPath expression that evaluates to true for data to be valid.
The feature interface-vlan
must be enabled if the ned-id
is cisco-nx
. This configuration will be a part of the service template.
Note: The final solutions for all labs, including this one, are located in the ~/solutions
directory. You can use them for copying longer pieces of code and as a reference point for troubleshooting your packages.
Step 1: Connect to the VM server by clicking the NSO icon.
Step 2: Open the terminal window; click the Terminal icon on the bottom bar.
rst@rst:~$
Step 3: Create and change directory to nso300
inside ~/NSO
directory.
rst@rst:~$ cd NSO rst@rst:~/NSO$ mkdir nso300 rst@rst:~/NSO$ cd nso300 rst@rst:~/NSO/nso300$
Step 4: Enter the development NSO Docker container shell with the make dev-shell command and enter the /src/packages
directory.
rst@rst:~/NSO/nso300$ make dev-shell docker run -it -v $(pwd):/src nso300.gitlab.local/cisco-nso-dev:5.3 root@46a3fd8d7374:/# cd src/packages/ root@46a3fd8d7374:/src/packages#
Step 5: Create a new svi package with the ncs-make-package
command.
root@46a3fd8d7374:/src/packages# ncs-make-package --service-skeleton python-and-template --component-class svi.Svi svi
Step 6: Change the ownership of the package and exit the Docker container.
root@46a3fd8d7374:/src/packages# chown -Rv 1000:1000 svi changed ownership of 'svi/README' from root:root to 1000:1000 changed ownership of 'svi/templates/svi-template.xml' from root:root to 1000:1000 changed ownership of 'svi/templates' from root:root to 1000:1000 changed ownership of 'svi/python/svi/__init__.py' from root:root to 1000:1000 changed ownership of 'svi/python/svi/svi.py' from root:root to 1000:1000 changed ownership of 'svi/python/svi' from root:root to 1000:1000 changed ownership of 'svi/python' from root:root to 1000:1000 changed ownership of 'svi/src/yang/svi.yang' from root:root to 1000:1000 changed ownership of 'svi/src/yang' from root:root to 1000:1000 changed ownership of 'svi/src/Makefile' from root:root to 1000:1000 changed ownership of 'svi/src' from root:root to 1000:1000 changed ownership of 'svi/test/internal/lux/service/dummy-device.xml' from root:root to 1000:1000 changed ownership of 'svi/test/internal/lux/service/dummy-service.xml' from root:root to 1000:1000 changed ownership of 'svi/test/internal/lux/service/pyvm.xml' from root:root to 1000:1000 changed ownership of 'svi/test/internal/lux/service/run.lux' from root:root to 1000:1000 changed ownership of 'svi/test/internal/lux/service/Makefile' from root:root to 1000:1000 changed ownership of 'svi/test/internal/lux/service' from root:root to 1000:1000 changed ownership of 'svi/test/internal/lux/Makefile' from root:root to 1000:1000 changed ownership of 'svi/test/internal/lux' from root:root to 1000:1000 changed ownership of 'svi/test/internal/Makefile' from root:root to 1000:1000 changed ownership of 'svi/test/internal' from root:root to 1000:1000 changed ownership of 'svi/test/Makefile' from root:root to 1000:1000 changed ownership of 'svi/test' from root:root to 1000:1000 changed ownership of 'svi/package-meta-data.xml' from root:root to 1000:1000 changed ownership of 'svi' from root:root to 1000:1000 root@46a3fd8d7374:/src/packages# root@46a3fd8d7374:/src/packages# exit logout rst@rst:~/NSO/nso300$
Step 7: List the contents of the package.
The package you created exists on your Student-VM machine and is mounted as a volume to the development Docker container.
rst@rst:~/NSO/nso300$ ls packages/svi package-meta-data.xml python README src templates test rst@rst:~/NSO/nso300$ code packages/svi/package-meta-data.xml
Step 8: Open and study the package-meta-data.xml
of your new service package.
rst@rst:~/NSO/nso300$ code packages/svi/package-meta-data.xml
This is how the package-meta-data.xml
file appears initially.
<ncs-package xmlns="http://tail-f.com/ns/ncs-packages"> <name>svi</name> <package-version>1.0</package-version> <description>Generated Python package</description> <ncs-min-version>5.3</ncs-min-version> <component> <name>svi</name> <application> <python-class-name>svi.svi.Svi</python-class-name> </application> </component> </ncs-package>
Step 9: Change the package description and component name to better represent service purpose.
<ncs-package xmlns="http://tail-f.com/ns/ncs-packages"> <name>svi</name> <package-version>1.0</package-version> <description>SVI Python and Template Service</description> <ncs-min-version>5.3</ncs-min-version> <component> <name>Switch Virtual Interface</name> <application> <python-class-name>svi.svi.Svi</python-class-name> </application> </component> </ncs-package>
Step 10: Save the file and exit the file editor.
Step 11: Delete the generated XML template file.
rst@rst:~/NSO/nso300$ rm packages/svi/templates/svi-template.xml
Step 12: Copy the existing templates from ../packages/svi/templates
to packages/svi/templates
and verify that you have the correct files.
The files are separated by functionality. One template is for interface addressing, the other for vlan database and switch port configuration.
rst@rst:~/NSO/nso300$ cp ../packages/svi/templates/svi-* packages/svi/templates/ rst@rst:~/NSO/nso300$ ls packages/svi/templates/ svi-intf-template.xml svi-vlan-template.xml
Step 13: Open the svi-intf-template.xml
template and add the configuration for enabling the feature interface-vlan
.
rst@rst:~/NSO/nso300$ code packages/svi/templates/svi-intf-template.xml
Step 14: Save the file.
Step 15: Your svi-intf-template-xml
should now appear as follows:
<config-template xmlns="http://tail-f.com/ns/config/1.0"> <devices xmlns="http://tail-f.com/ns/ncs"> <device> <name>{/device/name}</name> <config> <!-- IOS --> <interface xmlns="urn:ios"> <?if {string(name)=$SVI-DEVICE}?> <Vlan> <name>{$VLAN-ID}</name> <ip> <address> <primary> <address>{$IP-ADDR}</address> <mask>{$NETMASK}</mask> </primary> </address> </ip> </Vlan> <?end?> </interface> <!-- NX-OS --> <feature xmlns="http://tail-f.com/ned/cisco-nx"> <interface-vlan/> </feature> <interface xmlns="http://tail-f.com/ned/cisco-nx"> <?if {string(name)=$SVI-DEVICE}?> <Vlan> <name>{$VLAN-ID}</name> <ip> <address> <ipaddr>{$IP-PREFIX}</ipaddr> </address> </ip> </Vlan> <?end?> </interface> </config> </device> </devices> </config-template>
This feature is already enabled if you use an older version of the cisco-nx
NED
Step 16: Copy the existing service YANG model to your new service package from the ../packages/svi
folder.
This data model has already been created and serves you as base for your new service.
rst@rst:~/NSO/nso300$ cp ../packages/svi/src/yang/svi.yang packages/svi/src/yang/svi.yang
Step 17: Open the service YANG model. This is how the YANG module should initially appear.
Try to reuse old packages as much as you can. Reusing saves you time, but it also provides you with a well-tested and production-proven source.
rst@rst:~/NSO/nso300$ code packages/svi/src/yang/svi.yang module svi { namespace "http://example.com/svi"; prefix svi; import ietf-inet-types { prefix inet; } import tailf-common { prefix tailf; } import tailf-ncs { prefix ncs; } description "YANG model for SVI service"; revision 2020-07-21 { description "Initial revision."; } augment "/ncs:services" { list svi { key "name"; leaf name { tailf:info "Unique service id"; tailf:cli-allow-range; type string; } uses ncs:service-data; ncs:servicepoint "svi-servicepoint"; leaf vlan-id { tailf:info "Unique VLAN ID"; mandatory true; type uint32 { range "1..4096"; } } list device { tailf:info "L3 switch"; key "name"; leaf name { tailf:info "Device name"; type leafref { path "/ncs:devices/ncs:device/ncs:name"; } } leaf ip-prefix { tailf:info "Unique IPv4 prefix for VLAN"; type inet:ip-prefix; } list interface { tailf:info "Ethernet interface"; key "intf-type intf-id"; leaf intf-type { tailf:info "Ethernet interface type"; type enumeration { enum Ethernet; enum FastEthernet; enum GigabitEthernet; } } leaf intf-id { tailf:info "Ethernet interface ID"; type string; } } } } } }
Step 18: Restrict device selection to switches only.
This constraint makes sure that your service can only be used on devices with specific names. The device name comes from the device as registered in NSO and is not to beconfused with the device hostname configuration setting.
module svi { namespace "http://example.com/svi"; prefix svi; import ietf-inet-types { prefix inet; } import tailf-common { prefix tailf; } import tailf-ncs { prefix ncs; } description "YANG model for SVI service"; revision 2020-07-21 { description "Initial revision."; } augment "/ncs:services" { list svi { key "name"; leaf name { tailf:info "Unique service id"; tailf:cli-allow-range; type string; } uses ncs:service-data; ncs:servicepoint "svi-servicepoint"; leaf vlan-id { tailf:info "Unique VLAN ID"; mandatory true; type uint32 { range "1..4096"; } } list device { tailf:info "L3 switch"; key "name"; leaf name { tailf:info "Device name"; type leafref { path "/ncs:devices/ncs:device/ncs:name"; } must "starts-with(current(),'SW')" { error-message "Only SW devices can be selected."; } } leaf ip-prefix { tailf:info "Unique IPv4 prefix for VLAN"; type inet:ip-prefix; } list interface { tailf:info "Ethernet interface"; key "intf-type intf-id"; leaf intf-type { tailf:info "Ethernet interface type"; type enumeration { enum Ethernet; enum FastEthernet; enum GigabitEthernet; } } leaf intf-id { tailf:info "Ethernet interface ID"; type string; } } } } } }
Step 19: Create a vlan-id-cnt
leaf at the end of the YANG module.
This leaf serves as a counter and is updated only by your Python code inside the pre_modification callback. It always holds the vlan-id number for your next service instance.
module svi { namespace "http://example.com/svi"; prefix svi; import ietf-inet-types { prefix inet; } import tailf-common { prefix tailf; } import tailf-ncs { prefix ncs; } description "YANG model for SVI service"; revision 2020-07-21 { description "Initial revision."; } augment "/ncs:services" { list svi { key "name"; leaf name { tailf:info "Unique service id"; tailf:cli-allow-range; type string; } uses ncs:service-data; ncs:servicepoint "svi-servicepoint"; leaf vlan-id { tailf:info "Unique VLAN ID"; mandatory true; type uint32 { range "1..4096"; } } list device { tailf:info "L3 switch"; key "name"; leaf name { tailf:info "Device name"; type leafref { path "/ncs:devices/ncs:device/ncs:name"; } must "starts-with(current(),'SW')" { error-message "Only SW devices can be selected."; } } leaf ip-prefix { tailf:info "Unique IPv4 prefix for VLAN"; type inet:ip-prefix; } list interface { tailf:info "Ethernet interface"; key "intf-type intf-id"; leaf intf-type { tailf:info "Ethernet interface type"; type enumeration { enum Ethernet; enum FastEthernet; enum GigabitEthernet; } } leaf intf-id { tailf:info "Ethernet interface ID"; type string; } } } } } augment "/ncs:services" { leaf vlan-id-cnt { description "Provides a unique number used as VLAN identifier"; tailf:hidden "Counter"; type uint32 { range "2..4096"; } default "2"; } } }
Step 20: Look for the leaf node vlan-id
and delete it. This leaf is no longer needed since the vlan-id-cnt
counter is used as source.
leaf vlan-id { tailf:info "Unique VLAN ID"; mandatory true; type uint32 { range "1..4096"; } }
Step 21: Restrict the leaf ip-prefix
to be configured on one of the devices only.
module svi { namespace "http://example.com/svi"; prefix svi; import ietf-inet-types { prefix inet; } import tailf-common { prefix tailf; } import tailf-ncs { prefix ncs; } description "YANG model for SVI service"; revision 2020-07-21 { description "Initial revision."; } augment "/ncs:services" { list svi { key "name"; leaf name { tailf:info "Unique service id"; tailf:cli-allow-range; type string; } uses ncs:service-data; ncs:servicepoint "svi-servicepoint"; list device { tailf:info "L3 switch"; key "name"; leaf name { tailf:info "Device name"; type leafref { path "/ncs:devices/ncs:device/ncs:name"; } must "starts-with(current(),'SW')" { error-message "Only SW devices can be selected."; } } leaf ip-prefix { tailf:info "Unique IPv4 prefix for VLAN. Device with ip-prefix configured will serve as gateway."; type inet:ipv4-prefix; // Only one device can have ip-prefix configured when "count(../../device[name != current()/../name]/ip-prefix)=0"; } list interface { tailf:info "Ethernet interface"; key "intf-type intf-id"; leaf intf-type { tailf:info "Ethernet interface type"; type enumeration { enum Ethernet; enum FastEthernet; enum GigabitEthernet; } } leaf intf-id { tailf:info "Ethernet interface ID"; type string; } } } } } augment "/ncs:services" { leaf vlan-id-cnt { description "Provides a unique number used as VLAN identifier"; tailf:hidden "Counter"; type uint32 { range "2..4096"; } default "2"; } } }
Step 22: Save the file when finished.
You have completed this task when you attain this result: You successfully modified the YANG service model.
Task 2: Implement the Python Mapping Code
In this task, you will implement some Python code for configuration mapping. Files from an existing service have been provided for you in the $HOME/NSO/packages/svi
directory.
NSO will call your pre_modification
method before it calls the cb_create
method. Therefore, you can prepare and format data.
When the pre_modification
returns, your data can be passed to cb_create
through the proplist
object. proplist
is a Python list of tuples, and each tuple consists of two members—a name and a value.
The old service package used the netaddr Python module, which is not a part of the standard library and therefore is not present in any of the containers. You will replace this module with the ipaddress module and make minor adjustments.
Step 1: Copy the existing Python-mapping code.
rst@rst:~/NSO/nso300$ cp ../packages/svi/python/svi/svi.py packages/svi/python/svi/svi.py
Step 2: Open Python mapping code for the svi service. You can use an IDE tool or text editor of your choice.
rst@rst:~/NSO/nso300$ code packages/svi/python/svi/svi.py # -*- mode: python; python-indent: 4 -*- import ncs from ncs.application import Service import netaddr # ------------------------ # 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(f'Service create(service={service._path})') svi = {'vlan-id': "", 'svi-device': "", 'ip-prefix': "", 'ip-addr': "", 'netmask': ""} svi['vlan-id'] = service.vlan_id for device in service.device: self.log.info(f'Entering /device list = {device.name}') if device.ip_prefix: self.log.info(f'SVI device = {device.name}') ip_net = netaddr.IPNetwork(device.ip_prefix) svi['svi-device'] = device.name svi['ip-prefix'] = f'{ip_net[2]}/{ip_net.prefixlen}' svi['ip-addr'] = str(ip_net[1]) svi['netmask'] = str(ip_net.netmask) svi_tvars = ncs.template.Variables() svi_tvars.add('VLAN-ID', svi['vlan-id']) svi_tvars.add('SVI-DEVICE', svi['svi-device']) svi_tvars.add('IP-PREFIX', svi['ip-prefix']) svi_tvars.add('IP-ADDR', svi['ip-addr']) svi_tvars.add('NETMASK', svi['netmask']) svi_template = ncs.template.Template(service) svi_template.apply('svi-intf-template', svi_tvars) vlan_tvars = ncs.template.Variables() vlan_tvars.add('VLAN-ID', svi['vlan-id']) vlan_template = ncs.template.Template(service) vlan_template.apply('svi-vlan-template', vlan_tvars) # 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 Svi(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('svi-servicepoint', ServiceCallbacks) # 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')
Step 3: Delete all the comments to improve readability. You can keep the pre_modification
definition and decorator.
# -*- mode: python; python-indent: 4 -*- import ncs from ncs.application import Service import netaddr # ------------------------ # SERVICE CALLBACK EXAMPLE # ------------------------ class ServiceCallbacks(Service): @Service.create def cb_create(self, tctx, root, service, proplist): self.log.info(f'Service create(service={service._path})') svi = {'vlan-id': "", 'svi-device': "", 'ip-prefix': "", 'ip-addr': "", 'netmask': ""} svi['vlan-id'] = service.vlan_id for device in service.device: self.log.info(f'Entering /device list = {device.name}') if device.ip_prefix: self.log.info(f'SVI device = {device.name}') ip_net = netaddr.IPNetwork(device.ip_prefix) svi['svi-device'] = device.name svi['ip-prefix'] = f'{ip_net[2]}/{ip_net.prefixlen}' svi['ip-addr'] = str(ip_net[1]) svi['netmask'] = str(ip_net.netmask) svi_tvars = ncs.template.Variables() svi_tvars.add('VLAN-ID', svi['vlan-id']) svi_tvars.add('SVI-DEVICE', svi['svi-device']) svi_tvars.add('IP-PREFIX', svi['ip-prefix']) svi_tvars.add('IP-ADDR', svi['ip-addr']) svi_tvars.add('NETMASK', svi['netmask']) svi_template = ncs.template.Template(service) svi_template.apply('svi-intf-template', svi_tvars) vlan_tvars = ncs.template.Variables() vlan_tvars.add('VLAN-ID', svi['vlan-id']) vlan_template = ncs.template.Template(service) vlan_template.apply('svi-vlan-template', vlan_tvars) # @Service.pre_modification # def cb_pre_modification(self, tctx, op, kp, root, proplist): # self.log.info('Service premod(service=', kp, ')') # --------------------------------------------- # COMPONENT THREAD THAT WILL BE STARTED BY NCS. # --------------------------------------------- class Svi(ncs.application.Application): def setup(self): self.log.info('Main RUNNING') self.register_service('svi-servicepoint', ServiceCallbacks) def teardown(self):self.log.info('Main FINISHED')
Step 4: Implement the pre_modification callback method. If this is a service creation, then assign the value vlan-id-cnt
to a local variable vlan_id
, increment the counter by one, and append it to the proplist variable before returning.
# -*- mode: python; python-indent: 4 -*- import ncs from ncs.application import Service import netaddr # ------------------------ # SERVICE CALLBACK EXAMPLE # ------------------------ class ServiceCallbacks(Service): @Service.create def cb_create(self, tctx, root, service, proplist): self.log.info(f"Service create(service={service._path})") svi = { "vlan-id": "", "svi-device": "", "ip-prefix": "", "ip-addr": "", "netmask": "", } svi["vlan-id"] = service.vlan_id for device in service.device: self.log.info(f"Entering /device list = {device.name}") if device.ip_prefix: self.log.info(f"SVI device = {device.name}") ip_net = netaddr.IPNetwork(device.ip_prefix) svi["svi-device"] = device.name svi["ip-prefix"] = f"{ip_net[2]}/{ip_net.prefixlen}" svi["ip-addr"] = str(ip_net[1]) svi["netmask"] = str(ip_net.netmask) svi_tvars = ncs.template.Variables() svi_tvars.add("VLAN-ID", svi["vlan-id"]) svi_tvars.add("SVI-DEVICE", svi["svi-device"]) svi_tvars.add("IP-PREFIX", svi["ip-prefix"]) svi_tvars.add("IP-ADDR", svi["ip-addr"]) svi_tvars.add("NETMASK", svi["netmask"]) svi_template = ncs.template.Template(service) svi_template.apply("svi-intf-template", svi_tvars) vlan_tvars = ncs.template.Variables() vlan_tvars.add("VLAN-ID", svi["vlan-id"]) vlan_template = ncs.template.Template(service) vlan_template.apply("svi-vlan-template", vlan_tvars) @Service.pre_modification def cb_pre_modification(self, tctx, op, kp, root, proplist): self.log.info("Service premod(service=", kp, ")") if op == ncs.dp.NCS_SERVICE_CREATE: self.log.info("Service premod(operation=NCS_SERVICE_CREATE, allocate)") vlan_id = root.services.vlan_id_cnt proplist.append(("vlan-id", str(vlan_id))) self.log.info(f"Service premod(allocated vlan-id: {vlan_id})") root.services.vlan_id_cnt = vlan_id + 1 elif op == ncs.dp.NCS_SERVICE_DELETE: self.log.info("Service premod(operation=NCS_SERVICE_DELETE, skil)") return proplist # --------------------------------------------- # COMPONENT THREAD THAT WILL BE STARTED BY NCS. # --------------------------------------------- class Svi(ncs.application.Application): def setup(self): self.log.info("Main RUNNING") self.register_service("svi-servicepoint", ServiceCallbacks) def teardown(self): self.log.info("Main FINISHED")
Step 5: Remove the following line from the cb_create method because the vlan-id will be passed through the proplist
object.
svi['vlan-id'] = service.vlan_id
Step 6: Update the cb_create code. Retrieve the vlan-id
as returned from the pre_modification
callback inside the proplist object.
# -*- mode: python; python-indent: 4 -*- import ncs from ncs.application import Service import netaddr # ------------------------ # SERVICE CALLBACK EXAMPLE # ------------------------ class ServiceCallbacks(Service): @Service.create def cb_create(self, tctx, root, service, proplist): self.log.info(f"Service create(service={service._path})") svi = { "vlan-id": "", "svi-device": "", "ip-prefix": "", "ip-addr": "", "netmask": "", } # proplist object list(tuple(str,str)) to pass information between invocations. We set # value for vlan_id in cb_pre_modification and use it here in cb_create. svi["vlan-id"] = [x[1] for x in proplist if x[0] == "vlan-id"][0] for device in service.device: self.log.info(f"Entering /device list = {device.name}") if device.ip_prefix: self.log.info(f"SVI device = {device.name}") ip_net = netaddr.IPNetwork(device.ip_prefix) svi["svi-device"] = device.name svi["ip-prefix"] = f"{ip_net[2]}/{ip_net.prefixlen}" svi["ip-addr"] = str(ip_net[1]) svi["netmask"] = str(ip_net.netmask) svi_tvars = ncs.template.Variables() svi_tvars.add("VLAN-ID", svi["vlan-id"]) svi_tvars.add("SVI-DEVICE", svi["svi-device"]) svi_tvars.add("IP-PREFIX", svi["ip-prefix"]) svi_tvars.add("IP-ADDR", svi["ip-addr"]) svi_tvars.add("NETMASK", svi["netmask"]) svi_template = ncs.template.Template(service) svi_template.apply("svi-intf-template", svi_tvars) vlan_tvars = ncs.template.Variables() vlan_tvars.add("VLAN-ID", svi["vlan-id"]) vlan_template = ncs.template.Template(service) vlan_template.apply("svi-vlan-template", vlan_tvars) @Service.pre_modification def cb_pre_modification(self, tctx, op, kp, root, proplist): self.log.info("Service premod(service=", kp, ")") if op == ncs.dp.NCS_SERVICE_CREATE: self.log.info("Service premod(operation=NCS_SERVICE_CREATE, allocate)") vlan_id = root.services.vlan_id_cnt proplist.append(("vlan-id", str(vlan_id))) self.log.info(f"Service premod(allocated vlan-id: {vlan_id})") root.services.vlan_id_cnt = vlan_id + 1 elif op == ncs.dp.NCS_SERVICE_DELETE: self.log.info("Service premod(operation=NCS_SERVICE_DELETE, skil)") return proplist # --------------------------------------------- # COMPONENT THREAD THAT WILL BE STARTED BY NCS. # --------------------------------------------- class Svi(ncs.application.Application): def setup(self): self.log.info("Main RUNNING") self.register_service("svi-servicepoint", ServiceCallbacks) def teardown(self): self.log.info("Main FINISHED")
Step 7: Replace the Python netaddr module with the ipaddress
module, and IPNetwork call with ip_network
. The Python netaddr module is not a part of Python’s standard library and does not exist inside the container.
# -*- mode: python; python-indent: 4 -*- import ncs from ncs.application import Service import ipaddress # ------------------------ # SERVICE CALLBACK EXAMPLE # ------------------------ class ServiceCallbacks(Service): @Service.create def cb_create(self, tctx, root, service, proplist): self.log.info(f"Service create(service={service._path})") svi = { "vlan-id": "", "svi-device": "", "ip-prefix": "", "ip-addr": "", "netmask": "", } # proplist object list(tuple(str,str)) to pass information between invocations. We set # value for vlan_id in cb_pre_modification and use it here in cb_create. svi["vlan-id"] = [x[1] for x in proplist if x[0] == "vlan-id"][0] for device in service.device: self.log.info(f"Entering /device list = {device.name}") if device.ip_prefix: self.log.info(f"SVI device = {device.name}") ip_net = ipaddress.ip_network(device.ip_prefix) svi["svi-device"] = device.name svi["ip-prefix"] = f"{ip_net[2]}/{ip_net.prefixlen}" svi["ip-addr"] = str(ip_net[1]) svi["netmask"] = str(ip_net.netmask) svi_tvars = ncs.template.Variables() svi_tvars.add("VLAN-ID", svi["vlan-id"]) svi_tvars.add("SVI-DEVICE", svi["svi-device"]) svi_tvars.add("IP-PREFIX", svi["ip-prefix"]) svi_tvars.add("IP-ADDR", svi["ip-addr"]) svi_tvars.add("NETMASK", svi["netmask"]) svi_template = ncs.template.Template(service) svi_template.apply("svi-intf-template", svi_tvars) vlan_tvars = ncs.template.Variables() vlan_tvars.add("VLAN-ID", svi["vlan-id"]) vlan_template = ncs.template.Template(service) vlan_template.apply("svi-vlan-template", vlan_tvars) @Service.pre_modification def cb_pre_modification(self, tctx, op, kp, root, proplist): self.log.info("Service premod(service=", kp, ")") if op == ncs.dp.NCS_SERVICE_CREATE: self.log.info("Service premod(operation=NCS_SERVICE_CREATE, allocate)") vlan_id = root.services.vlan_id_cnt proplist.append(("vlan-id", str(vlan_id))) self.log.info(f"Service premod(allocated vlan-id: {vlan_id})") root.services.vlan_id_cnt = vlan_id + 1 elif op == ncs.dp.NCS_SERVICE_DELETE: self.log.info("Service premod(operation=NCS_SERVICE_DELETE, skil)") return proplist # --------------------------------------------- # COMPONENT THREAD THAT WILL BE STARTED BY NCS. # --------------------------------------------- class Svi(ncs.application.Application): def setup(self): self.log.info("Main RUNNING") self.register_service("svi-servicepoint", ServiceCallbacks) def teardown(self): self.log.info("Main FINISHED")
Step 8: Save the file when finished.
You have successfully modified the Python mapping code.
Task 3: Compile and Deploy the Service
In this task, you will compile and deploy the service package created in the previous task. But before doing so, you will create a new target for pylint.
Before data model compilation, your Python code should be checked for errors. You do this action by running pylint on every .py file found in your package.
After a successful pylint run, your data model must be checked and compiled into binary form. This is achieved by running the NSO compiler. For every .yang
file found in your package, the compiler will output a corresponding .fxs
file. These files are compiled versions of your text data models
All this work is abstracted from you in the form of Makefile targets. Running the make testenv-build command will progress to the next target only if there are no errors in the current target.
Only after a successful compilation will the package be complete and ready for deployment. This step is also the last Makefile target.
If there is an error in any of the targets, simply identify the cause, make corrections, and try to build again.
Step 1: Open the Makefile with a text editor of your choice.
rst@rst:~/NSO/nso300$ code packages/svi/src/Makefile
Step 2: Edit the first line at the very top and add pylint at the end.
all: fxs pylint .PHONY: all
Step 3: Scroll down and add a pylint target declaration. Make sure you use TAB for indentation.
NCSCPATH = $(YANGPATH:%=--yangpath %) YANGERPATH = $(YANGPATH:%=--path %) pylint: pylint --disable=R,C --reports=n ../python/svi/*.py || (test $$? -ge 4) fxs: $(DIRS) $(FXS) ### OUTPUT OMITTED ###
Step 4: Save changes and exit the text editor.
Step 5: Compile the package. Use the make testenv-build
command. This command recompiles and reloads or redeploys the packages, used by the Docker NSO containers.
Inspect the output and make sure that no errors are present.
Notice the pylint lines in the output. Linter checks your code for errors and provides a score, which at this point should be of no concern.
rst@rst:~/NSO/nso300$ make testenv-build for NSO in $(docker ps --format '{{.Names}}' --filter label=com.cisco.nso.testenv.name=testenv-nso300-5.3-rst --filter label=com.cisco.nso.testenv.type=nso); do \ echo "-- Rebuilding for NSO: ${NSO}"; \ docker run -it --rm -v /home/rst/NSO/nso300:/src --volumes-from ${NSO} --network=container:${NSO} -e NSO=${NSO} -e PACKAGE_RELOAD= -e SKIP_LINT= -e PKG_FILE=nso300.gitlab.local/nso300/package:5.3-rst nso300.gitlab.local/cisco-nso-dev:5.3 /src/nid/testenv-build; \ done -- Rebuilding for NSO: testenv-nso300-5.3-rst-nso (^package-meta-data.xml$|\.cli$|\.yang$) make: Entering directory '/var/opt/ncs/packages/svi/src' /opt/ncs/ncs-5.3/bin/ncsc `ls svi-ann.yang > /dev/null 2>&1 && echo "-a svi-ann.yang"` \ -c -o ../load-dir/svi.fxs yang/svi.yang pylint --disable=R,C --reports=n ../python/svi/*.py || (test $? -ge 4) ************* Module svi.svi /var/opt/ncs/packages/svi/python/svi/svi.py:17:48: W0212: Access to a protected member _path of a client class (protected-access) ----------------------------------- Your code has been rated at 9.78/10 make: Leaving directory '/var/opt/ncs/packages/svi/src' make: Entering directory '/src/packages/svi' if [ ! -f build-meta-data.xml ]; then \ export PKG_NAME=$(xmlstarlet sel -N x=http://tail-f.com/ns/ncs-packages -t -v '/x:ncs-package/x:name' $(ls package-meta-data.xml src/package-meta-data.xml.in 2>/dev/null | head -n 1)); \ export PKG_VERSION=$(xmlstarlet sel -N x=http://tail-f.com/ns/ncs-packages -t -v '/x:ncs-package/x:package-version' $(ls package-meta-data.xml src/package-meta-data.xml.in 2>/dev/null | head -n 1)); \ eval "cat <<< \"$(</src/nid/build-meta-data.xml)\"" > /var/opt/ncs/packages/svi//build-meta-data.xml; fi fatal: not a git repository (or any parent up to mount point /) Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set). fatal: not a git repository (or any parent up to mount point /) Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set). make: Leaving directory '/src/packages/svi' make: Entering directory '/var/opt/ncs/packages/hostname/src' make: Nothing to be done for 'all'. make: Leaving directory '/var/opt/ncs/packages/hostname/src' make: Entering directory '/src/packages/hostname' if [ ! -f build-meta-data.xml ]; then \ export PKG_NAME=$(xmlstarlet sel -N x=http://tail-f.com/ns/ncs-packages -t -v '/x:ncs-package/x:name' $(ls package-meta-data.xml src/package-meta-data.xml.in 2>/dev/null | head -n 1)); \ export PKG_VERSION=$(xmlstarlet sel -N x=http://tail-f.com/ns/ncs-packages -t -v '/x:ncs-package/x:package-version' $(ls package-meta-data.xml src/package-meta-data.xml.in 2>/dev/null | head -n 1)); \ eval "cat <<< \"$(</src/nid/build-meta-data.xml)\"" > /var/opt/ncs/packages/hostname//build-meta-data.xml; fi fatal: not a git repository (or any parent up to mount point /) Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set). fatal: not a git repository (or any parent up to mount point /) Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set). make: Leaving directory '/src/packages/hostname' -- Reloading packages for NSO testenv-nso300-5.3-rst-nso >>> 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 cisco-asa-cli-6.7 result true } reload-result { package cisco-ios-cli-6.42 result true } reload-result { package cisco-iosxr-cli-7.18 result true } reload-result { package cisco-nx-cli-5.13 result true } reload-result { package hostname result true } reload-result { package svi result true } rst@rst:~/NSO/nso300$
Step 6: Connect to the NSO CLI and switch to the Cisco mode.
rst@rst:~/NSO/nso300$ make testenv-cli docker exec -it testenv-nso300-5.3-rst-nso bash -lc 'ncs_cli -u admin' admin connected from 127.0.0.1 using console on 83dc28b9733d admin@ncs> admin@ncs> switch cli admin@ncs#
Step 7: Configure the service instance.
admin@ncs# config Entering configuration mode terminal admin@ncs(config)# services svi ACME admin@ncs(config-svi-ACME)# device SW31 admin@ncs(config-device-SW31)# ip-prefix 10.10.0.0/16 admin@ncs(config-device-SW31)# interface GigabitEthernet 0/1 admin@ncs(config-interface-GigabitEthernet/0/1)# exit admin@ncs(config-device-SW31)# exit admin@ncs(config-svi-ACME)# device SW32 admin@ncs(config-device-SW32)# interface Ethernet 0/2 admin@ncs(config-interface-Ethernet/0/2)# top admin@ncs(config)# show configuration services svi ACME device SW31 ip-prefix 10.10.0.0/16 interface GigabitEthernet 0/1 ! ! device SW32 interface Ethernet 0/2 ! ! ! admin@ncs(config)#
Step 8: Review the configuration that will be created using the commit dry-run
command.
admin@ncs(config)# commit dry-run cli { local-node { data devices { device SW31 { config { vlan { + vlan-list 2 { + } } interface { GigabitEthernet 0/1 { + switchport { + mode { + access { + } + } + access { + vlan 2; + } + } ip { no-address { - address false; } } } + Vlan 2 { + ip { + address { + primary { + address 10.10.0.1; + mask 255.255.0.0; + } + } + } + } } } } device SW32 { config { feature { + interface-vlan; } vlan { + vlan-list 2 { + } } interface { + Ethernet 0/2 { + switchport { + mode access; + access { + vlan 2; + } + } + } } } } } services { + svi ACME { + device SW31 { + ip-prefix 10.10.0.0/16; + interface GigabitEthernet 0/1; + } + device SW32 { + interface Ethernet 0/2; + } + } } } }
Step 9: Commit the configuration to deploy the service. Your service should be configured with vlan-id 2
.
admin@ncs(config)# commit Commit complete.
Step 10: Exit the privileged mode and exit the NSO CLI.
admin@ncs(config)# exit admin@ncs# exit rst@rst:~/NSO/nso300$
You have successfully deployed a service instance.