Overview

This document describes how to use docker tooling to develop a C++ based application which is packaged as a Container image.

The application contains a simple C++ program that does the below tasks:

  • displays the current system date and time
  • displays the factorial of 5 numbers
  • implements inheritance, polymorphism and a virtual function operations
  • implements the STL vector operations

Goals

  • Demonstrate various development tasks and concepts to implement a C++ based Container application package using docker tooling.
  • Application build process using docker build environment.

Sample Code

This sample application code is maintained here.

Clone this repo and use branch master. Find the application under directory 'simple-cpp-app'

$ git clone https://github.com/CiscoIOx/docker-demo-apps.git -b master
$ cd docker-demo-apps/simple-cpp-app

Some parts of the code may be reproduced in this document to give more context. But always use the above git repo and branch for the latest code.

Procedure Overview

  • Implementing a simple C++ application
  • Creating a docker image to setup the build environment
  • Run the image locally and compile C++ application
  • Creating a docker image with application binary contents
  • Testing the application locally using docker tools
  • Creating an IOx application package from the docker image
  • Deploying and testing on the target platform

Procedure

This section describes above steps with more details.

Before going through the tutorial, get familiar with cisco provided artifacts for an IOx app development.

Here, we have used 2 Dockerfiles for creating a C++ application package:

  • One Dockerfile for creating a docker image for the build environment. This is used for compiling a C++ application. This Dockerfile basically pulls a docker base image and installs the toolchain.
  • Other Dockerfile is for creating a docker image for deployment. This Dockerfile basically pulls a docker base image and copies the application binaries.

Note that the application examples are demonstrated for IR800 platform, but the same procedure will be applicable for all other supported IOx platforms. Developer needs to use the right docker base image as per the IOx platform. Refer for images supported platforms & corresponding base images

Implementing a simple C++ application

Writing a simple C++ application which includes various C++ features like STL operations, geometric shape example to explain inheritance, polymorphism and a virtual function operations.

Go to source code link for getting the complete source code. Only some of the important code snippets are shown in this document.

Creating a project directory
$ mkdir simple-cpp-app
$ cd simple-cpp-app
Writing a C++ application
$ mkdir -p dev/src
$ cd dev/src
$ ls
loop_app.cpp  test-cpp-app.cpp

Here is a code snippet to show how define the base class shape which contains the properties common to all the shapes:

/* Base class to represent the geometric shape */
class Shape
{
    public:
        /* Name to represent the type of a shape */
        string name;

        /* A pure virtual function to calculate the area */
        virtual float get_area(void) = 0;

        /* A pure virtual function to calculate the perimeter */
        virtual float get_perimeter(void) = 0;
};

The following snippet show how to define a rectangle class which is derived from the base class shape:

/* Rectangle class which is derived from a base class Shape */
class Rectangle : public Shape
{
    protected:
        /* Variables to store the length and width of a shape */
        float length, width;

    public:
        /* Rectangle class constructor */
        Rectangle(float input_length, float input_width)
        {   
            name = "Rectangle";
            length = input_length;
            width = input_width;
        }

        /* Find the area of a rectangle */
        float get_area()
        {   
            return (length * width);
        }

        /* Find the perimeter of a rectangle */
        float get_perimeter()
        {
            return ((2 * length) + (2 * width));
        }
};
Writing a loop application

Docker type applications are not based on /sbin/init to make the container alive and to start the init scripts. So, we need to use some loop application which will run in the background to make the container alive. This will be used in a Dockerfile as an application entry point. You will understand more details in the subsequent sections how to use this loop application.

Here is a simple loop application:

$ vi loop_app.cpp
/*
 * Sample code for basic CPP app which loops forever
 */

# include <stdio.h>
# include <unistd.h>

/*
 * Create a loop
 */
int main()
{
    /* loop */
    while(1)
    {   
        /* Sleep for a second for each iteration */
        sleep(10000);
    }   
    return 0;
}

$ ls
loop_app.cpp  test-cpp-app.cpp
$

Note that you can also use a simple shell script to create a loop app instead of using this C++ based loop application.

Creating a docker image to setup the build environment

Write a Dockerfile

This Dockerfile does the following tasks:

  • Pulls the docker base image
  • Installs the docker toolchain to compile the C++ application

Here is a Dockerfile:

$ cd simple-cpp-app/dev
$ vi Dockerfile
FROM devhub-docker.cisco.com/iox-docker/ir800/base-rootfs

