Leveraging Meraki APIs, you can cloud manage your network with the ServiceNow platform!

In this article, we discuss what it would take to build your own application. To do this, we will explore a scenario in which a service provider would like to provide a custom Meraki tool where their operators can easily provision a network. The tool will consist of a few forms to enter the site details, bind a newly created network to a template and then add devices. The results will be stored into a database table for use with other ServiceNow services.

Install the Demo App

Setup Steps

Prerequisites

Meraki Dashboard API key
ServiceNow instance

Setup

  • Login to ServiceNow STUDIO
  • Load Application
  • Select the newly created Meraki application
  • Test the UI
    • UI Pages
      • Endpoint: x_170302_global_index.do (or your global scope name)
  • Update the API key
    • Tables
      • meraki_configs
        • Add a record
          • name: default     (All API calls will use the record “default”)
          • api_key: yourApiKey
          • base_url: https://api.meraki.com/api/v0
            • Note: if you are unable to make changes, use the shard number URL, seen when logged into the Meraki Dashboard. (https://n149.meraki.com~)
  • The UI should now load the organizations that your API key has access to.

 

Development

In this scenario, we will use ServiceNow to host our frontend application and provide APIs to interact with the Meraki and ServiceNow platforms.

A basic understanding of JavaScript and Angular will be helpful to understand the details of the code. The goal of this project is to explain the art of the possible. By the end of this guide, you should have a better understanding of how you can build your own solution using the techniques described here.

Objectives

  • Create a network
  • Bind network to configuration template
  • Add devices to inventory
  • Add devices to network
  • Log messages to UI
  • Log network and device details to ServiceNow Glide Table

Tool Design

To understand what the tool will look like and the data required, we begin by creating a mockup of the application.

Some information will be user supplied, whereas other data will be dynamically provided from the Meraki Dashboard API.

The APIs have been listed in the order required for each operation. The results of these API calls will be used to populate the options and commit the changes.

 

API Constraints

  • API key must be hidden from the client application
  • Meraki API web apps must be proxied through a backend server (read more about CORS)
  • The API calls must respect rate limits

ServiceNow STUDIO

To begin our development, ServiceNow provides an IDE for building applications within their platform called STUDIO. You can use their graphical editor or write your own HTML/JavaScript code. By default, the data and layout are managed through <jelly> tags, jQuery and parsing through XML with their editor. The alternative approach is to use your own environment to import your code and libraries. In this guide we will be using our own environment to develop the front-end code using Angular as the UI framework.

Application Framework

  • Client-side
    • Angular Web App
  • Server-side
    • Meraki Proxy
    • Glide Table
    • Configs

There are a few pieces to this application. The client needs to interact with the various systems, but we must make the real API calls from the server. This will allow us to hide the API key and control access to the endpoints.

Client-side

This will be presented as an Angular web app within ServiceNow dashboard. Much of the logic will take place here, as we need to make several API calls, parse through the data and manage the state of the information on the screen.

This tool will use the ng-snow open source project to configure the ServiceNow application and build the initial Angular app. If you want to build an application from scratch, this is a good place to start. Alternatively, you can clone the sample code for this project and initialize it. The project generator uses webpack and some custom scripts to build the application and link it to your ServiceNow instance.

Project Source Code

Clone and setup the project using the source code provided and the following commands.

Meraki Network Creator Angular UI

Build Project
git clone https://github.com/dexterlabora/snow-angular-network-creator-ui.git snowMeraki
cd snowMeraki
npm install
npm run setup

The setup will ask a number of questions about your environment. If all goes well, you should have a new application setup in ServiceNow and a new application ready to start.

Local Dev

The web app uses webpack to manage dependencies, optimization and run a local development server. To start using the application run the following command and then open your browser to http://localhost:4200

npm start

Deploy

To upload your local changes to ServiceNow, run the following command.

npm run deploy

The URL for the application can be found by clicking on the newly created index file in the STUDIO editor.

Server-side

The server-side portion will present two Scripted REST APIs into our application, meraki-proxy and glide-table. Each will have various endpoints to perform our service operations.

We also need to store some environment variables in a configuration table called meraki_configs. The API will use information in this table to configure the various API calls. The API calls will use the record with the name default as the source of config settings.

Meraki Proxy

This is where we will create endpoints that mirror the Meraki API standard. Each endpoint will parse the parameters and body, attach the API key header and proxy the API call to Meraki. Additional logging and access control can be enforced at this step.

Based on our mockup, we identified which API endpoints will need to be created. These endpoints are called Scripted REST Resources. Follow the link for detailed instructions on creating an endpoint.

To get an idea of how to create these endpoints, lets explore the endpoint for binding a template to a network. The method is of type POST. The relative path mirrors the same pattern as the official Meraki API, where the networkId is a parameter. The processing script will then extract this parameter from the path and parse the body. The headers are defined and the API call is then submitted to the Meraki API. Once a response is received, it is relayed back to the client application.

The additional endpoints will need to be setup is a similar way with minor adjustments to the relative path and the parameter variable names within the script.

Bind a Template

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    try {
        var apiKey = "2f301bccd61b6cBOGUSBOGUSd3f76e5eb66ebd170f"; // Sandbox, hard coded for dev
        var baseUrl = "https://n149.meraki.com/api/v0"; // Hard coded shard number
        var path = request.pathParams;
        // https://{{shard}}.meraki.com/api/v0/networks/{{networkId}}/bind
        var url = baseUrl + "/networks/" + path.networkId + '/bind';
        

        // API call options
        var req = new sn_ws.RESTMessageV2();
        req.setHttpMethod("post");  
        req.setRequestHeader("X-Cisco-Meraki-API-Key", apiKey);
        req.setRequestHeader("Content-Type", "application/json");
        req.setEndpoint(url); 

        // Copy user request body to Meraki API

        var requestBody = request.body;
        var requestString = requestBody.dataString;
        var requestParsed = {};
        requestParsed = JSON.parse(requestString);
        req.setRequestBody(requestString);    

        // Call API
        var res = req.execute();

        // Response data
        var httpStatus = res.getStatusCode(); // FYI
        var httpResponseContentType = res.getHeader('Content-Type');
        var resBody = res.getBody();

        // Check if body is JSON or a string
        var resBodyFinal;
        try { 
            var parsedBody = JSON.parse(resBody);
            resBodyFinal = parsedBody;
        } catch (ex) { 
            resBodyFinal = resBody;
        }

        return resBodyFinal;

    

        // Return API call results
        /*
        if(typeof resBody === 'object'){
            return JSON.parse(resBody);
        }else{
            return resBody;
        }
        */
        
        /*
        if (httpStatus == 200 && httpResponseContentType == 'application/json') {
            return JSON.parse(resBody); 
        }else{
            return resBody;
        }
        */

    }
    catch (ex) {
        // handle error
        return ex;
    }
})(request, response);

 

