Appliance builder guide: webcam

Snappy Ubuntu Core is made from the ground up to allow appliance builders to produce their very own appliance by using the snappy packaging system to combine and configure building blocks that span from enablement to application level.

The Ubuntu Core 15.04 release delivers all the primitives that are needed to produce such an appliance and this guide will walk you through how this is done by making a webcam appliance out of your beaglebone, publish it to the store and produce a preinstalled image that is ready to go.

As this guide makes intentional use of a very broad set of primitives provided by snappy Ubuntu Core 15.04, we recommend you make yourself familiar with how to setup a beaglebone as well as our various other guides and docs about the internals of a snappy system.

This guide will not cover details but focus on how to put such a system together and how to make a reproducible appliance out of it.

The webcam appliance overview

The webcam appliance we build is very simple. We will construct an appliance by combining the following elements:

  • Beaglebone board
  • Beaglebone u-boot
  • Generic Ubuntu Core 15.04 armhf enablement
  • Logitech USB Webcam
  • ubuntu-core 15.04
  • webcam-demo application from store

The webcam-demo application will come with a webserver that will serve static files from a data directory of our app on the snappy system as well as a daemon that takes a snapshot through the webcam every 10 seconds.

Further we want the webcam-demo to run fully sandboxed which means that by default it cannot control any devices exposed through /dev, but want to assign rights to access all /dev/video* devices that have a usb webcam manufactured by Logitech plugged in.

The following picture shows how a "System Builder" will fill the abstract architecture of the snappy Ubuntu Core system with concrete elements to construct the demo webcam appliance:

You can see at the bottom that we are using a beaglebone with a logitech webcam attached. The user would in the end be able to interact with this appliance by pointing a standard webbrowser at it to see the screenshots taken by the webcam.

Obviously, other webcams will also work, but since we want to show how to restrict access of the app to a subset of hot pluggable devices this guide will assume that the appliance builder wants to allow only logitech USB webcams to be used.

To realise the software stack that will fuel this appliance we will reuse the generic enablement part that we release and maintain as part of our ubuntu 15.04 release and will of course use Ubuntu Core as is. Fact is that appliance manufacturers don't need to and also cannot change Ubuntu Core and that all snappy appliances will share the same core and same lean and crystalline goodness we have put in there.

Since our appliance is meant to be a fixed function device that cannot be extended by the user, we consider it will not require a framework. It is simply an app that does what our appliance is supposed to do on top of pure ubuntu-core and enablement. Access to the webcam will in the end not be provided by the user, but will be preconfigured by the OEM as part of the system building exercise.

The Webcam Demo App will include all parts needed to implement the main experience of our appliance. In particular the pieces that are bundled in our app include a simple webbrowser that serves static files, a tool to capture snapshots from the webcam and a server that triggers camera snapshots every 10 seconds and publishes the latest image through the webserver included in the same snap.

Interactively building the appliance from scratch

Since the goal of this guide is to teach you how easy it is to build similar devices, we won't just present you with the solution and explain how that was done, but rather show how we built this appliance interactively from scratch.

We won't discuss how the actual webcam demo app has been developed and got packaged up in a snap. For this we recommend you check out the source itself and read one of the other tutorials and docs on app development for snappy instead to get the background.

To get started we just installed the beagleboard development image that we release both for 15.04 as well as our rolling release as explained on our getting started page.

Once up and running you can log in and start adding the building blocks you want and configure the system so the parts are configured right and deliver your very own appliance experience as you want it. So lets get started...

First log in and start the webcam-demo snap using the snappy command:

$ ssh ubuntu@webdm.local
Welcome to Ubuntu 15.04 (GNU/Linux 3.19.0-15-generic armv7l)

 * Documentation:  https://help.ubuntu.com/
Welcome to snappy Ubuntu Core, a transactionally updated Ubuntu.

 * See https://ubuntu.com/snappy

It's a brave new world here in snappy Ubuntu Core! This machine
does not use apt-get or deb packages. Please see 'snappy --help'
for app installation and transactional updates.