COPY src /opt/src/
RUN opkg update
RUN opkg install iox-toolchain
RUN opkg install gdb
Creating a docker image

Login to the devhub using docker daemon before building a docker image. Instructions for devhub login

Now let us build an image from this Dockerfile.

$ docker build -t ir800-tools . 

Check if the image is successfully created using following command:

$ docker images
REPOSITORY                                             TAG                 IMAGE ID            CREATED             SIZE
ir800-tools                                        latest              bd716ae504f2        27 seconds ago      156.9 MB
$

Run the image locally and compile C++ application

Let us run the image locally to compile C++ application.

Note that here we are volume mounting the directory (-v ${PWD}/apps:/opt/share) from host pc in to docker container. This will help to get the compiled binary files to host machine.

$ cd simple-cpp-app/
$ docker run -v ${PWD}/apps:/opt/share -it ir800-tools /bin/sh
sh-4.3# cd /opt/src/
sh-4.3# ls -l
-rw-r--r--    1 root     root           209 Oct 27 20:42 loop_app.cpp
-rw-r--r--    1 root     root          8389 Oct 14 18:42 test-cpp-app.cpp
sh-4.3# which g++
/usr/bin/g++
sh-4.3# ls -l /usr/bin/g++
lrwxrwxrwx    1 root     root            21 Oct 14 22:17 /usr/bin/g++ -> x86_64-poky-linux-g++
sh-4.3# g++ test-cpp-app.cpp -o test-cpp-app    
sh-4.3# g++ loop_app.cpp -o loop_app
sh-4.3# 
sh-4.3# ls -l
-rwxr-xr-x    1 root     root         11123 Oct 28 01:10 loop_app
-rw-r--r--    1 root     root           238 Oct 28 01:10 loop_app.cpp
-rwxr-xr-x    1 root     root         56523 Oct 28 01:08 test-cpp-app
-rw-r--r--    1 root     root          8389 Oct 14 18:42 test-cpp-app.cpp
sh-4.3# 
sh-4.3# cp loop_app ../share/
sh-4.3# cp test-cpp-app ../share/
sh-4.3# cd ..
sh-4.3# cd share/
sh-4.3# pwd
/opt/share
sh-4.3# ls -l
-rwxr-xr-x    1 root     root         11123 Oct 28 01:11 loop_app
-rwxr-xr-x    1 root     root         56523 Oct 28 01:11 test-cpp-app
sh-4.3# 
sh-4.3# exit 

Creating a docker image with application binary contents

Now we are ready with the application binaries. Next step is creating a docker image with these application binaries.

Write a Dockerfile

This Dockerfile does the following tasks:

  • Pulls the docker base image
  • Copies the application binary contents on to base image
  • Specifies the application entry point

Here is a Dockerfile content:

$ cd simple-cpp-app/
$ vi Dockerfile
FROM devhub-docker.cisco.com/iox-docker/ir800/base-rootfs

COPY apps /opt/apps/
RUN opkg update
RUN opkg install libstdc++
CMD ["/opt/apps/loop_app"]
Creating a docker image

Now let us build an image from this Dockerfile.

$ docker build -t ir800-base-image . 

Check if the image created successfully

$ docker images
REPOSITORY                                             TAG                 IMAGE ID            CREATED             SIZE
ir800-base-image                                       latest              bbb013d0118b        14 minutes ago      14.99 MB
$

Testing the application locally using docker tools

$ docker run -it ir800-base-image /bin/sh
/ # cd /opt/apps/
/opt/apps # ls -l
-rwxr-xr-x    1 root     root         11123 Oct 28 01:11 loop_app
-rwxr-xr-x    1 root     root         56523 Oct 28 01:11 test-cpp-app
/opt/apps # ./test-cpp-app 
Demonstrating the C++ application
####################################################
Current date and time: Fri Oct 28 01:36:00 2016

Executing the factorial program: 
factorial of 0=1
factorial of 1=1
factorial of 2=2
factorial of 3=6
factorial of 4=24

Executing the Polymorphism, Inheritance operations:
---------------------------------------------------
Geometric Shape Class Operations: 

Shape type: Rectangle
area: 30
perimeter: 22

Shape type: Circle
area: 50.24
perimeter: 25.12

Deleting: Rectangle
Deleting: Circle