Glide Table

ServiceNow uses the Glide Stack to interact with the ServiceNow database. We will create custom APIs to trigger Glide table updates. As a side note, the Glide system provides a native table API that could potentially be used, but by creating our own API handler, we can adjust the data before committing any changes.

glideTable/newDevice POST

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    try {

        // Copy user request body
        var requestBody = request.body;
        var requestString = requestBody.dataString;
        var requestParsed = {};
        requestParsed = JSON.parse(requestString);

        // Glide Table Logic
        var gr = new GlideRecord('x_170302_global_u_cmdb_ci_cisco_meraki');
        //query on the name field for network names
        gr.addQuery('requestParsed.name', 'SAMEAS', 'name');
        gr.query();

        if (gr.next()) {
            gr.name = requestParsed.name;
            gr.correlation_id = requestParsed.id
            gr.location = requestParsed.address;
            gr.update();
        } else {
            gr.initialize();
            gr.serial_number = requestParsed.serial;
            gr.name = requestParsed.name;
            gr.correlation_id = requestParsed.id;
            gr.model_id = requestParsed.model;
            gr.location = requestParsed.address;
            gr.insert();
        }

        // send data back to client
        return {
            message: "Table Updated",
            network: requestParsed
        };
    }
    catch (ex) {
        // handle error
        return ex;
    }
})(request, response);

Client Angular App

The Angular app is comprised of components to separate our page logic into smaller pieces. For instance, the claim component will present a form and table to manage network devices. The HTML file is responsible for the view, and the TypeScript file will handle the form data and trigger any backend services.

There are also a number of services that will abstract the interaction with the server APIs. So instead of making the API call directly within the component script, a simple function will be exposed for each API endpoint this.merakiService.getOrgs() . This means the component code can remain focused on the local tasks and outsource the API code to the services. This also means these API endpoints can easily be used in various components without duplicating code. In more complex applications, you could use the service to manage the overall state of an application.

The app is launched using the index.htmlfile. The file is automatically generated by our setup script and contains links to the packaged JavaScript files. When we run npm run build , the webpack tool will process the source code and optimize it for use in a normal browser. The HTML file respects the <jelly> tag requirements and dynamically creates the links.

 

Code Scaffolding

 

