At Interoute, we make extensive use of Juniper’s MX-960 platform to support enterprise and service provider business on a uniform packet network. A mature and established platform, the MX-960 is a solid workhorse for the usual suite of IP-based services offered by most providers: Internet access/transit, IP VPN, VPLS. JUNOS has persisting roots in FreeBSD and the core of Juniper’s routing architecture, rpd, has a respected heritage in the original UNIX gated – probably one of the earliest UNIX software programs to make use of asynchronous I/O in order to provide the facility to execute multiple concurrent tasks at the same time without resorting to UNIX processes.
If you come from a Cisco IOS background, Juniper’s command line interface can seem a little alien at first, but one can see that the software architects behind JUNOS were fully-versed in the limitations of the Cisco IOS command line and its limited atomicity of a single command. In contrast, JUNOS inherently supports locking a centralised configuration database, applying a series of changes atomically even if these changes affect multiple underlying sub-systems.
One of the more darker arts of JUNOS, however, is its automation system. Underneath the hood, when one enters a simple command such as “show configuration”, the command line interpreter is actually generating an XML RPC (not to be confused with actual Winer-esque XML-RPC) request, sending it to the management daemon, reading the corresponding XML result, unpacking it and rendering it to the terminal. It’s actually a rather extremely protracted process, but it has the surprisingly attractive side-effect in that programmatically slaving the Juniper device is much simpler as a result.
If the device configuration can be represented as an XML tree, common operations to query configuration can be expressed as XPaths (a method of addressing a node within an XML document), and operations to modify configuration can be expressed in terms of a series of transforms to an XML document.
Juniper quickly realised that writing XML is horrible for programmers, and produced a front-end pre-processor for the XSLT, called SLAX which is a little more accessibly, but one of the reasons, I think, that the JUNOS automation frameworks are not particularly well-trodden by either hardened network engineers or software developers alike is that it typically represents a different programming paradigm to most simple software scripting languages.
Instead of expressing a series of instructions to be executed sequentially as is common in most imperatives, one usually finds oneself plonked in the middle of an input stream representing either the current configuration or one of the operational state reports of the device. The task for the programmer is thus usually to find the most efficient way to determine the state conditions that they want to act on, and output structured data to either display aggregated value-add data to the operator, or provide configuration-merge instructions.
For-each loops are forced to execute to completion, and variables are, counter-intuitively, non-mutable. This is basically a reflection of the fact that the operation is pinned by the underlying XSLT transformation semantics. No Turing-complete Towers of Hanoi here then (conditionals do exist, but arbitrary variable modification is not possible).
Cutting the computer science theory though, and skipping to a practical primer, here I provide a little worked example of an oft-asked-for utility that is surprisingly absent in the Juniper CLI: display a simple table that shows:
- Logical interface,
- VPN association,
- Network address.
First of all we start with some boilerplate: definitions of namespace and definitions of some of the JUNOS support functions. For the most part you can generally ignore this. Most scripts start with exactly the same.
version 1.0; ns junos = "http://xml.juniper.net/junos/*/junos"; ns xnm = "http://xml.juniper.net/xnm/1.1/xnm"; ns jcs = "http://xml.juniper.net/junos/commit-scripts/1.0"; import "../import/junos.xsl";
The real work starts with the first variable definition, $config-rpc, which is what SLAX calls an RTF: certainly not rich-text, far from it. Rather a “result-tree fragment” or a portion of unprocessed XML. In this case, the XML is actually specifying a request parameter. It is defining the properties of the configuration that we want to extract: we want committed configuration, and we want the routing-instances tree. Think of this as a C-struct or, if you’re a Javascript programmer, a hierarchical object.
This object is passed to the library utility function jcs:invoke and the result is what SLAX calls a node-set. A node-set is processed XML that can be addressed and manipulated using an XPath. For example, in this case the ‘variable’ $routing-instance-configuration/routing-instances/instance/MyVPN would expand to a structure defining the properties of the MyVPN routing-instance.
var $config-rpc = { <get-configuration database="committed"> { <configuration> { <routing-instances>; } } } var $routing-instance-configuration = jcs:invoke( $config-rpc ); var $interface-information = jcs:invoke("get-interface-information");
The second variable declaration does exactly the same, except it sources its data from one of the JUNOS RPC calls that is the equivalent of the show interfaces command. (Tip: if you want to see how a JUNOS CLI command maps to a JUNOS RPC call, at the CLI issue the command and pipe the output to | display xml rpc).
You can probably see where we’re going now: take the table of routing instance information, which declares the interfaces, and join it up with the interface table to show interface addresses and hopefully save the last few remaining hairs on the heads of our frustrated VPN provisioning operators.
The match statement is where the guts of the processing happens. Why the match statement, you might ask? Well, remember, the structure of a JUNOS SLAX script is effectively a transformation map: map an input to an output. So the match statement is accepting an XPath, and providing an opportunity for modification and manipulation. In this case though, we’ve already acquired our input data courtesy of the jcs:invoke() calls, so we’re good to go: we just include a token match on the root node.
What follows is a staircase for-each traversal of the routing-instance table where we extract every interface associated with every routing-instance and then go to seek out the protocol addresses configured on the specific interface. The output tag is used to write a line of the resulting output table.
match / { <op-script-results> { <output> "Instance\tInterface\tAddress\t(Local)"; for-each ($routing-instance-configuration/routing-instances/instance) { var $instance-name = name; var $instance-type = instance-type; for-each (interface) { var $interface = name; for-each ($interface-information/physical-interface/logical-interface) { if (name == $interface) { for-each (address-family) { var $family = address-family-name; for-each (interface-address) { <output> $instance-type _" "_ $instance-name _ "\t" _ $interface _ "\t" _ $family _ " " _ ifa-destination _ " (" _ ifa-local _ ")"; } } } } } } } }
Sample output looks similar to the following (yes, that is IPv6 – this is a test box!):
chappa10@lon-lab-score-1-re0> op show-vpn-interfaces Instance Interface Address (Local) vrf VRF-1006 ge-9/0/2.10341 inet 195.81.77.40/30 (195.81.77.41) vrf VRF-1006 ge-9/0/2.10341 inet6 fd00:1103:1006:1::/64 (fd00:1103:1006:1::1) vrf VRF-1006 ge-9/0/2.10341 inet6 fe80::/64 (fe80::21d:b500:712c:6dcf) vrf VRF-1103 xe-8/3/0.12151 inet 194.158.26.92/30 (194.158.26.93) vrf VRF-1103 ge-9/0/2.10312 inet 195.81.77.52/30 (195.81.77.53) vrf VRF-1103 ge-9/0/2.10312 inet6 fd00:1103:1006::/48 (fd00:1103:1006::1)
Completed source file available here
If this inspires you sufficiently to take the plunge into automating your Juniper network platforms, my advice would be to keep a well-grounded approach to your expectations and take a look at these resources to move beyond the simple example I provide here.