Tutorial: Build sample Docker type IOx app which consumes GPS service

In this tutorial, we will go through quick start quide on how to build a sample docker type application which will access GPS service, using docker development environment and package it to an IOx application. For more details on what are the different types of IOx application supported, refer this section.

Requirements

  • Supported development environment architecture for ioxclient tool
    • amd64
    • darwin_386 (MACOS 10)
    • darwin_amd64
    • linux_386
    • linux_amd64
    • windows_386
    • windows_amd64
  • Minimum Docker version 1.10 (API 1.22)
  • Docker daemon up and running
  • Cisco IR800 device with GPS antenna.
  • IOxCore and IOxGPS service running on the device. To know more about how to deploy and get the GPS service running on IR800 refer to this section.

Setup Docker development environment for IOx

Refer this section to setup docker daemon on your development machine for authentication with Cisco hosted DevHub repository.

Create Dockerfile

Create a file called Dockerfile and copy the below contents into it.You can chose any baserootfs comptabile with Linux 4.1 kernel x86 arch.IOx base rootfs is hosted on cisco dev as well.Below oneis for reference

FROM gliderlabs/alpine:3.3
RUN apk add --update \
    python \
    py-pip \
  && rm -rf /var/cache/apk/*
RUN pip install --upgrade pip
RUN pip install websocket-client
RUN pip install oauth
RUN pip install requests==2.10.0

ADD GPS_Config.json /usr/share/gpsapp/resources/GPS_Config.json
ADD oauth.py /usr/share/gpsapp/oauth.py
ADD gps_app.py /usr/share/gpsapp/gps_app.py
ADD gps_api.py /usr/share/gpsapp/gps_api.py
ADD gpsapp.sh /usr/share/gpsapp/gpsapp.sh
  • Lets create a simple python application which configures the gps service to stream location co-ordinates every 5 seconds and prints the output to a log file called app.log.

IOx exposes location of peristent log directory for apps via environment variable CAF_APP_LOG_DIR. Default location of this dir is /data/logs. Refer this section for more environment variables exposed by IOx to applications.

Printing to console can overflow the buffer and hang the target application. It is recommended not to print infinitely to console.

Copy below contents into a file called gps_app.py.

# use python2
# pip install websocket_client
# usage: webclient.py <websocket-url> <access-token>
#!/usr/bin/python

import sys
import ssl
import os
import signal
import websocket
import oauth
from datetime import datetime
import time
import gps_api
import traceback
from threading import Thread

def web_socket_sub( protoName, topicName):
    print "Started thread"
    nbi_label = "nbi"
    nbi_host = os.environ[nbi_label+"_IP_ADDRESS"]
    nbi_port = os.environ[nbi_label+"_TCP_9999_PORT"]
    topic = topicName
    url = "wss://%s:%s/api/v1/mw/topics/%s" % (nbi_host, nbi_port, topic)

    websocket.enableTrace(True)
    try:
        ws = websocket.create_connection(                            
                   url,                                                  
                   header = ["Authorization: Bearer " + oauth.access_token],
                   sslopt= {"cert_reqs": ssl.CERT_NONE}
                 ) 
    except Exception, err:                                                              
        print Exception, err
        time.sleep(2)

    while 1:
        try:
            print "RCVD<%s>: %s" %  (protoName, ws.recv())
        except Exception, err:                                                              
            print Exception, err


def handler(signum, frame):
    print 'Signal handler called with signal', signum
    sys.exit(0)

signal.signal(signal.SIGTERM, handler)
signal.signal(signal.SIGINT, handler)

gps_conf = gps_api.get_gps_config()
if gps_conf == None :
    print("GPS Config Read failed")
    sys.exit(0)
ret = gps_api.configure_gps("GPS_Config.json")
if ret == -1 :
    print("GPS Config failed")
    sys.exit(0)
print("GPS Configuration successfully done")
    
try:
   t = Thread(target=web_socket_sub, args=("GPS", "gps"))
   t.start()
except:
   print "Error: unable to start thread"

t.join()

Copy below contents into a file called gps_api.py in the same directory as gps_app.py.

import os
import httplib
import time
import oauth
import urllib2

def get_gps_config( ):
    nbi_label = "nbi"
    nbi_host = os.environ[nbi_label+"_IP_ADDRESS"]
    nbi_port = os.environ[nbi_label+"_TCP_9999_PORT"]

    print("%s %s" % (nbi_host, nbi_port)) 
    headers = {
        "Authorization": "Bearer "+oauth.access_token
    }

    con = httplib.HTTPSConnection("%s:%s" % (nbi_host, nbi_port))
    con.request(
        "GET", 
        "/api/v1/mw/gps/config",
    None,
        headers
    )
    response = con.getresponse()
    print("%s" % (response.read()))
    if response.status != 200 :
        print ("Failed")
        con.close()
        return None 
    print ("Success")
    con.close()
    return response


def configure_gps( fileName):
    nbi_label = "nbi"
    nbi_host = os.environ[nbi_label+"_IP_ADDRESS"]
    nbi_port = os.environ[nbi_label+"_TCP_9999_PORT"]

    with open ("resources/"+fileName, "r") as myfile:
        payload=myfile.read()
    print("payload %s" % (payload)) 
    send = {}
    headers = {"Content-Type": "application/json",
               "Authorization": "Bearer "+oauth.access_token,
               "Accept": "text/plain"}

    con = httplib.HTTPSConnection("%s:%s" % (nbi_host, nbi_port))
    con.request(
        "POST", 
        "/api/v1/mw/gps/config",
        payload,
        headers
    )
    response = con.getresponse()
    print("%s %s %s" % (response.status, response.reason, response.read())) 
    if response.status != 200 :
        print ("Failed")
        con.close()
        return -1
    print ("Success")
    con.close()
    return 0

def get_gps_location( ):
    nbi_label = "nbi"
    nbi_host = os.environ[nbi_label+"_IP_ADDRESS"]
    nbi_port = os.environ[nbi_label+"_TCP_9999_PORT"]

    headers = {
            "Authorization": "Bearer "+oauth.access_token
            }

    con = httplib.HTTPSConnection("%s:%s" % (nbi_host, nbi_port))
    con.request(
        "GET", 
        "/api/v1/mw/gps/location",
        headers
    )
    response = con.getresponse()
    print("%s %s" % (response.status, response.reason)) 
    if response.status != 200 :
        print ("Failed")
        con.close()
        return None 
    print (response.read())
    con.close()
    return response


Copy below contents into a file called oauth.py in the same directory as gps_app.py.

import os
import base64
import urlparse
import httplib
import json
from urllib import urlencode
import ssl 
ssl._create_default_https_context = ssl._create_unverified_context

client_id = os.environ["OAUTH_CLIENT_ID"]
client_secret = os.environ["OAUTH_CLIENT_SECRET"]
server_ip = os.environ["OAUTH_TOKEN_SERVER_IPV4"]
server_port = os.environ["OAUTH_TOKEN_SERVER_PORT"]
api_path = os.environ["OAUTH_TOKEN_API_PATH"]
token_url = "https://"+server_ip+":"+server_port+api_path


print "client_id: "+client_id
print "client_secret: "+client_secret
print "token_url: "+token_url

tokens = urlparse.urlparse(token_url)
if tokens.scheme == 'https':
    con = httplib.HTTPSConnection(tokens.netloc)
else:
    con = httplib.HTTPConnection(tokens.netloc)

con.request(
    "POST", 
    tokens.path, 
    urlencode({'grant_type': 'client_credentials'}),
    {
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": "Basic " + base64.b64encode("%s:%s" % (client_id, client_secret))
    }
)
response = con.getresponse()
if response.status / 100 != 2:
    raise Exception("oauth token url returned %s" % response.status)

access_token = json.loads(response.read())['access_token']
con.close()

Copy below contents into a file called gpsapp.sh in the same directory as gps_app.py.

#!/bin/sh

if [ -f /data/.env ]
then
    source /data/.env
fi

start_app() {
    cd /usr/share/gpsapp
    python gps_app.py > /data/logs/app.log 2>&1
}

_term() {
  echo "Caught SIGTERM signal!"
  kill -TERM "$pid" 2>/dev/null
}                              
                               
trap _term SIGTERM

case "$1" in
start)
        echo -n "Starting app gpsapp"
        start_app
        echo "done."
        ;;

stop)
        echo -n "Stopping app phapp"
        pidof python | xargs kill -15
        : exit 0
        echo "done."
        ;;

force-reload|restart)
        echo "Reconfiguring phapp"
        echo "done."
        ;;

*)
        echo "Usage: /etc/init.d/gpsapp {start|stop}"
        exit 1
        ;;
esac

exit 0

Copy below contents into a file called GPS_Config.json in the same directory as gps_app.py.

{    
    "gps-interval-seconds": 5,             
    "streaming-enabled": true,                 
    "topic" : "gps" 
}

Build Docker image

Now build docker image named gpsapp with version 1.0 using previously created dockerfile. Prepend sudo if the docker build command fails due to permission restrictions.

$ docker build -t gpsapp:1.0 .

Verify application dependencies in docker image - optional

You can verify if the docker image now has all the applicantions's dependant libraries or modules by running docker container locally in developement environment. Execute below command to run docker container and getting access to shell session inside the container. For this tutorial, make sure that application binary is copied under /var/helloworld/ directory.

$ docker  run -it gpsapp:1.0 /bin/sh

Create IOx package descriptor file

IOx package descriptor file contains metadata about the application, minimum resource requirements for running the application. Refer this section for more detailed description of various attributes and schema for package descriptor contents. Note: In some cases target field needs to be specified as an array like in case of python based target application, specify target value like ["python", "/usr/bin/app.py"].

Copy below contents into a file called package.yaml in the same directory as gps_app.py.

descriptor-schema-version: "2.5"

info:
  name: GPS application
  description: Sample Application that makes use of GPS Protocol Handler
  version: "1.0.0"
  author-link: "http://www.cisco.com"
  author-name: "Cisco Systems"

app:
  # Indicate app type (vm, paas, lxc etc.,)
  cpuarch: "x86_64"
  type: docker

  depends-on:
    packages:
      -
        name: "IOxGPS"
        version: "1.5.0"

  resources:
    profile: custom
    cpu: "100"
    disk: "20"
    memory: "30"
    oauth: [OauthClient]

    network:
     -
       interface-name: eth0

  # Specify runtime and startup
  startup:
    rootfs: rootfs.tar
    target: ["/usr/share/gpsapp/gpsapp.sh start"]

Create final IOx application package

Download and install latest version of ioxclient for your development environment from this location. Setup ioxclient device profile by configuring device IP, credentials and SSH port.

bash$ ioxclient profile create

Use below ioxclient command to build final IOx application package named package.tar. Need to pass same docker image name and version that has been previously used to build the image. Prepend sudo if the ioxclient command fails due to permission restrictions.

bash$ ioxclient docker package gpsapp:1.0 .

Deploy/Activate/Start the application

Now you can deploy the application onto a physical or virtual IOx device using either of the clients ioxclient or Local Manager or Fog Director. You can access device Local Manager UI using the URL path https://:8443. Lets use ioxclient to deploy this application to a device. Execute below ioxclient commands to setup device profile, install, activate and start the application. Refer profiles and app management for more ioxclient details. Note: Here we are using default attributes for application activation.Provide the right activation payload as mentioned in gps service page

bash$ ioxclient app install gps_app package.tar

bash$ ioxclient app activate --payload activation.json gps_app

bash$ ioxclient app start gps_app

Verify the application is running

Now lets console into the application and confirm that application is running successfully by logging the string at /data/logs/app.log. Use below ioxclient command to console into the application.

bash$ ioxclient app console gps_app

Tail the app log and you must see Location coordinates streamed every 5 seconds.

bash$tail -f /data/logs/app.log
RCVD<GPS>: {"message":{"altitude":"192.000000","latitude":"37.418072","longitude":"-121.919228","return-code":"200","timestamp":"Sat Nov 11 00:19:18 2017"},"topic":"gps"}
RCVD<GPS>: {"message":{"altitude":"192.000000","latitude":"37.418072","longitude":"-121.919228","return-code":"200","timestamp":"Sat Nov 11 00:19:23 2017"},"topic":"gps"}
RCVD<GPS>: {"message":{"altitude":"192.000000","latitude":"37.418072","longitude":"-121.919228","return-code":"200","timestamp":"Sat Nov 11 00:19:28 2017"},"topic":"gps"}
RCVD<GPS>: {"message":{"altitude":"191.000000","latitude":"37.418072","longitude":"-121.919228","return-code":"200","timestamp":"Sat Nov 11 00:19:33 2017"},"topic":"gps"}
RCVD<GPS>: {"message":{"altitude":"191.000000","latitude":"37.418072","longitude":"-121.919228","return-code":"200","timestamp":"Sat Nov 11 00:19:38 2017"},"topic":"gps"}

More sample applications