.
├── app
│   ├── app.component.css
│   ├── app.component.html
│   ├── app.component.ts
│   ├── app.module.ts
│   ├── claim
│   │   ├── claim.component.css
│   │   ├── claim.component.html
│   │   └── claim.component.ts
│   ├── home
│   │   ├── home.component.css
│   │   ├── home.component.html
│   │   ├── home.component.ts
│   ├── index.ts
│   ├── messages
│   │   ├── messages.component.css
│   │   ├── messages.component.html
│   │   ├── messages.component.spec.ts
│   │   └── messages.component.ts
│   ├── not-found
│   │   ├── not-found.component.css
│   │   ├── not-found.component.html
│   │   └── not-found.component.ts
│   └── services
│       ├── meraki.service.ts
│       ├── message.service.ts
│       ├── snow.interceptor.ts
│       └── table.service.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.css

 

 

 

 

Meraki Services

The Meraki Service is responsible for interacting with the server-side Meraki-proxy API. It will consist of several methods which make an API call to Meraki and return a JavaScript Promise. By doing this, we can wait for the response of the API call before making additional requests. This is important for triggering API workflows as well as throttling API calls.

Example: listOrganizations

Note: this.baseUrl is hard coded with the scope name for this application. This was defined in the initial setup of the Angular app. When asked for a scope name, it was called “global”. Ideally this would be a variable from a config file. 

import {Injectable, OnInit} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MessageService } from '../services/message.service';
import { TableService } from '../services/table.service';

@Injectable()
export class MerakiService implements OnInit {

constructor(
  private http: HttpClient, 
  private tableService: TableService,
  private messageService: MessageService) { 
    this.baseUrl = '/api/x_170302_global/meraki_proxy';
}

baseUrl: String;

listOrganizations = () => new Promise((resolve, reject) => {
    console.log('meraki service: listOrganizations');
      this.http.get<any>(this.baseUrl+'/organizations').subscribe( res => {
        console.log('listOrganizations res.result: ', res.result)
        if(res.result.errors){
          let error = res.result.errors[0];
          console.log('listOrganizations error: ', error);
          reject(error);
        }
        resolve(res.result);
      });
  }); ...

Then to use this service in our component script, we would do something like the following.

getOrgs(){
    this.meraki.listOrganizations().then(res => {
      console.log('Received Organizations', res);
      this.orgs = res;
    }).catch(err => {
      console.log('Bummer, there was an error');
    });
  }

note: we would need to import the service into our component file, module, etc. See the source code for complete details

Glide Service

This service is responsible for interacting with the server-side Glide Table API. It will define a base URL for API endpoints and configure the HTTP requests as promises just as we did in the Meraki Service.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MessageService } from '../services/message.service';

@Injectable()
export class GlideService {
 
  constructor(private http: HttpClient, private messageService: MessageService) { 
      this.baseUrl = '/api/x_170302_global/glide_table';
  }

  baseUrl: String;

  // API calls - Stores Meraki data into ServiceNow

  newNetwork = (data:any) => new Promise((resolve, reject) => {
    console.log('glide newData data ',data);
      this.http.post<any>(this.baseUrl+'/networks/new', data).subscribe(res => {
        console.log('glide newNetwork res.result: ', res.result)
        if(res.result.errors){
            let error = res.result.errors[0];
            console.log('glide newNetwork error: ', error)
            reject(error);
          }
          resolve(res.result);
        });
  });

Messaging Service

The messaging service will be used to post an event log to the user. By creating it as a service, we are able to easily import it into our components and send messages to a common display.

import { Injectable } from '@angular/core';

@Injectable()
export class MessageService {
  messages: string[] = [];

  add(message: string) {
    this.messages.push(message);
  }

  clear() {
    this.messages = [];
  }
}

Then use it in the components like this.

this.messageService.add("Creating Network...");

 

App Component

The app component is the first component loaded and will setup the initial layout for the application. It will contain the <router-outlet></router-outlet> tag, which is where our additional components will be inserted. It also contains the <app-messages></app-messages> tag to insert our event log via the message component.

HTML

<div class="container">
  
  <div class="panel">
    <toaster-container></toaster-container>
    <div class="panel-heading"><h1><img height="50px" src="https://raw.githubusercontent.com/dexterlabora/snow-angular-network-creator-ui/master/src/assets/transparent_legos.png"/><span> {{branding}}</span></h1></div>
    <div class="panel-body">
      <router-outlet></router-outlet>
    </div>
    <div class="col-md-6">
        <app-messages></app-messages>
    </div>
  </div>

</div>

Home component

The home component is the first route to be loaded and will contain our network creation form.

 

HTML

<div class="col-md-6">
  <h2>New Network Set-Up</h2>
  <form class="form-horizontal" [formGroup]="form" (ngSubmit)="onSubmit(form)">
    <div class="form-group">
      <label for="orgId" class="col-sm-4 control-label">Organization</label>
      <div class="col-sm-8">
        <select formControlName="orgId">
          <option *ngFor="let org of orgs" [value]="org.id">{{org.name}}</option>
        </select>
      </div>
    </div>

