Creating an appliance image for a single purpose can be quite some effort- you need to take care of your application, need to build a safe root filesystem and make modifications to the system so it behaves the way you or the application expect it to behave. With Ubuntu Core,this effort becomes immediately a lot easier. Ubuntu Core is completely made out of snap packages; the kernel, rootfs and all applications are all snap based and benefit from the advantages this package format brings..
Snap packages are transactional. They can automatically rollback on error after an upgrade including the kernel snap. If a breakage is noticed after upgrading to the new kernel version, the system will detect this and automatically roll back to the former known-working version. The same goes for every other snap in the system, including the root filesystem.
Snap packages are binary readonly filesystem images that support binary delta upgrades generated on the store server. This means an upgrade only downloads the actual binary delta between two snaps thereby reducing the download cost to a minimum. If your appliance is only attached via 3G, for example, this is a significant cost saver.
Snap packages communicate with each other and with the system hardware through predefined interfaces that the image creator has full control over and your applications only see the hardware and data from other snap packages if you allow it to.
Ubuntu Core images consist of three snap packages by default. There is a kernel snap, running your hardware, the core snap which contains a minimal root filesystem and the gadget snap which ships a bootloader, the desired partitioning scheme, rules for interface connections and configuration defaults for application snaps included in the image.
If you pick hardware that is supported by existing Ubuntu Core (Generic x86_64, Raspberry Pi (armv7l) or the Dragonboard 420c (aarch64)) you will not only find the ready-made root filesystem core snap in the store already but also ready-made kernel snaps for your hardware. All it needs to create an Ubuntu Core appliance image is an application snap (and if you have a more complex setup a fork of the existing gadget snap for your device with adjustments for interfaces and app defaults).
This set up reduces the development time drastically … pretty much down to a one-time operation for the gadget and a constant focus on your application. There is no need to care for any other image specific bits, they can be downloaded you get them for free from the Snap Store for free, always up to date and security maintained for 5 years.
The following walk-through will show the creation of a mid-complex appliance image that does some automatic self configuration but also allows providing additional set-up (i.e. wireless LAN, system user) via plugging in a USB stick.
Picking the required snaps
For a Digital Signage demo where we want to control the attached displays through a web interface the dashkiosk [1] dashboard managing tool looks like a good candidate. It allows managing, grouping and assigning attached remote displays through a simple drag and drop web UI. The snap finds its clients via the mDNS protocol, so our appliance image will ship the avahi snap along with the server application.
The attached “displays” are actually simple web-browsers. To not waste resources we will turn our server image into being a client at the same time and have it ship a web-browser snap pointing to the local dashkiosk server.
To have the browser display something on the screen we will also need a graphics stack, so we will ship the mir-kiosk snap as well in our image.
This leaves us with the following list of snaps:
- dashkiosk (source for this can be found at [2])
- avahi
- mir-kiosk
- “a browser” (this could be chromium-mir-kiosk, but sadly this snap has no support for avahi built in so we will have to create a fork that adds this feature) [3]
We want to not only create a server image but also have auto-connecting clients, so we will create a second image that only contains the browser and display stack.
Now that we have identified what snap packages we want to pre-install in our two images, it is time to read up about how Ubuntu Core images are created. [4] has a walk-through for this, take a special look at the “required-snaps” option of the model assertion we will create.
The client:
{ "type": "model", "authority-id": "", "brand-id": "", "series": "16", "model": "dashkiosk-client", "architecture": "armhf", "gadget": "pi3", "kernel": "pi2-kernel", "required-snaps": [ "avahi", "mir-kiosk", "dashkiosk-client-browser" ], "timestamp": "2018-09-25T14:45:25+00:00" }
The server:
{ "type": "model", "authority-id": "", "brand-id": "", "series": "16", "model": "dashkiosk", "architecture": "armhf", "gadget": "pi3", "kernel": "pi2-kernel", "required-snaps": [ "avahi", "dashkiosk", "mir-kiosk", "dashkiosk-client-browser" ], "timestamp": "2018-09-25T14:45:25+00:00" }
The “dashkiosk-client-browser” [3] in the above two model assertions is a fork of the original chromium-mir-kiosk with the original “chromium.launcher” replaced with a script that first does an avahi-resolve-host-name call to receive the IP of the dashkiosk server via mDNS, beyond this it is largely original.
When you now use “ubuntu-image” as described in [4] you will already end up with a bootable image that has these snap packages pre-installed. They should automatically start up on boot, but since they are securely confined they will not yet be able to properly communicate with each other or the hardware because not all interfaces are automatically pre-connected.
Interfaces and defaults
While you can have default connections of snap packages defined via a store declaration, many of these interfaces are often not suitable security wise to have them auto-connected everywhere such a snap is installed. Luckily Ubuntu Core gives an option to do these additional interface connections with a “connections:” entry in the gadget snap of your image [5].
To use these “connections:” entries in your gadget.yaml all the snaps you want to use need to have a snap id. This means you first need to upload them to the store, in the details of the store UI for your snap package you can then see the ID hash that you need to use in the gadget.yaml entry.
A forked pi3 gadget example with a gadget.yaml that includes various default connections for dashkiosk, the browser and a few other bits can be found at [6].
To give our modified browser (snap ID: hBB9l3miabfAKr2Dmnzd5RgzmEbMQVbj) the permission to actually use the added “avahi-resolve-host-name” call, we add a plug/slot combination for the “avahi-observe” interface that the avahi snap (dVK2PZeOLKA7vf1WPCap9F8luxTk9Oll) provides to us:
connections: [...] - plug: hBB9l3miabfAKr2Dmnzd5RgzmEbMQVbj:avahi-observe slot: dVK2PZeOLKA7vf1WPCap9F8luxTk9Oll:avahi-observe [...]
Such a set-up can be done for any interface combination of your shipped snap packages in the image.
If you scroll down further in the example gadget.yaml above you also find a “defaults:” field where we define that rsyslog should not log to the SD card to prevent wearing it out with massive logging and set the default port for the dashkiosk server to port 80. If your application snap packages use configuration via “snap set/get” [7] you can set all desired defaults through this method.
Considering additional image features
There are some adjustments we need to do to the image, for this we will create a “config snap” that we ship by default. This snap will run a few simple scripts during boot and set the additional defaults we need by utilizing the available snap interfaces of the system [8] and [9].
Since the clients find their server via an mDNS lookup we need to make sure the correct avahi hostname is set on our server (see the “set-name” script in the above config snap trees). We use the “avahi-control” interface for this and connect it in [6] with a slot/plug combination.
By default Ubuntu Core always uses UTC as its timezone. The dashkiosk default setup always shows a clock on the initial screen on all clients, so one thing we want to have the image do is to set a proper timezone on boot, to show the correct local time on start. For this we do a simple web lookup on a public geoip service that provides us the timezone for the IP we connect from, this is done via the “set-timezone” script in the above config snap tree. To allow our script to access the timezone configuration, we connect our configuration snap to the “timezone-control” interface of the system (note, connecting to system or “core” slots means you do not deed to define the slot in the definition, only the plug side needs to be defined)
While the above is already enough to have the images properly work on a wired network (Ubuntu Core always defaults to a DHCP configured ethernet without any further configuration), I personally plan to also be able to use this image on the Raspberry Pi3 with a wireless connection.
Network connections are configured by using netplan [10] on Ubuntu Core. To configure a WLAN connection we can either manually configure each client board through a serial console (which offers the interactive setup through console-conf/subiquity) or we can write a little tool that monitors the USB port for plugged in USB sticks and checks if it finds a netplan configuration file that we define. For this we create the “netplan-import” script in [8] and [9], this tool uses the “system-observe”, “mount-observe”, “udisks2”, “removable-media”, “network-setup-control” and “shutdown” interfaces, see again plugs and slots in our gadget.yaml for this … and since udisks2 is only provided by the udisks2 snap, we also need to make sure to add this snap to the “required-snaps” of our model assertion.
Now we have a completely self-configuring appliance image. To hook up all our clients to the same WLAN we can walk around with the same USB stick containing the netplan.yaml and configure the systems by simply plugging it in (which causes them to copy that config in place and reboot with the newly configured WLAN connection.)
A netplan.yaml for a valid WLAN connection should look like:
network: version: 2 wifis: wlan0: access-points: <YOUR ESSID>: {password: <YOUR WPA PASSWORD>} addresses: [] dhcp4: true
(Replace “<YOUR ESSID>” and “<YOUR WPA PASSWORD>” with the correct values)
Building the final image
Now that we have all the bits together, let us build the final images. To have all the above snaps included We add the remaining snaps to the “required-snaps” entries of the model assertions:
Server:
"required-snaps": [ "avahi", "udisks2", "dashkiosk-image-config", "dashkiosk", "mir-kiosk", "dashkiosk-client-browser" ],
Client:
"required-snaps": [ "avahi", "udisks2", "dashkiosk-client-image-config", "mir-kiosk", "dashkiosk-client-browser" ],
Make sure to have all interface connections set right like in [6].
Sign your model assertion as described in [4] and do a local build of your gadget snap (do not upload it to the store, it will get stuck in manual review there. If you actually want to use a gadget snap in a commercial project, contact Canonical about obtaining a brand store [11]).
When you build your image, use the –extra-snaps (see [4]) option to point to your locally built gadget package.
After the initial flashing of the image to your device/SD card, your kernel and rootfs will automatically receive updates without you doing anything. Any fixes you want to do to the applications or configuration snap packages, can just be uploaded to the store, your image will automatically refresh and pick them up in a timely manner.
Summary
If you followed the above step by step guide you will now have a Digital Signage demo appliance image for a Raspberry Pi, some helpful notes at the end:
Do the first boot of the images with ethernet connected, else you will have to wait extra long for systemd to time out trying to do an initial network connection (the boot will not fail but will take significantly longer, and indeed since the timezone above is set via a geoip lookup, your timezone will also not be set)
Be patient for the first boot in case you are on the ARM architecture. Snapd installs all the pre-seeded snap packages on first boot, this includes a sha3 checksum verification of the snap files. Snapd is written in go and the sha3 function of go is extremely slow (1-2min per pre-seeded snap) on armhf. All subsequent boots will be as fast as you would expect them (around 30 sec to having the graphics on screen).
If you want to log in via ssh you can create a system-user assertion that you can put on the same USB key your netplan.yaml lives on, it will create an ssh user for you to allow you to inspect the booted system [12]. There is a tool provided as a snap package to make creating a system user assertion easy for you at [13].
Last but not least, you can find ready-made images that followed the above step by step guide under [14].
If there are any open questions, feel free to ask them in the “device” category on https://forum.snapcraft.io/
[1] https://github.com/vincentbernat/dashkiosk.git
[2] https://github.com/ogra1/dashkiosk-snap
[3] https://github.com/ogra1/dashkiosk-client-browser
[4] https://docs.ubuntu.com/core/en/guides/build-device/image-building
[5] https://forum.snapcraft.io/t/the-gadget-snap/696#gadget.yaml
[6] https://github.com/ogra1/pi-kiosk-gadget/blob/master/gadget.yaml
[7] https://forum.snapcraft.io/t/configuration-in-snaps/510
[8] https://github.com/ogra1/dashkiosk-image-config
[9] https://github.com/ogra1/dashkiosk-client-image-config
[10] https://netplan.io/
[11] https://docs.ubuntu.com/core/en/build-store/create
[12] https://docs.ubuntu.com/core/en/reference/assertions/system-user
[13] https://snapcraft.io/make-system-user
[14] http://people.canonical.com/~ogra/snappy/kiosk/
One comment