Executing the STL vector operations:
------------------------------------
Vector elements:
This is a C++ example STL program 

Vector statistics:
size:     7
maxsize:  2305843009213693951
capacity: 7

Vector elements after swapping the 4th and 5th elements:
This is a C++ STL example program 

Vector elements after inserting the new elements:
This is a C++ STL example vector program 

Vector statistics with new elements:
size:     8
maxsize:  2305843009213693951
capacity: 8
/opt/apps #
/opt/apps #exit
$ 

Package descriptor file

Package descriptor file specifies requirements for the application. For docker type applications, creating a package descriptor file is optional. ioxclient creates a default package descriptor file while creating IOx docker application package. If the developer want to add custom entries in a package descriptor file, he can add those entries using docker's LABEL directive. Ioxclient uses those labels, add it in a package descriptor file during creation of an IOx docker application package.

Refer ioxclient page to learn how to use docker Label's and package.yaml.

Here is a sample application descriptor (package.yaml) file looks like:

Note: If you are using application descriptor file for non x86 architecture, Refer the docker images table for appropriate value of "cpuarch" in yaml file

Ex: For ie4k platform use the 'cpuarch' as 'ppc' in following yaml file

$ vi package.yaml

descriptor-schema-version: "2.2"

info:
  name: SampleNodeApp
  description: "Simple Docker Style C++ Application"
  version: "1.0"
  author-link: "http://www.cisco.com"
  author-name: "Cisco Systems"

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

    network:
      -   
        interface-name: eth0
        ports:
            tcp: [8000]
            udp: [10000]

# Specify runtime and startup
  startup:
    rootfs: rootfs.tar
    target: ["/opt/apps/loop_app"]

Few things to notice:

  • Descriptor schema version is 2.2. This is the minimum version that supports docker style apps.
  • The required port (8000) to be opened is specified under network->ports.
  • rootfs.tar is the name of the file containing the docker image (output of docker save command). More details in the next sections.
  • Command to be run when starting up the app is ["/opt/apps/loop_app"]. Note that here loop_app is a loop application binary which is used to make the container alive. This is because container is not based on /sbin/init so that application will stop if we don’t use the loop application.

Creating an IOx application package

Now that we have created the required docker image (ir800-base-image), it is time to create an IOx application package from these artifacts.

ioxclient (>= 1.4.0) provides a convenience command that generates an IOx application package from a docker image and package.yaml file.

Refer this page for ioxclient usage: ioxclient

Create an empty directory ex. 'app_package' and execute the ioxclient command in this directory

Make sure the profile for target platform is created and activated before creating package

Let us use this command.

$ cd simple-cpp-app/
$ ls
apps  dev  Dockerfile

$ mkdir app_package
$ cd app_package/
$ ioxclient docker package -a ir800-base-image .
Currently active profile :  default
Command Name: docker-package
No package type specified, but auto flag is set
cpu arch is x86, generating docker app x86_64
Generating docker style app
The Image is better left in it's pristine state
Warning: package.yaml not present in project folder. Will attempt to generate one.
Generation complete. Validating generated descriptor file.

Validating descriptor file /tmp/desc034467301 with package schema definitions
Parsing descriptor file..
Found schema version  2.2
Loading schema file for version  2.2
Validating package descriptor file..
File /tmp/desc034467301 is valid under schema version 2.2
Package MetaData file was not found at  /home/<user>/projects/test_sde/simple-cpp-app/app_package/.package.metadata
Wrote package metadata file :  /home/<user>/projects/test_sde/simple-cpp-app/app_package/.package.metadata
Checking if package descriptor file is present..
Validating descriptor file /home/<user>/projects/test_sde/simple-cpp-app/app_package/package.yaml with package schema definitions
Parsing descriptor file..
Found schema version  2.2
Loading schema file for version  2.2
Validating package descriptor file..
File /home/<user>/projects/test_sde/simple-cpp-app/app_package/package.yaml is valid under schema version 2.2
Created Staging directory at :  /tmp/567996416
Copying contents to staging directory
Checking for application runtime type
Couldn't detect application runtime type
Creating an inner envelope for application artifacts
Generated  /tmp/567996416/artifacts.tar.gz
Calculating SHA1 checksum for package contents..
Parsing Package Metadata file :  /tmp/567996416/.package.metadata
Wrote package metadata file :  /tmp/567996416/.package.metadata
Root Directory :  /tmp/567996416
Output file:  /tmp/895847263
Path:  .package.metadata
SHA1 : d25f6e2589ed32574d9d5db67164186d3365d33c
Path:  artifacts.tar.gz
SHA1 : 57406a263a5876fbabea4b2badc3c20574279bf1
Path:  package.yaml
SHA1 : 96a1484d1a6c505cc2fad0c2f7ec8abba57ce342
Generated package manifest at  package.mf
Generating IOx Package..
Package docker image ir800-base-image at /home/<user>/projects/test_sde/simple-cpp-app/app_package/package.tar
$