    <div class="form-group">
      <label for="template" class="col-sm-4 control-label">Template</label>
      <div class="col-sm-8">
        <select formControlName="templateId">
          <option *ngFor="let template of templates" [value]="template.id">{{template.name}}</option>
        </select>
      </div>
    </div>
...

Script

The first task of the component is to load the form using ngOnInit, which dynamically pulls in the list of organizations and triggers the template lookup for the first Org. Once a user selects an organization, the script will pull in a list of templates for the selected organization ID.

ngOnInit() {
    // setup form
    this.form = this.fb.group({
      ...
    });

    // watch for state changes
    this.onChanges();

    // get Organizations, templates to fill form selection
    this.getOrgs();
  }

  onChanges(): void {
    this.form.get('orgId').valueChanges.subscribe(val => {
      this.orgId = this.form.get('orgId').value;
      console.log('orgId', this.orgId);

      // get templates for this org    
      this.getTemplates();
    });
  }

The submit button will then trigger the various services to create the network.

onSubmit(f: FormGroup) {
    console.log('f.value', f.value);

    // Create Network
    let merakiData =   {
      "name": f.value.name,
      "tags": f.value.tags,
      "type": "wireless switch appliance" 
    }
    this.loading = true;
    this.messageService.add("Creating Network...");
    this.meraki.newNetwork(f.value.orgId, merakiData).then(res => {
      this.newNet = res;
      this.loading = false; 
      console.log('newNetwork this.newNet', this.newNet);
      this.messageService.add("Network Created: "+ this.newNet.id);
    }).then(() => {
      // Attach Template
...

Claim component

This component will perform two core tasks, claim devices & licenses into the inventory and add devices to the new network. The component uses the open source ngx-datatable project to build dynamic tables with our inventory data.

HTML

The table is built using the 3rd party component where the data sources our bound to our script data.

...
<div class="row">
    <div class="col-md-7">
      <ngx-datatable style="padding:20px; width: 100%" class="bootstrap" [rows]="rows" [columnMode]="'force'" [headerHeight]="50"
        [footerHeight]="50" [rowHeight]="'auto'" [limit]="5" [selected]="devicesToAdd" [selectionType]="'checkbox'" [selectAllRowsOnPage]="false"
        (select)='onSelect($event)'>
        <ngx-datatable-column [width]="20" [sortable]="true" [canAutoResize]="true" [draggable]="false" [resizeable]="true" [headerCheckboxable]="true"
          [checkboxable]="true">
        </ngx-datatable-column>
        <ngx-datatable-column *ngFor="let c of columns" [name]="c.name">
        </ngx-datatable-column>
      </ngx-datatable>
    </div>
  </div>
...

Script

Get inventory and populate table columns and rows. Once devices are selected, the devicesToAdd variable will be updated. Finally, a button will submit the devices to our APIs via our services.

getInventory (){
    // get Inventory
    this.devicesToAdd = [];
    this.loading = true;
    this.meraki.listInventory(this.orgId).then(res => {
      console.log('onInit lisInventory: res => ', res)
      this.loading = false;
      this.inventory = res;

      // FILTER inventory
      // Remove networks bound to a network
      this.inventory = this.inventory.filter(i => i.networkId === null);
      // Remove networkId property
      this.inventory.forEach(i => { delete i.networkId });
      // Remove publicIp property
      this.inventory.forEach(i => { delete i.publicIp });
      
      console.log('onInit lisInventory Filtered: res => ', this.inventory);

      //map data to rows and columns
      this.columns = [];
      this.rows = [];
      for (var prop in this.inventory[0]) {
        // skip loop if the property is from prototype
        if(!this.inventory[0].hasOwnProperty(prop)) continue;
        this.columns.push({
          // insert a space before all caps, then capatilize everything
          name: prop.replace(/([A-Z])/g, ' $1').toUpperCase()      
        })
      }
      console.log('columns ', this.columns);
      this.rows = this.inventory;
...
onAddDevices(){
    this.loading = true;
    this.meraki.addDevices(this.netId, this.devicesToAdd).then( res => {
      this.messageService.add("Device Add Complete");
      this.loading = false;
      //this.glideService.addDevices(this.netId, this.devicesToAdd);
      this.updateServiceNow();
      this.toasterService.pop('success', 'Devices Added');
      // refresh inventory list
      this.getInventory();
      }).catch(error => {
        this.loading = false;
        this.messageService.add("Device Add Error: "+ error)
        this.toasterService.pop('error', 'Devices Add Error', error);
      });
  }

Wrapping Up!

Hopefully you’ve learned how the Meraki tool works and inspired you to build your own solution based on your business requirements. There were many more details of how this application works, but that is largely an exercise in Angular development. I personally learned a lot about ServiceNow and Angular through my research and hope you do to.

Please share your suggestions and personal projects with us in our Community forum!

Meraki API Community