Post JSON data to URL from Junos, “Event-driven”

Post JSON data to URL from Junos, “Event-driven”

Why didn’t I think of that? … was my immediate thought, when I was confronted with the following problem and asked to implement it: Juniper ACX devices are deployed using Zero-Touch-Provisioning (ZTP). The ask was simple: Whenever a link Up is detected, send the learned LLDP information together with a dump of the learned mac addresses from the local bridge to a remote web server, preferably in JSON format. This covered the what, but not the how, so there was still work to be done.

The solution is an on-box event script, triggered by LLDP_NEIGHBOR_UP and SNMP_TRAP_LINK_UP event, that collects JSON formatted output of show lldp neighbor and show bridge mac-table and post it to a web URL provided as argument to the script.

Script and how it works

I know, everyone moved on to Python for off-box and on-box scripting, but I can’t help myself and always fall back to SLAX … and I’m pleased with the result: simple, short and efficient. Would be very interested if someone converts this into Python and share it; anyone?
Update Aug 27th: Check out my follow-up post with a working Python Example: Post JSON data to URL from Junos, now in Python

The script works as an operational and event script, which makes it simple to troubleshoot. Here is how it works:

  1. When called, it expects the URL to POST the data to as its only argument, stored into $url.
  2. Open a local NETCONF session to pass the Junos queries to.
  3. Open a CURL extension session to post data after the collection phase.
  4. Send , requesting the output in JSON format and store the result in $mactable
  5. Send , requesting the output also in JSON format, stored in $lldp_nb.
  6. Construct the POST request by concatenating $mactable with $lldp_nb into a JSON array and remove newlines and spaces. No human will hopefully need to read the raw data.
  7. Execute the POST HTML request via curl:perform().
  8. Close sessions.

Some basic error handling and confirmation messaging is done via SYSLOG and STDOUT.

/*
 * collect_and_post.slax
 *
 * Junos op/event script to collect the output of
 * show lldp neighbors & show bridge mac-table
 * and post it in JSON format to the provided URL.
 */

version 1.1;

ns junos = “http://xml.juniper.net/junos/*/junos”;
ns jcs = “http://xml.juniper.net/junos/commit-scripts/1.0”;
ns curl extension = “http://xml.libslax.org/curl”;

var $logprefix = “collect_and_post.slax: “;

param $url;
var $arguments = {
  <argument> {
    <name> “url”;
    <description> “URL to post data to (e.g. http://192.168.1.100:8000)”;
  }
}

match / {
  <op-script-results> {
    if (not($url)) {
      <xsl:message terminate=“yes”> “Please specify url”;
    }
    var $con = jcs:open();
    var $curl = curl:open();

    var $rpc-query1 = <get-bridge-mac-table format=“json”>;
    var $mactable = jcs:execute($con, $rpc-query1);

    var $rpc-query2 = <get-lldp-neighbors-information format=“json”>;
    var $lldp_nb = jcs:execute($con, $rpc-query2);

    var $html_post := {
      <url> $url;
      <method> “post”;
      <contents> translate(“[“ 
        _ $mactable _ “,” _ $lldp_nb 
        _ “]”, “ \n\r”,””);
    }
    var $results = curl:perform($curl, $html_post);

    if ($results/error) {
      expr slax:output(“Error: “ _ $results/error);
      expr jcs:syslog(“user.error”, $logprefix, $results/error);
    } else {
      var $message = “Data sent to “ _ $url _ “: “
        _ $results/headers/code _ “ “ _ $results/headers/message;
      expr slax:output($message);
      expr jcs:syslog(“user.notice”, $logprefix, $message);
    }
    <curl> {
      copy-of $results;
    }
    expr jcs:close($con);
    expr curl:close($curl);
  }
}

Installation

Upload the script to the Junos device into /var/db/scripts/op/.
No need to save it also under /var/db/scripts/events/, the same location works for op and event scripts. Though if you only deploy it as an event script, pick the events location to describe the true nature of the script.

If you prefer the script be fetched remotely at startup, check out an earlier blog post: Fetch remote Junos script at startup.

Configure Junos, pick a URL with a listening web server. A simple, Python based web server is shown further down for testing purposes.

set system scripts op file collect_and_post.slax
set event-options policy interface-up events lldp_neighbor_up
set event-options policy interface-up events snmp_trap_link_up
set event-options policy interface-up then event-script collect_and_post.slax arguments url http://192.168.1.100:8000

Done! Now it is time to try it out. I use vMX in a virtual lab to develop and test it out, but this will work equally well on any Junos device.

Very simple web server

This example web server only understands POST requests and dumps the JSON data in a (somewhat) human friendly format to STDOUT.

$ cat webserver.py
from http.server import HTTPServer, BaseHTTPRequestHandler

from io import BytesIO
import json


class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):

    def do_POST(self):
        content_length = int(self.headers[‘Content-Length’])
        post_data = self.rfile.read(content_length)
        json_data = json.loads(post_data.rstrip())
        print(“Got post request with: “, json.dumps(
            json_data, indent=2))
        self.send_response(200)
        self.end_headers()
        response = BytesIO()
        self.wfile.write(response.getvalue())


httpd = HTTPServer((‘’, 8000), SimpleHTTPRequestHandler)
httpd.serve_forever()

Launch it in a seperate terminal and wait for data to show up:

$ python3 ./webserver.py

Execute the op script on Junos

Now it is time to fire the script on the Junos device:

