Interacting with Cisco NSO from a Javascript frontend

Interacting with Cisco NSO from a Javascript frontend

UPDATE: This may no longer be possible in many NSO versions, as Cisco has implemented authentication requirements for OPTIONS requests, breaking the CORS framework.

Introduction

I am currently writing a web-based frontend to interact with Cisco NSO to demo some of the code we're working on at work. It's simply because I don't find it very visually engaging to watch someone click on some predefined Postman collections.

Sure, we as engineers and geeks find it interesting to watch raw code fly around, but not everyone finds this engaging.

For another project I sat down and taught myself the Vue framework, and I wanted to use this for it.

This blog post is updated as I code.

Cisco NSO

If you are totally unfamiliar with Cisco NSO, it a NETCONF and YANG based Network Orchestrator, or automation engine if you will. It allows you to write complex automation services based on YANG with additional programming logic on top, typically either Python or Java.

It automatically creates Northbound APIs for all its YANG models, and allows you to access those through a number of protocols, including RESTCONF, a REST-wrapped Netconf methodology.

If you're unfamiliar with Cisco NSO, this blog post is not for you.

RESTconf and Client security

The first challenge came from the fact that the user's browser is in fact the client that interacts directly with NSO. This means that we are at the mercy of Google and Mozilla, and their implementations of security. This provides challenges that are not present when using Postman.

CORS

The first challenge is that of CORS, or Cross-Origin Resource Sharing. The browser expects a header to be present on the API provider, indicating who may access the API and with which resources and calls.

It was quite easy to configure this in NSO. It was just a matter of changing the default <restconf> section in ncs.conf

  <restconf>
    <enabled>true</enabled>
        <custom-headers>
          <header>
          <name>Access-Control-Allow-Origin</name>
          <value>*</value>
          </header>
          <header>
          <name>Access-Control-Allow-Credentials</name>
          <value>true</value>
          </header>
          <header>
          <name>Access-Control-Allow-Methods</name>
          <value>GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS</value>
          </header>
          <header>
          <name>Access-Control-Allow-Headers</name>
          <value>Accept, Accept-CH, Accept-Charset, Accept-Datetime, Accept-Encoding, Accept-Ext, Accept-Features, Accept-Language, Accept-Params, Accept-Ranges, Access-Control-Allow-Credentials, Access-Control-Allow-Headers, Access-Control-Allow-Methods, Access-Control-Allow-Origin, Access-Control-Expose-Headers, Access-Control-Max-Age, Access-Control-Request-Headers, Access-Control-Request-Method, Age, Allow, Alternates, Authentication-Info, Authorization, C-Ext, C-Man, C-Opt, C-PEP, C-PEP-Info, CONNECT, Cache-Control, Compliance, Connection, Content-Base, Content-Disposition, Content-Encoding, Content-ID, Content-Language, Content-Length, Content-Location, Content-MD5, Content-Range, Content-Script-Type, Content-Security-Policy, Content-Style-Type, Content-Transfer-Encoding, Content-Type, Content-Version, Cookie, Cost, DAV, DELETE, DNT, DPR, Date, Default-Style, Delta-Base, Depth, Derived-From, Destination, Differential-ID, Digest, ETag, Expect, Expires, Ext, From, GET, GetProfile, HEAD, HTTP-date, Host, IM, If, If-Match, If-Modified-Since, If-None-Match, If-Range, If-Unmodified-Since, Keep-Alive, Label, Last-Event-ID, Last-Modified, Link, Location, Lock-Token, MIME-Version, Man, Max-Forwards, Media-Range, Message-ID, Meter, Negotiate, Non-Compliance, OPTION, OPTIONS, OWS, Opt, Optional, Ordering-Type, Origin, Overwrite, P3P, PEP, PICS-Label, POST, PUT, Pep-Info, Permanent, Position, Pragma, ProfileObject, Protocol, Protocol-Query, Protocol-Request, Proxy-Authenticate, Proxy-Authentication-Info, Proxy-Authorization, Proxy-Features, Proxy-Instruction, Public, RWS, Range, Referer, Refresh, Resolution-Hint, Resolver-Location, Retry-After, Safe, Sec-Websocket-Extensions, Sec-Websocket-Key, Sec-Websocket-Origin, Sec-Websocket-Protocol, Sec-Websocket-Version, Security-Scheme, Server, Set-Cookie, Set-Cookie2, SetProfile, SoapAction, Status, Status-URI, Strict-Transport-Security, SubOK, Subst, Surrogate-Capability, Surrogate-Control, TCN, TE, TRACE, Timeout, Title, Trailer, Transfer-Encoding, UA-Color, UA-Media, UA-Pixels, UA-Resolution, UA-Windowpixels, URI, Upgrade, User-Agent, Variant-Vary, Vary, Version, Via, Viewport-Width, WWW-Authenticate, Want-Digest, Warning, Width, X-Content-Duration, X-Content-Security-Policy, X-Content-Type-Options, X-CustomHeader, X-DNSPrefetch-Control, X-Forwarded-For, X-Forwarded-Port, X-Forwarded-Proto, X-Frame-Options, X-Modified, X-OTHER, X-PING, X-PINGOTHER, X-Powered-By, X-Requested-With </value>
          </header>
    </custom-headers>
  </restconf>

Obviously, you want to change the Access-Control-Allow-Origin section to match your own clients before going in to production, but setting it to * here works for development.

Axios and Restconf

In my project, I use Axios to run the Restconf calls. The challenge was to insert the correct Content-Type header in GET requests, because Axios per default would not set this, if it was sending a normal GET request.

This was achieved by providing an empty data variable to the Axios request.

myrequest() {
    const config = {
        data: {},
        headers: {
            'Content-Type': 'application/yang-data+json',
            'Accept': '*/*'
        },
        responseType: 'json'
    }
    const path = '/restconf/data/tailf-ncs:services/some/path'
    return Axios.get(path, config).then(response => response.data)
}

POST and any other request with an actual payload should not require this slightly hacky workaround.

Troubleshooting

One thing that confused me a bit when working with this was that the browser development console would log any RESTCONF error as a CORS error, even 404 errors would be logged as a CORS error. I would recommend you to keep an eye on the actual HTTP status code.

I had a few challenges getting the output back from NSO in case of any errors, but it's most likely just my own lack of experience with Axios.

In any case, this slightly hacky construction fetched the response. msg in this case refers to a .catch((msg) => {this.handleerror(msg)})

         handleerror(msg) {
             console.log(msg)
             console.log(msg.response)
             if ('errors' in msg.response.data) {
                this.nsomsg = msg.response.data.errors.error[0]['error-message']
             }
             // actual error handling ommitted
         },

Screenshot

For obvious reasons, I'm not going to publish any code from this, but as I find it pleasing to code frontend interfaces, I can just share a quick screenshot of the main configuration wizard, used to configure a demo L3VPN service. I'm showing both a wizard, and a direct view of the JSON representation of the YANG object that I send to NSO via RESTCONF POST.

/images/nso-demo-screenshot.png

comments powered by Disqus