Last login: Wed Apr 22 21:55:46 2015 from XX.XXX.XX.XX
(BeagleBoneBlack)ubuntu@localhost:~$
Next we install the webcam-demo package from store and check the status of the service that we expect to see running:
(BeagleBoneBlack)ubuntu@localhost:~$ sudo snappy install webcam-demo
(BeagleBoneBlack)ubuntu@localhost:~$ sudo systemctl status -l webcam-demo_webcam-demo_1.0.2.service
● webcam-demo_webcam-demo_1.0.2.service
   Loaded: loaded (/etc/systemd/system/webcam-demo_webcam-demo_1.0.2.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2015-04-22 23:53:26 UTC; 2s ago
 Main PID: 1206 (webcam-webui)
   CGroup: /system.slice/webcam-demo_webcam-demo_1.0.2.service
           ├─1206 /bin/sh /apps/webcam-demo.canonical/1.0.2/bin/webcam-webui
           ├─1207 golang-static-http
           └─1234 sleep 10

Apr 22 23:53:26 localhost.localdomain systemd[1]: Started webcam-demo_webcam-demo_1.0.2.service.
Apr 22 23:53:26 localhost.localdomain systemd[1]: Starting webcam-demo_webcam-demo_1.0.2.service...
Apr 22 23:53:26 localhost.localdomain ubuntu-core-launcher[1206]: --- Opening /dev/video0...
Apr 22 23:53:26 localhost.localdomain ubuntu-core-launcher[1206]: Trying source module v4l2...
Apr 22 23:53:26 localhost.localdomain ubuntu-core-launcher[1206]: Error opening device: /dev/video0
Apr 22 23:53:26 localhost.localdomain ubuntu-core-launcher[1206]: open: Permission denied
Apr 22 23:53:26 localhost.localdomain ubuntu-core-launcher[1206]: Trying source module v4l1...
Apr 22 23:53:26 localhost.localdomain ubuntu-core-launcher[1206]: Error opening device: /dev/video0
Apr 22 23:53:26 localhost.localdomain ubuntu-core-launcher[1206]: open: Permission denied
Apr 22 23:53:26 localhost.localdomain ubuntu-core-launcher[1206]: Unable to find a source module that can read /dev/video0.
(BeagleBoneBlack)ubuntu@localhost:~$
What you see now is that the service is actually running, but that we have some problems with opening the /dev/video0 device, which is expected given that apps by default run in a very strictly confined sandbox here in snappy land. If you run the command again you will see that the server continues trying.
In case your app doesn’t print its problem on the console you can check what resources your app fails to access, but is blocked by snappy’s apparmor sandbox by inspecting syslog:
$ sudo grep DENIED /var/log/syslog | tail -n 1
Apr 23 00:14:31 localhost kernel: [ 3540.954993] audit: type=1400 audit(1429748071.644:277): apparmor="DENIED" operation="open" profile="webcam-demo.canonical_webcam-demo_1.0.2" name="/dev/video0" pid=2892 comm="fswebcam" requested_mask="wr" denied_mask="wr" fsuid=0 ouid=0
In our case this just confirms what we already suspected: our app cannot access /dev/video0. So let's get that fixed so we can confirm that our appliance app is any good; for this purpose snappy provides sysadmins a tool to grant privileges to apps to individual /dev/ nodes using our hw-assign primitive. Of course you need to be root, so simply run:
(BeagleBoneBlack)ubuntu@localhost:~$ sudo snappy hw-assign webcam-demo.canonical /dev/video0
'webcam-demo.canonical' is now allowed to access '/dev/video0'
Now if you go back and check the status of your our service again you will see that all has started working and that indeed our service is now capturing snapshots every 10 seconds:
sudo systemctl status -l webcam-demo_webcam-demo_1.0.2.service
● webcam-demo_webcam-demo_1.0.2.service
   Loaded: loaded (/etc/systemd/system/webcam-demo_webcam-demo_1.0.2.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2015-04-22 23:53:26 UTC; 28min ago
 Main PID: 1206 (webcam-webui)
   CGroup: /system.slice/webcam-demo_webcam-demo_1.0.2.service
           ├─1206 /bin/sh /apps/webcam-demo.canonical/1.0.2/bin/webcam-webui
           ├─1207 golang-static-http
           └─3429 sleep 10

Apr 23 00:21:27 localhost.localdomain ubuntu-core-launcher[1206]: /dev/video0 opened.
Apr 23 00:21:27 localhost.localdomain ubuntu-core-launcher[1206]: No input was specified, using the first.
Apr 23 00:21:27 localhost.localdomain ubuntu-core-launcher[1206]: Adjusting resolution from 384x288 to 352x288.
Apr 23 00:21:27 localhost.localdomain ubuntu-core-launcher[1206]: --- Capturing frame...
Apr 23 00:21:28 localhost.localdomain ubuntu-core-launcher[1206]: Captured frame in 0.00 seconds.
Apr 23 00:21:28 localhost.localdomain ubuntu-core-launcher[1206]: --- Processing captured image...
Apr 23 00:21:28 localhost.localdomain ubuntu-core-launcher[1206]: Fontconfig error: Cannot load default config file
Apr 23 00:21:28 localhost.localdomain ubuntu-core-launcher[1206]: Unable to load font 'sans': fontconfig: Couldn't find font.
Apr 23 00:21:28 localhost.localdomain ubuntu-core-launcher[1206]: Disabling the the banner.
Apr 23 00:21:28 localhost.localdomain ubuntu-core-launcher[1206]: Writing JPEG image to 'shot.jpeg'.
Just run it a few times to see it progressing. Also you should now be able to point your browser to your snappy appliance’s port 8080 and get the most recent snapshot by opening your browser like this:
$ firefox http://webdm.local:8080/shot.jpeg
Et voilà, seems we succeeded to interactively build our snappy appliance!
Before we go over to the next section to show how to make a reproducible appliance let’s take a quick look under the covers at what snappy hw-assign did and why this doesn't work for a reproducible appliance in all cases. Snappy hardware assignment basically works in two steps:
  1. identify and tag the device nodes that should be assigned to an app
  2. grant access by mapping those device nodes into a lightweight container world that our app launcher sets up for every app
The tagging is done through udev, and you can see that our hw-assign rule created a very simplistic rule for us that looks like this:
(BeagleBoneBlack)ubuntu@localhost:~$ cat /etc/udev/rules.d/70-snappy_hwassign_webcam-demo.canonical.rules
KERNEL=="video0", TAG:="snappy-assign", ENV{SNAPPY_APP}:="webcam-demo.canonical"
This obviously has a problem: the video0 name is not stable in a USB hotpluggable world. In particular it is not guaranteed that the same webcam will be mapped to the same kernel name which would make it hard to have multiple apps in parallel using different webcams. To allow system and appliance builders to make an explicit choice about what device can be assigned to what framework or app, we have a language that allows you to use udev-like statements to match and select the device nodes and assign them to snappy parts. For now this feature is reserved for appliance builders as part of the oem.snap; we will work on making nice CLI and more interactive ways to configure your appliance at runtime available in upcoming releases.
With that let's move on to our next section on how we can make a reproducible appliance that will grant access to all logitech usb cameras to our webcam-demo app, while leaving the others up for use by potential other apps.

Bundling a reproducible Appliance Product: Logitech Webcam

After having built our webcam appliance prototype from scratch starting from a generic beagleboard snappy image, it is pretty obvious what would need doing to bundle up these things and preconfigure them. All it takes is a way to express what we want to preinstall and what hwassign we want to do. Also it would be nice to do some branding tweaks so that this appliance really feels like ours. So lets look into how that is done.
To realize a reproducible snappy appliance the system builder makes a special snappy part called a "oem snap", documented in depth in a different document.
Since we want to build an appliance around a beaglebone we will use the oem snap published by the Ubuntu team for the reference beaglebone image and change it to our needs.
To get the source, the easiest way is to ensure you have bzr installed on your ubuntu developer desktop and use the following command to branch the tree that has the oem snaps for our official prebuilt images. Lets look at our beagleblack oem snap to get started:
$ bzr branch lp:~snappy-dev/snappy-hub/snappy-systems
$ cd snappy-systems/beagleblack
Without explaining the details, we want to make our webcam-demo a built-in software, give this appliance a good hostname and also want to preconfigure hardware access so that all video* devnodes that are backed by Logitech webcams will be used by our webcam-demo app.
For this we add a section to package.yaml that makes webcam-demo a built-in part of our appliance. Here the diff if you want to do this using patch:
=== modified file 'beagleblack/meta/package.yaml'
--- beagleblack/meta/package.yaml    2015-04-22 15:46:27 +0000
+++ beagleblack/meta/package.yaml    2015-04-23 01:43:01 +0000
@@ -12,6 +12,10 @@
         name:  Beagle Bone Black
         subname: BeagleBoard

+    software: # optional
+        built-in:
+            - webcam-demo.canonical
+
     hardware:
         platform: am335x-boneblack
         architecture: armhf
Next step is to change more meta info and in particular we surely want to change branding for our appliance to something new:
=== modified file 'beagleblack/meta/package.yaml'
--- beagleblack/meta/package.yaml    2015-04-23 01:56:51 +0000
+++ beagleblack/meta/package.yaml    2015-04-23 01:58:27 +0000
@@ -1,5 +1,5 @@
-name: beagleblack
-vendor: Sergio Schvezov <sergio.schvezov@canonical.com>
+name: beagle-webcamdemo
+vendor: Appliance Student <student@snappy-appliances.tld>
 icon: meta/element14.png
 version: 1.7
 type: oem
@@ -9,8 +9,8 @@
 
 oem:
     branding:
-        name:  Beagle Bone Black
-        subname: BeagleBoard
+        name:  Beagle Web Cam Appliance
+        subname: BeagleWebCamDemo
 
     software: # optional
         built-in:
Last but not least we want to preassign the hardware access to our webcam-demo snap; we use the OEM hardware assign: feature for this like below:
=== modified file 'beagleblack/meta/package.yaml'
--- beagleblack/meta/package.yaml    2015-04-23 02:00:07 +0000
+++ beagleblack/meta/package.yaml    2015-04-23 02:36:34 +0000
@@ -29,3 +29,13 @@
                   offset: 131072 # 128 * 1024
                 - path: u-boot.img
                   offset: 393216 # 384 * 1024
+        assign:
+            # note that the app-id can be from a different snap too, e.g.
+            # random-app_some-service
+            - part-id: webcam-demo
+              rules:
+                  - kernel: video*
+                    with-attrs:
+                        # only Logitech vendor (046d)
+                        - idVendor=046d
+
With that we should be ready to go and you can snap it up as usual and use ubuntu-device-flash to produce your very own appliance. If you didn't apply the changes manually or made a mistake you can get a branch with this demo appliance oem snap through bzr branch lp:~snappy-dev/snappy-hub/snappy-appliance-guide-15.04 where you will find the beagle-webcamdemo directory.
$ snappy build
Generated 'beagle-webcamdemo_1.7_all.snap' snap
$ sudo ubuntu-device-flash core 15.04 --developer-mode \
       --channel edge --oem ./beagle-webcamdemo_1.7_all.snap \
       -o beagle-webcam.img
# replace /dev/sdd with the real block device you want to wipe and install this image to
$ sudo dd if=beagle-webcam.img of=/dev/sdX bs=32M
As you can see above we also decided to include webdm as this gives us a nice discoverable avahi name to make it easier to ssh into the machine. Once on it you will be greeted by your personal appliance prompt including your appliance branding "BeagleWebCamAppliance". And snappy list will show you the expected parts being installed for your appliance:...
$ ssh ubuntu@webdm.local
...
(BeagleWebCamAppliance)ubuntu@localhost:~$ snappy list
Name          	Date   	Version Developer
ubuntu-core   	2015-04-23 33  	ubuntu    
webcam-demo   	2015-04-23 1.0.2   canonical
webdm         	2015-04-23 0.5 	canonical
beagle-webcamdemo 2015-04-23 1.7
Also you might be curious to see how the hardware assign: block you described in your beagle-webcam package.yaml a few paragraphs above got mapped on disk ...
(BeagleWebCamAppliance)ubuntu@localhost:~$ cat /etc/udev/rules.d/80-snappy_beagle-webcamdemo_webcam-demo.canonical.rules
KERNEL=="video*", ATTRS{idVendor}=="046d", TAG:="snappy-assign", ENV{SNAPPY_APP}:="webcam-demo.canonical"
Note how it’s similar to the rule that got created when we put together our system and used the snappy hw-assign primitives to get things going; just that it includes more refined matching elements as you expressed in your package.yaml.
Furthermore you will be able to observe that our webcam-demo service is running and if you haven't plugged in your webcam yet systemctl will confirm that there is no /dev/video0 yet on your system.
(BeagleWebCamAppliance)ubuntu@localhost:~$ sudo systemctl status -l webcam-demo_webcam-demo_1.0.2.service
● webcam-demo_webcam-demo_1.0.2.service - webcam service that provides a webserver and takes snapshots every 10 seconds
   Loaded: loaded (/etc/systemd/system/webcam-demo_webcam-demo_1.0.2.service; enabled; vendor preset: enabled)
   Active: active (running) since Thu 2015-04-23 11:28:08 UTC; 8min ago
 Main PID: 678 (webcam-webui)
   CGroup: /system.slice/webcam-demo_webcam-demo_1.0.2.service
       	├─ 678 /bin/sh /apps/webcam-demo.canonical/1.0.2/bin/webcam-webui
       	├─ 690 golang-static-http
       	└─1418 sleep 10

Apr 23 11:35:23 localhost.localdomain ubuntu-core-launcher[678]: --- Opening /dev/video0...
Apr 23 11:35:23 localhost.localdomain ubuntu-core-launcher[678]: stat: No such file or directory
Apr 23 11:35:33 localhost.localdomain ubuntu-core-launcher[678]: --- Opening /dev/video0...
Apr 23 11:35:33 localhost.localdomain ubuntu-core-launcher[678]: stat: No such file or directory
Apr 23 11:35:43 localhost.localdomain ubuntu-core-launcher[678]: --- Opening /dev/video0...
Apr 23 11:35:43 localhost.localdomain ubuntu-core-launcher[678]: stat: No such file or directory
Apr 23 11:35:54 localhost.localdomain ubuntu-core-launcher[678]: --- Opening /dev/video0...
Apr 23 11:35:54 localhost.localdomain ubuntu-core-launcher[678]: stat: No such file or directory
Apr 23 11:36:04 localhost.localdomain ubuntu-core-launcher[678]: --- Opening /dev/video0...
Apr 23 11:36:04 localhost.localdomain ubuntu-core-launcher[678]: stat: No such file or directory
Now plug in our Logitech webcam and confirm that our udev rule matches and tags the device accordingly and that all is working and that you get a snapshot every 10 second:
# confirm that udev matches your Logitech Webcam after plugging it in
$ udevadm trigger --verbose --dry-run --tag-match=snappy-assign
/sys/devices/platform/ocp/47400000.usb/47401c00.usb/musb-hdrc.1.auto/usb1/1-1/1-1:1.0/video4linux/video0

# confirm how our webcam-demo service is happy and snapshots every 10 seconds
$ sudo systemctl status -l  webcam-demo
● webcam-demo_webcam-demo_1.0.2.service - webcam service that provides a webserver and takes snapshots every 10 seconds
   Loaded: loaded (/etc/systemd/system/webcam-demo_webcam-demo_1.0.2.service; enabled; vendor preset: enabled)
   Active: active (running) since Thu 2015-04-23 11:51:20 UTC; 3min 43s ago
 Main PID: 685 (webcam-webui)
   CGroup: /system.slice/webcam-demo_webcam-demo_1.0.2.service
       	├─ 685 /bin/sh /apps/webcam-demo.canonical/1.0.2/bin/webcam-webui
       	├─ 687 golang-static-http
       	└─1053 sleep 10

Apr 23 11:54:54 localhost.localdomain ubuntu-core-launcher[685]: /dev/video0 opened.
Apr 23 11:54:54 localhost.localdomain ubuntu-core-launcher[685]: No input was specified, using the first.
Apr 23 11:54:54 localhost.localdomain ubuntu-core-launcher[685]: Adjusting resolution from 384x288 to 352x288.
Apr 23 11:54:54 localhost.localdomain ubuntu-core-launcher[685]: --- Capturing frame...
Apr 23 11:54:55 localhost.localdomain ubuntu-core-launcher[685]: Captured frame in 0.00 seconds.
Apr 23 11:54:55 localhost.localdomain ubuntu-core-launcher[685]: --- Processing captured image...
Apr 23 11:54:55 localhost.localdomain ubuntu-core-launcher[685]: Fontconfig error: Cannot load default config file
Apr 23 11:54:55 localhost.localdomain ubuntu-core-launcher[685]: Unable to load font 'sans': fontconfig: Couldn't find font.
Apr 23 11:54:55 localhost.localdomain ubuntu-core-launcher[685]: Disabling the the banner.
Apr 23 11:54:55 localhost.localdomain ubuntu-core-launcher[685]: Writing JPEG image to 'shot.jpeg'.
And now just use your browser again and and look at your snapshots!
$ firefox http://webdm.local:8080/shot.jpeg

If you see a screenshot and it gets updated every 10 seconds you really succeeded in building your very own reproducible snappy appliance! Congratulations and have fun building your own appliance!

Of course, you can upload your appliance oem snap to the store so others can reproduce it; note though that you must include a proper contact email as we will have to review these manually for now due to some of the features exposed through OEM snaps being inherently unsafe for the community to consume without review.

Questions

If you have any questions or issues with this guide, reach out to the snappy-devel mailing-list or on IRC, the #snappy channel on Freenode.