user@router> op collect_and_post url http://192.168.1.100:8000
Data sent to http://192.168.1.100:8000: 200 OK

The ‘200 OK’ is the received ack from the web server we launched in the other window, which shows the received JSON data:

$ python3 ./webserver.py
Got post request with:  [
{
    “l2ald-rtb-macdb”: [
      {
        “l2ald-mac-entry”: [
          {
            “attributes”: {
              “junos:style”: “brief-rtb”
            },
            “l2-mac-routing-instance”: [
              {
                “data”: “default-switch”
              }
            ],
            “l2-mac-bridging-domain”: [
              {
                “data”: “VLAN-1”
              }
            ],
            “l2-bridge-vlan”: [
              {
                “data”: “1”
              }
            ],
            “l2-mac-entry”: [
              {
                “l2-mac-address”: [
                  {
                    “data”: “02:00:5b:00:00:01”
                  }
                ],
                “l2-mac-flags”: [
                  {
                    “data”: “D”
                  }
                ],
                “l2-mac-logical-interface”: [
                  {
                    “data”: “ge-0/0/0.1”
                  }
                ]
              },
              {
                “l2-mac-address”: [
                  {
                    “data”: “02:00:5b:00:00:04”
                  }
                ],
                “l2-mac-flags”: [
                  {
                    “data”: “D”
                  }
                ],
                “l2-mac-logical-interface”: [
                  {
                    “data”: “ge-0/0/0.1”
                  }
                ]
              },
             {
                “l2-mac-address”: [
                  {
                    “data”: “02:00:5c:00:00:14”
                  }
                ],
                “l2-mac-flags”: [
                  {
                    “data”: “D”
                  }
                ],
                “l2-mac-logical-interface”: [
                  {
                    “data”: “ge-0/0/1.1”
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  },
  {
    “lldp-neighbors-information”: [
      {
        “lldp-neighbor-information”: [
          {
            “lldp-local-port-id”: [
              {
                “data”: “ge-0/0/3”
              }
            ],
            “lldp-local-parent-interface-name”: [
              {
                “data”: “-“
              }
            ],
            “lldp-remote-chassis-id-subtype”: [
              {
                “data”: “Macaddress”
              }
            ],
            “lldp-remote-chassis-id”: [
              {
                “data”: “2c:6b:f5:b1:c8:c0”
              }
            ],
            “lldp-remote-port-id-subtype”: [
              {
                “data”: “Locallyassigned”
              }
            ],
            “lldp-remote-port-id”: [
              {
                “data”: “521”
              }
            ],
            “lldp-remote-system-name”: [
              {
                “data”: “vmx2”
              }
            ]
          }
        ]
      }
    ]
  }
]
172.18.0.3 - - [19/Aug/2018 11:10:13] “POST / HTTP/1.1” 200 -

Even script Trigger

How can you test an event script? Apart from the obvious to wait for an actual event to happen, we have at least two options here: Use the Junos shell command ‘logger -e LLDP_NEIGHBOR_UP’ or simply clear the LLDP neighbors and wait for the next update via clear lldp neighbors.

user@router> clear lldp neighbors

(wait a minute or two ..)

user@router> show log messages
Aug 19 09:10:06 vmx1 clear-log[23607]: logfile cleared
Aug 19 09:10:08  vmx1 last message repeated 2 times
Aug 19 09:10:08  vmx1 cscript.crypto: collect_and_post.slax: Data sent to http://138.201.122.182:8000: 200 OK
Aug 19 09:10:08  vmx1 root: invoke-commands: Executed /tmp/evt_cmd_1YyFnr, output to /tmp/evt_op_pKvBAc in text format

Troubleshooting

No script works right away, at least not the ones I write …

If the web server isn’t responding, a brief error message is returned. If that isn’t sufficient to figure out what goes on, display the script output in XML to get more information from the web server:

user@router> op collect_and_post url http://192.168.1.100:8000 |display xml
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/17.3R2/junos">
    <output>
        Data sent to http://192.168.1.100:8000: 200 OK
    </output>
    <curl>
        <results>
            <url>
                http://192.168.1.100:8000
            </url>
            <curl-success/>
            <raw-headers>
                HTTP/1.0 200 OK
                Server: BaseHTTP/0.6 Python/3.6.5
                Date: Sun, 19 Aug 2018 14:00:13 GMT
                
            </raw-headers>
            <headers>
                <version>
                    HTTP/1.0
                </version>
                <code>
                    200
                </code>
                <message>
                    OK
                </message>
                <header name="Server">
                    BaseHTTP/0.6 Python/3.6.5
                </header>
                <header name="Date">
                    Sun, 19 Aug 2018 14:00:13 GMT
                </header>
            </headers>
        </results>
    </curl>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

Summary

I hope you enjoyed this small example of an on-box script, capable of collecting operational data and post it to a webserver in JSON format for easy consumption. I always learn something new along the way. This time it was about how to get NETCONF to return the output in JSON format by adding the attribute ‘format=‘JSON’ to the XML RPC query.

BTW if you ever need to figure out the corresponding XML RPC request for an operational command, try this:

user@router> show lldp neighbor | display xml rpc
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/17.3R2/junos">
    <rpc>
        <get-lldp-neighbors-information>
        </get-lldp-neighbors-information>
    </rpc>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

One thought on “Post JSON data to URL from Junos, “Event-driven”

Add yours

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Blog at WordPress.com.

Up ↑

%d bloggers like this: