- About
- NSO 5.7 Getting Started Guide
- NSO 5.7 User Guide
- NSO Installation Guide
- NSO 5.7 Administration Guide
- NSO 5.7 Northbound APIs
- NSO 5.7 Development Guide
- Preface
- The Configuration Database and YANG
- Basic Automation with Python
- Creating a Service
- Applications in NSO
- The NSO Java VM
- The NSO Python VM
- Embedded Erlang applications
- The YANG Data Modeling Language
- Using CDB
- Java API Overview
- Python API Overview
- NSO Packages
- Package Development
- Developing NSO Services
- Templates
- NED Upgrades and Migration
- Developing Alarm Applications
- SNMP Notification Receiver
- The web server
- Kicker
- Scheduler
- Progress Trace
- Nano Services for Staged Provisioning
- Encryption Keys
- External Logging
- NSO 5.7 Web UI
- NSO CDM Migration Guide
- NSO Layered Service Architecture
- NSO 5.7 NED Development
- NSO 5.7 Manual Pages
- SDK API Reference
- NSO on DevNet
- Get Support
The idea is to write a YANG data model and feed that into the NSO CLI engine such that the resulting CLI mimics that of the device to manage. This is fairly straightforward once you have understood how the different constructs in YANG are mapped into CLI commands. The data model usually needs to be annotated with specific Tail-f CLI extension to tailor exactly how the CLI is rendered.
This chapter will describe how the general principles work and give a number of cook book style examples of how certain CLI constructs are modeled.
The CLI NED is primarily designed to be used with devices that has a CLI that is similar to the CLIs on a typical Cisco box (i.e. IOS, XR, NX-OS etc). However, if the CLI follows the same principles but with a slightly different syntax, it may still be possible to use a CLI NED if some of the differences are handled by the Java part of the CLI NED. This chapter will describe how this can be done.
Lets start with the basic data model to CLI mapping. YANG consists of three major elements: containers, lists, and leaves. For example
container interface { list ethernet { key id; leaf id { type uint16 { range "0..66"; } } leaf description { type string { length "1..80"; } } leaf mtu { type uint16 { range "64..18000"; } } } }
The basic rendering of the constructs are as follows. Containers are rendered as command prefixes which can be stacked at any depth. Leaves are rendered as commands that takes one parameter. Lists are rendered as submodes, where the key of the list is rendered as a submode parameter. The example above would result in the command
interface ethernet ID
for entering the interface ethernet submode. Interface is a container and is rendered as a prefix, ethernet is a list and is rendered as a submode. Two additional commands would be available in the submode
description WORD mtu INTEGER<64-18000>
A typical configuration with two interfaces could look like this
interface ethernet 0 description "customer a" mtu 1400 ! interface ethernet 1 description "customer b" mtu 1500 !
Note that it makes sense to add help texts to the data model since these texts will be visible in the NSO and help the user see the mapping between the J-style CLI in the NSO and the CLI on the target device. The data model above may look like the following with proper help texts.
container interface { tailf:info "Configure interfaces"; list ethernet { tailf:info "FastEthernet IEEE 802.3"; key id; leaf id { type uint16 { range "0..66"; tailf:info "<0-66>;;FastEthernet interface number"; } leaf description { type string { length "1..80"; tailf:info "LINE;;Up to 80 characters describing this interface"; } } leaf mtu { type uint16 { range "64..18000"; tailf:info "<64-18000>;;MTU size in bytes"; } } } }
I will generally not include the help texts in the examples below to save some space but they should be present in a production data model.
The basic rendering suffice in many cases but is also not enough in many situations. What follows is a list of ways to annotate the data model in order to make the CLI engine mimic a device.
Sometimes you want a number of instances (a list) but do not want a submode. For example
container dns { leaf domain { type string; } list server { ordered-by user; tailf:cli-suppress-mode; key ip; leaf ip { type inet:ipv4-address; } } }
The above would result in the commands
dns domain WORD dns server IPAddress
A typical show-config output may look like:
dns domain tail-f.com dns server 192.168.1.42 dns server 8.8.8.8
Sometimes you want a submode to be created without having a list instance, for example a submode called aaa where all aaa configuration is located.
This is done by using the tailf:cli-add-mode extension. For example:
container aaa { tailf:info "AAA view"; tailf:cli-add-mode; tailf:cli-full-command; ... }
This would result in the command aaa for entering the container. However, sometimes the CLI requires that a certain set of elements are also set when entering the submode, but without being a list. For example the police rules inside a policy map in the Cisco 7200.
container police { // To cover also the syntax where cir, bc and be // doesn't have to be explicitly specified tailf:info "Police"; tailf:cli-add-mode; tailf:cli-mode-name "config-pmap-c-police"; tailf:cli-incomplete-command; tailf:cli-compact-syntax; tailf:cli-sequence-commands { tailf:cli-reset-siblings; } leaf cir { tailf:info "Committed information rate"; tailf:cli-hide-in-submode; type uint32 { range "8000..2000000000"; tailf:info "<8000-2000000000>;;Bits per second"; } } leaf bc { tailf:info "Conform burst"; tailf:cli-hide-in-submode; type uint32 { range "1000..512000000"; tailf:info "<1000-512000000>;;Burst bytes"; } } leaf be { tailf:info "Excess burst"; tailf:cli-hide-in-submode; type uint32 { range "1000..512000000"; tailf:info "<1000-512000000>;;Burst bytes"; } } leaf conform-action { tailf:cli-break-sequence-commands; tailf:info "action when rate is less than conform burst"; type police-action-type; } leaf exceed-action { tailf:info "action when rate is within conform and "+ "conform + exceed burst"; type police-action-type; } leaf violate-action { tailf:info "action when rate is greater than conform + "+ "exceed burst"; type police-action-type; } }
Here the leaves with the annotation tailf:cli-hide-in-submode is not present as commands once the submode has been entered, but are instead only available as options the police command when entering the police submode.
Often a command is defined as taking multiple parameters in a typical Cisco CLI. This is achieved in the data model by using the annotations tailf:cli-sequence-commands, tailf:cli-compact-syntax, tailf:cli-drop-node-name and possibly tailf:cli-reset-siblings.
For example:
container udld-timeout { tailf:info "LACP unidirectional-detection timer"; tailf:cli-sequence-commands { tailf:cli-reset-all-siblings; } tailf:cli-compact-syntax; leaf "timeout-type" { tailf:cli-drop-node-name; type enumeration { enum fast { tailf:info "in unit of milli-seconds"; } enum slow { tailf:info "in unit of seconds"; } } } leaf "milli" { tailf:cli-drop-node-name; when "../timeout-type = 'fast'" { tailf:dependency "../timeout-type"; } type uint16 { range "100..1000"; tailf:info "<100-1000>;;timeout in unit of " +"milli-seconds"; } } leaf "secs" { tailf:cli-drop-node-name; when "../timeout-type = 'slow'" { tailf:dependency "../timeout-type"; } type uint16 { range "1..60"; tailf:info "<1-60>;;timeout in unit of seconds"; } } }
This results in the command
udld-timeout [fast <millisecs> | slow <secs> ]
The tailf:cli-sequence-commands annotation tells the CLI engine to process the leaves in sequence. The tailf:cli-reset-siblings tells the CLI to reset all leaves in the container if one is set. This is necessary in order to ensure that no lingering config remains from a previous invocation of the command where more parameters were configured. The tailf:cli-drop-node-name tells the CLI that the leaf name shouldn't be specified. The tailf:cli-compact-syntax annotation tells the CLI that the leaves should be formatted on one line, i.e. as
udld-timeout fast 1000
as opposed to
uldl-timeout fast uldl-timeout 1000
without the annotation. When constructs are used to control if the numerical value should be the milli or the secs leaf.
This command could also be written using a choice construct as
container udld-timeout { tailf:cli-sequence-command; choice udld-timeout-choice { case fast-case { leaf fast { tailf:info "in unit of milli-seconds"; type empty; } leaf milli { tailf:cli-drop-node-name; must "../fast" { tailf:dependency "../fast"; } type uint16 { range "100..1000"; tailf:info "<100-1000>;;timeout in unit of " +"milli-seconds"; } mandatory true; } } case slow-case { leaf slow { tailf:info "in unit of milli-seconds"; type empty; } leaf "secs" { must "../slow" { tailf:dependency "../slow"; } tailf:cli-drop-node-name; type uint16 { range "1..60"; tailf:info "<1-60>;;timeout in unit of seconds"; } mandatory true; } } } }
Sometimes the tailf:cli-incomplete-command is used to ensure that all parameters are configured. The cli-incomplete-command only applies to the C- and I-style CLI. To ensure that prior leaves in a container is also configured when the configuration is written using J-style or Netconf proper 'must' declarations should be used.
Another example is this where tailf:cli-optional-in-sequence is used
list pool { tailf:cli-remove-before-change; tailf:cli-suppress-mode; tailf:cli-sequence-commands { tailf:cli-reset-all-siblings; } tailf:cli-compact-syntax; tailf:cli-incomplete-command; key name; leaf name { type string { length "1..31"; tailf:info "WORD<length:1-31> Pool Name or Pool Group"; } } leaf ipstart { mandatory true; tailf:cli-incomplete-command; tailf:cli-drop-node-name; type inet:ipv4-address { tailf:info "A.B.C.D;;Start IP Address of NAT pool"; } } leaf ipend { mandatory true; tailf:cli-incomplete-command; tailf:cli-drop-node-name; type inet:ipv4-address { tailf:info "A.B.C.D;;End IP Address of NAT pool"; } } leaf netmask { mandatory true; tailf:info "Configure Mask for Pool"; type string { tailf:info "/nn or A.B.C.D;;Configure Mask for Pool"; } } leaf gateway { tailf:info "Gateway IP"; tailf:cli-optional-in-sequence; type inet:ipv4-address { tailf:info "A.B.C.D;;Gateway IP"; } } leaf ha-group-ip { tailf:info "HA Group ID"; tailf:cli-optional-in-sequence; type uint16 { range "1..31"; tailf:info "<1-31>;;HA Group ID 1 to 31"; } } leaf ha-use-all-ports { tailf:info "Specify this if services using this NAT pool " +"are transaction based (immediate aging)"; tailf:cli-optional-in-sequence; type empty; when "../ha-group-ip" { tailf:dependency "../ha-group-ip"; } } leaf vrid { tailf:info "VRRP vrid"; tailf:cli-optional-in-sequence; when "not(../ha-group-ip)" { tailf:dependency "../ha-group-ip"; } type uint16 { range "1..31"; tailf:info "<1-31>;;VRRP vrid 1 to 31"; } } leaf ip-rr { tailf:info "Use IP address round-robin behavior"; type empty; } }
The tailf:cli-optional-in-sequence means that the parameters should be processed in sequence but a parameter can be skipped. However, if a parameter is specified then only parameters later in the container can follow it.
It is also possible to have some parameters in sequence initially in the container, and then the rest in any order. This is indicated by the tailf:cli-break-sequence command. For example:
list address { key ip; tailf:cli-suppress-mode; tailf:info "Set the IP address of an interface"; tailf:cli-sequence-commands { tailf:cli-reset-all-siblings; } tailf:cli-compact-syntax; leaf ip { tailf:cli-drop-node-name; type inet:ipv6-prefix; } leaf link-local { type empty; tailf:info "Configure an IPv6 link local address"; tailf:cli-break-sequence-commands; } leaf anycast { type empty; tailf:info "Configure an IPv6 anycast address"; tailf:cli-break-sequence-commands; } }
where it is possible to write
ip 1.1.1.1 link-local anycast
as well as
ip 1.1.1.1 anycast link-local
Sometimes a command for entering a submode has parameters that are not really key values, i.e. not part of the instance identifier, but still needs to be given when entering the submode. For example
list service-group { tailf:info "Service Group"; tailf:cli-remove-before-change; key "name"; leaf name { type string { length "1..63"; tailf:info "NAME<length:1-63>;;SLB Service Name"; } } leaf tcpudp { mandatory true; tailf:cli-drop-node-name; tailf:cli-hide-in-submode; type enumeration { enum tcp { tailf:info "TCP LB service"; } enum udp { tailf:info "UDP LB service"; } } } leaf backup-server-event-log { tailf:info "Send log info on back up server events"; tailf:cli-full-command; type empty; } leaf extended-stats { tailf:info "Send log info on back up server events"; tailf:cli-full-command; type empty; } ... }
In this case the tcpudp is a non-key leaf that needs to be specified as a parameter when entering the service-group submode. Once in the submode the commands backup-server-event-log and extended-stats are present. Leaves with the tailf:cli-hide-in-submode attribute are given after the last key, in the sequence they appear in the list.
It is also possible to allow leaf values to be entered in between key elements. For example:
list community { tailf:info "Define a community who can access the SNMP engine"; key "read remote"; tailf:cli-suppress-mode; tailf:cli-compact-syntax; tailf:cli-reset-container; leaf read { tailf:cli-expose-key-name; tailf:info "read only community"; type string { length "1..31"; tailf:info "WORD<length:1-31>;;SNMPv1/v2c community string"; } } leaf remote { tailf:cli-expose-key-name; tailf:info "Specify a remote SNMP entity to which the user belongs"; type string { length "1..31"; tailf:info "Hostname or A.B.C.D;;IP address of remote SNMP " +"entity(length: 1-31)"; } } leaf oid { tailf:info "specific the oid"; // SIC tailf:cli-prefix-key { tailf:cli-before-key 2; } type string { length "1..31"; tailf:info "WORD<length:1-31>;;The oid qvalue"; } } leaf mask { tailf:cli-drop-node-name; type string { tailf:info "/nn or A.B.C.D;;The mask"; } } }
Here we have a list that is not mapped to a submode. It has two keys, read and remote, an an optional oid that can be specified before the remote key. Finally after the last key an optional mask parameter can be specified. The use of the tailf:cli-expose-key-name means that the key names should be part of the command, which they are not by default. The above construct results in the commands
community read WORD [oid WORD] remote HOSTNAME [/nn or A.B.C.D]
The tailf:cli-reset-container attribute means that all leaves in the container will be reset if any leaf is given.
Some devices requires that a setting is removed before it can be changed, for example the service-group list above. This is indicated with the tailf:cli-remove-before-change annotation. It can be used both on lists and on leaves. A leaf example:
leaf source-ip { tailf:cli-remove-before-change; tailf:cli-no-value-on-delete; tailf:cli-full-command; type inet:ipv6-address { tailf:info "X:X::X:X;;Source IPv6 address used by DNS"; } }
This means that the diff sent to the device will contain first a no source-ip command, followed by a new source-ip command to set the new value.
The data model also use the tailf:cli-no-value-on-delete annotation which means that the leaf value should not be present in the no command. With the annotation a diff to modify the source ip from 1.1.1.1 to 2.2.2.2 would look like
no source-ip source-ip 2.2.2.2
and without the annotation as
no source-ip 1.1.1.1 source-ip 2.2.2.2
By default a diff for an ordered-by user list contains information about where a new item should be inserted. This is typically not supported by the device. Instead the commands (diff) to send the device needs to remove all items following the new item, and then reinsert the items in the proper order. This behavior is controlled using the tailf:cli-long-obu-diff annotation. For example
list access-list { tailf:info "Configure Access List"; tailf:cli-suppress-mode; key id; leaf id { type uint16 { range "1..199"; } } list rules { ordered-by user; tailf:cli-suppress-mode; tailf:cli-drop-node-name; tailf:cli-show-long-obu-diffs; key "txt"; leaf txt { tailf:cli-multi-word-key; type string; } } }
Suppose we have the access list
access-list 90 permit host 10.34.97.124 access-list 90 permit host 172.16.4.224
and we want to change this to
access-list 90 permit host 10.34.97.124 access-list 90 permit host 10.34.94.109 access-list 90 permit host 172.16.4.224
we would generate the diff
no access-list 90 permit host 172.16.4.224 access-list 90 permit host 10.34.94.109 access-list 90 permit host 172.16.4.224
with the tailf:cli-long-obu-diff. Without the annotation the diff would be
# after permit host 10.34.97.124 access-list 90 permit host 10.34.94.109
Often in a config when a leaf is set to its default value it is not displayed by the 'show running-config' command, but we still need to set it explicitly. Suppose we have the leaf 'state'. By default the value is 'active'.
leaf state { tailf:info "Activate/Block the user(s)"; type enumeration { enum active { tailf:info "Activate/Block the user(s)"; } enum block { tailf:info "Activate/Block the user(s)"; } } default "active"; }
If the device state is 'block' and we cant to set it to 'active', i.e. the default value. The default behavior is to send
no state block
to the device. This will not work. The correct command sequence should be
state active
The way to achieve this is to do the following.
leaf state { tailf:info "Activate/Block the user(s)"; type enumeration { enum active { tailf:info "Activate/Block the user(s)"; } enum block { tailf:info "Activate/Block the user(s)"; } } default "active"; tailf:cli-trim-default; tailf:cli-show-with-default; }
This way a value for 'state' will always be generated. This may seem unintuitive but the reason this works comes from how the diff is calculated. When generating the diff the target configuration and the desired configuration is compared (per line). The target config will be
state block
and the desired config will be
state active
This will be interpreted as a leaf value change and the resulting diff will be to set the new value, i.e. active.
However, without the 'cli-show-with-default' option the desired config will be an empty line, i.e. no value set. When we compare the two lines we get
(current config)
state block
(desired config)
<empty>
This will result in the command to remove the configured leaf, i.e.
state block
which does not work.
What you see in the C-style CLI when you do 'show configuration' is the commands needed to go from the running config to the configuration you have in your current session. It usually corresponds to the command you have just issued in your CLI session, but not always.
The output is actually generated by comparing the two configurations, i.e. the running config and your current uncommitted configration. It is done by running 'show running-config' on both the running config and your uncommitted config, and then comparing the output line by line. Each line is complemented by some meta information which makes it possible to generate a better diff.
For example, if you modify a leaf value, say set the mtu to 1400 and the previous value was 1500. The two configs will then be
interface FastEthernet0/0/1 interface FastEthernet0/0/1 mtu 1500 mtu 1400 ! !
When we compare these configs the first line are the same -> no action but we remember that we have entered the FastEthernet0/0/1 submode. The second line differ in the value (the meta information associated with the lines have the path and the value). When we analyze the two lines we determine that a value_set has occurred. The default action when the value has been changed is to output the command for setting the new value, i.e. mtu 1500. However, we also need to reposition to the current submode. If this is the first line we are outputting in the submode we need to issue the command
interface FastEthernet0/0/1
before issuing the mtu 1500 command.
Similarly, suppose a value has been removed, i.e. mtu used to be set but it is no longer present
interface FastEthernet0/0/1 interface FastEthernet0/0/1 ! mtu 1400 !
As before, the first lines are equivalent, but the second line has ! in the new config, and mtu 1400 in the running config. This is analyzed as being a delete and the commands
interface FastEthernet0/0/1 no mtu 1400
are generated.
There are tweaks to this behavior. For example, some machines does not like the no command to include the old value but want instead the command
no mtu
We can instruct the CLI diff engine to behave in this way by using the YANG annotation tailf:cli-no-value-on-delete;
leaf mtu { tailf:cli-no-value-on-delete; type uint16; }
It is also possible to tell the CLI engine to not include the element name in the delete operation. For example the command
aaa local-user password cipher "C>9=UF*^V/'Q=^Q`MAF4<1!!"
but the command to delete the password is
no aaa local-user password
The data model for this would be
// aaa local-user container password { tailf:info "Set password"; tailf:cli-flatten-container; leaf cipher { tailf:cli-no-value-on-delete; tailf:cli-no-name-on-delete; type string { tailf:info "STRING<1-16>/<24>;;The UNENCRYPTED/" +"ENCRYPTED password string"; } } }
It is often necessary to do some minor modifications to the Java part of a CLI NED. There are mainly four functions that needs to be modified: connect, show, applyConfig, enter/exit config mode.
The CLI NED code should do a few things when the connect callback is invoked.
-
Set up a connection to the device (usually ssh).
-
If necessary send a secondary password to enter exec mode. Typically a Cisco IOS like CLI requires the user to give the enable command followed by a password.
-
Verify that it is the right kind of device and respond to NSO with a list of capabilities. This is usually done by running the show version command, or equivalent, and parsing the output.
-
Configure the CLI session on the device to not use pagination. This is normally done by setting the screen length to 0 (or infinity or disable). Optionally it may also fiddle with the idle time.
Some modifications may be needed in this section if the commands for the above differ from the Cisco IOS style.
The NSO will invoke the show() callback multiple times, one time for each top-level tag in the data model. Some devices have support for displaying just parts of the configuration, others do not.
For a device that cannot display only parts of a config the recommended strategy is to wait for a show() invocation with a well known top tag and send the entire config at that point. If, if you know that the data model has a top tag called interface then you can use code like:
public void show(NedWorker worker, String toptag) throws NedException, IOException { session.setTracer(worker); try { int i; if (toptag.equals("interface")) { session.print("show running-config | exclude able-management\n"); ... } else { worker.showCliResponse(""); } } catch (...) { ... } }
From the point of NSO it is perfectly ok to send the entire config as a response to one of the requested toptags, and to send an empty response otherwise.
Often some filtering is required of the output from the device. For example, perhaps part of the configuration should not be sent to NSO, or some keywords replaced with other. Here follows some examples:
Some devices start the output from show running-config with a short header, and some add a footer. Common headers are Current configuration: and a footer may be end or return. In the example below we strip out a header and remove a footer.
if (toptag.equals("interface")) { session.print("show running-config | exclude able-management\n"); session.expect("show running-config | exclude able-management"); String res = session.expect(".*#"); i = res.indexOf("Current configuration :"); if (i >= 0) { int n = res.indexOf("\n", i); res = res.substring(n+1); } i = res.lastIndexOf("\nend"); if (i >= 0) { res = res.substring(0,i); } worker.showCliResponse(res); } else { // only respond to first toptag since the A10 // cannot show different parts of the config. worker.showCliResponse(""); }
Also, you may choose to only model part of a device configuration in which case you can strip out the parts that you have not modelled. For example stripping out the snmp configuration:
if (toptag.equals("context")) { session.print("show configuration\n"); session.expect("show configuration"); String res = session.expect(".*\\[.*\\]#"); snmp = res.indexOf("\nsnmp"); home = res.indexOf("\nsession-home"); port = res.indexOf("\nport"); tunnel = res.indexOf("\ntunnel"); if (snmp >= 0) { res = res.substring(0,snmp)+res.substring(home,port)+ res.substring(tunnel); } else if (port >= 0) { res = res.substring(0,port)+res.substring(tunnel); } worker.showCliResponse(res); } else { // only respond to first toptag since the STOKEOS // cannot show different parts of the config. worker.showCliResponse(""); }
Sometimes a device generates non-parsable commands in the output from show running-config. For example, some A10 devices adds a keyword cpu-process at the end of the ip route command, i.e.
ip route 10.40.0.0 /14 10.16.156.65 cpu-process
but it does not accept this keyword when a route is configured. The solution is to simply strip the keyword before sending the config to NSO, and to not include the keyword in the data model for the device. Code to do this may look like:
if (toptag.equals("interface")) { session.print("show running-config | exclude able-management\n"); session.expect("show running-config | exclude able-management"); String res = session.expect(".*#"); // look for the string cpu-process and remove it i = res.indexOf(" cpu-process"); while (i >= 0) { res = res.substring(0,i)+res.substring(i+12); i = res.indexOf(" cpu-process"); } worker.showCliResponse(res); } else { // only respond to first toptag since the A10 // cannot show different parts of the config. worker.showCliResponse(""); }
Sometimes a device has some other names for delete than the standard no command found in a typical Cisco CLI. NSO will only generate no commands when, for example, an element does not exist (i.e. no shutdown for an interface), but the device may need undo instead. This can be dealt with as a simple transformation of the configuration before sending it to NSO. For example:
if (toptag.equals("aaa")) { session.print("display current-config\n"); session.expect("display current-config"); String res = session.expect("return"); session.expect(".*>"); // split into lines, and process each line lines = res.split("\n"); for(i=0 ; i < lines.length ; i++) { int c; // delete the version information, not really config if (lines[i].indexOf("version ") == 1) { lines[i] = ""; } else if (lines[i].indexOf("undo ") >= 0) { lines[i] = lines[i].replaceAll("undo ", "no "); } } worker.showCliResponse(join(lines, "\n")); } else { // only respond to first toptag since the H3C // cannot show different parts of the config. // (well almost) worker.showCliResponse(""); }
Another example is the following situation. A device has a configuration for port trunk permit vlan 1-3 and may at the same time have disallow some vlans using the command no port trunk permit vlan 4-6. Since we cannot use a no container in the config, we instead add a disallow container, and then rely on the Java-code to do some processing, e.g.:
container disallow { container port { tailf:info "The port of mux-vlan"; container trunk { tailf:info "Specify current Trunk port's " +"characteristics"; container permit { tailf:info "allowed VLANs"; leaf-list vlan { tailf:info "allowed VLAN"; tailf:cli-range-list-syntax; type uint16 { range "1..4094"; } } } } } }
And in the Java show() code:
if (toptag.equals("aaa")) { session.print("display current-config\n"); session.expect("display current-config"); String res = session.expect("return"); session.expect(".*>"); // process each line lines = res.split("\n"); for(i=0 ; i < lines.length ; i++) { int c; if (lines[i].indexOf("no port") >= 0) { lines[i] = lines[i].replaceAll("no ", "disallow "); } } worker.showCliResponse(join(lines, "\n")); } else { // only respond to first toptag since the H3C // cannot show different parts of the config. // (well almost) worker.showCliResponse(""); }
A similar transformation needs to take place when the NSO sends a configuration change to the device. A more detailed discussion about apply config modifications follow later but the corresponding code would in this case be:
lines = data.split("\n"); for (i=0 ; i < lines.length ; i++) { if (lines[i].indexOf("disallow port ") == 0) { lines[i] = lines[i].replace("disallow ", "undo "); } }
If the way a device quotes strings differ from the way it can be modelled in NSO it can be handled in the Java code. For example, one device does not quote encrypted password strings which may contain odd characters like the command character !. Java code to deal with this may look like:
if (toptag.equals("aaa")) { session.print("display current-config\n"); session.expect("display current-config"); String res = session.expect("return"); session.expect(".*>"); // process each line lines = res.split("\n"); for(i=0 ; i < lines.length ; i++) { if ((c=lines[i].indexOf("cipher ")) >= 0) { String line = lines[i]; String pass = line.substring(c+7); String rest; int s = pass.indexOf(" "); if (s >= 0) { rest = pass.substring(s); pass = pass.substring(0,s); } else { s = pass.indexOf("\r"); if (s >= 0) { rest = pass.substring(s); pass = pass.substring(0,s); } else { rest = ""; } } // find cipher string and quote it lines[i] = line.substring(0,c+7)+quote(pass)+rest; } } worker.showCliResponse(join(lines, "\n")); } else { worker.showCliResponse(""); }
And similarly dequoting when applying a configuration.
lines = data.split("\n"); for (i=0 ; i < lines.length ; i++) { if ((c=lines[i].indexOf("cipher ")) >= 0) { String line = lines[i]; String pass = line.substring(c+7); String rest; int s = pass.indexOf(" "); if (s >= 0) { rest = pass.substring(s); pass = pass.substring(0,s); } else { s = pass.indexOf("\r"); if (s >= 0) { rest = pass.substring(s); pass = pass.substring(0,s); } else { rest = ""; } } // find cipher string and quote it lines[i] = line.substring(0,c+7)+dequote(pass)+rest; } }
NSO will send configuration to the device in three different callbacks: prepare(), abort(), and revert(). The Java code should issue these commands to the device but some processing of the commands may be necessary. Also, the ongoing CLI session needs to enter configure mode, issue the commands, and then exit configure mode. Some processing may be needed if the device has different keywords, or different quoting, as described under the "Displaying the configuration of a device" section above.
For example, if a device uses undo in place of no then the code may look like this, where data is the string of commands received from NSO:
lines = data.split("\n"); for (i=0 ; i < lines.length ; i++) { if (lines[i].indexOf("no ") == 0) { lines[i] = lines[i].replace("no ", "undo "); } }
This relies on the fact that NSO will not have any indentation in the commands sent to the device (as opposed to the indentation usually present in the output from show running-config).