The package.tar is an IOx application package that can be used to deploy on an IOx platform.

Here is a simple-cpp-app directory structure on Host PC after successful building of IOx application package:

$ cd simple-cpp-app
$ tree 
.
├── apps
│   ├── loop_app
│   └── test-cpp-app
├── app_package
│   ├── package.tar
│   └── package.yaml
├── dev
│   ├── Dockerfile
│   └── src
│       ├── loop_app
│       ├── loop_app.cpp
│       └── test-cpp-app.cpp
└── Dockerfile

4 directories, 9 files
$

Deploying and testing on the target platform

Ensure that your target platform supports docker style applications. Refer to Platform Matrix for details about your platform. This tutorial uses IR829 as the target IOx platform.

We will use ioxclient to demonstrate deployment and lifecycle management of this application. You can do the same using Local Manager or Fog Director.

Add your device to ioxclient
$ ioxclient  profiles create
Active Profile :  default
Enter a name for this profile : ir800demo
Your IOx platform's IP address[127.0.0.1] : <<IOx device IP>>
Your IOx platform's port number[8443] :
Authorized user name[root] : root
Password for root :
Local repository path on IOx platform[/software/downloads]:
URL Scheme (http/https) [https]:
API Prefix[/iox/api/v2/hosting/]:
Your IOx platform's SSH Port[2222]: 2022
Activating Profile  ir800demo
Install the app on the target platform
$ ioxclient app install demoapp app_package/package.tar
Currently active profile :  ir800demo
Command Name: application-install
Saving current configuration
Installation Successful. App is available at : https://<IOx device IP>:8443/iox/api/v2/hosting/apps/demoapp 
Successfully deployed
Activate the app
$ ioxclient app activate demoapp
Currently active profile :  ir800demo
Command Name: application-activate
App demoapp is Activated
Start the app
$ ioxclient app start demoapp
Currently active profile :  ir800demo
Command Name: application-start
App demoapp is Started
Access the app console
$ ioxclient app console demoapp
Currently active profile :  ir800demo
Command Name: application-console
Console setup is complete..
Running command : [ssh -p 2022 -i demoapp.pem appconsole@<IOx device IP>]
SSH client is not present on the platform.
Attempting to connect using native SSH implementation..., press Ctrl+C to exit

Connecting to appconsole@172.27.103.65:2022 using pem file demoapp.pem
/ # cd /opt/apps
/opt/apps # ls -l
-rwxr-xr-x    1 root     root         11123 Oct 28 01:11 loop_app
-rwxr-xr-x    1 root     root         56523 Oct 28 01:11 test-cpp-app
/opt/apps # ./test-cpp-app
Demonstrating the C++ application
####################################################
Current date and time: Fri Oct 28 01:58:26 2016

Executing the factorial program: 
factorial of 0=1
factorial of 1=1
factorial of 2=2
factorial of 3=6
factorial of 4=24

Executing the Polymorphism, Inheritance operations:
---------------------------------------------------
Geometric Shape Class Operations: 

Shape type: Rectangle
area: 30
perimeter: 22

Shape type: Circle
area: 50.24
perimeter: 25.12

Deleting: Rectangle
Deleting: Circle

Executing the STL vector operations:
------------------------------------
Vector elements:
This is a C++ example STL program 

Vector statistics:
size:     7
maxsize:  2305843009213693951
capacity: 7

Vector elements after swapping the 4th and 5th elements:
This is a C++ STL example program 

Vector elements after inserting the new elements:
This is a C++ STL example vector program 

Vector statistics with new elements:
size:     8
maxsize:  2305843009213693951
capacity: 8
/opt/apps # 

Summary

As part of this tutorial we saw:

  • Using docker tools and base image to build a C++ app.
  • Packaging a docker image into an IOx application package.
  • Deploying this package to an IR800 router and testing the application functionality.