vmdb2 MVP with echo and error

Lars Wirzenius

work in progress

1 Introduction

vmdb2 is a program for producing a disk image with Debian installed. This document is a manual of sorts, and an automated test suite for it.

vmdebootstrap installs Debian onto a disk, or disk image. It is like the debootstrap tool, except the end result is a disk or disk image, not a directory tree. vmdebootstrap takes care of creating partitions, and filesystems, and allows some more customization than vmdebootstrap does.

vmdebootstrap is also a messy pile of kludge, and not flexible enough. vmdb2 is a re-implementation from scratch, without a need for backwards compatibility. It aims to provide more flexibility than vmdeboostrap, without becoming anywhere near as complicated. Think of vmdb2 as “vmdebootstrap the second generation”. The name has changed to allow the two tools to installable in paralllel.

The main user-visible difference between vmdebootstrap and vmdb2 is that the older program provides extensibitlity via a legion of command line options and the newer program by having the user user a domain specific language to express what kind of Debian system they want to create.

(Lars Wirzenius wrote both vmdebootstrap and vmdb2 and is entitled to sneer at his younger self.)

1.1 Contact

To make contact with the vmdb2 project you can email Lars directly (liw@liw.fi?) or use the #vmdb2 IRC channel on the irc.oftc.net network.

2 Specification files

A vmdb2 specification file is a YAML file that looks like this (this is an imaginary example that doens’t actually work right now):

EXAMPLE
steps:
- mkimg: raw
  size: 4G
- mkfs: ext4
  label: vmdb2rootfs
- debootstrap: jessie
- shell: |
    rm -rf /usr/share/man
- adduser: jimbo
  gecos: James Bond
  shell: /bin/zsh
- sudo: jimbo
- ifupdown: eth0
  auto: yes
  dhcp: yes
- apt: openssh-server
- convert: qcow2
- compress: xz

The list of steps produces the kind of image that the user wants (or else an unholy mess). The specification file can easily be shared, and put under version control.

Every action in a step is provided by a plugin to vmdb2. Each action (technically, “step runner”) is a well-defined task, which may be parameterised by some of the key/value pairs in the step. For example, mkimg would create a disk image file. In the above example it is a raw disk image file, as opposed to some other format. The image is 4 gigabytes in size. mkfs creates an ext4 filesystem in the image file; in thie example there are no partitions. And so on.

Steps may need to clean up after themselves. For example, a step that mounts a filesystem will need to unmount it at the end of the image creation. Also, if a later step fails, then the unmount needs to happen as well. This is called a “teardown”. Some steps are provided by a plugin that handles the teardown automatically, others may need to provide instructions for the teardown in the specification file.

By providing well-defined steps that the user may combine as they wish, vmdb2 gives great flexibility without much complexity, but at the cost of forcing the user to write a longer specification file than a single vmdeboostrap command line.

3 A happy path

The first case we look at is one for the happy path: a specification with two echo steps, and nothing else. It’s very simple, and nothing goes wrong when executing it. In addition to the actual thing to do, each step may also define a “teardown” thing to do. For example, if the step mounts a filesystem, the teardown would unmount it.

SCENARIO happy path
GIVEN a specification file called happy.vmdb containing
... {
...     steps: [
...         { echo: "foo", teardown: "foo_teardown" },
...         { echo: "bar", teardown: "bar_teardown" }
...     ]
... }
WHEN user runs vmdb2 -v happy.vmdb
THEN exit code is 0
AND stdout contains "foo" followed by "bar"
AND stdout contains "bar" followed by "bar_teardown"
AND stdout contains "bar_teardown" followed by "foo_teardown"

4 Jinja2 templating in specification file values

Vmdb2 allows values in specification files to be processed by the Jinja2 templating engine. This allows users to do thing such as write specifications that use configuration values to determine what happens. For our simple echo/error steps, we will write a rule that outputs the image file name given by the user. A more realistic specification file would instead do thing like create the file.

SCENARIO jinja2 templating
GIVEN a specification file called j2.vmdb containing
... {
...     steps: [
...         { echo: "image is {{ output }}" },
...         { echo: "bar" },
...     ]
... }
WHEN user runs vmdb2 -v --output=foo.img j2.vmdb
THEN exit code is 0
AND stdout contains "image is foo.img" followed by "bar"

5 Error handling

Sometimes things do not quite go as they should. What does vmdb2 do then?

SCENARIO error handling
GIVEN a specification file called unhappy.vmdb containing
... {
...     steps: [
...         { echo: "foo", teardown: "foo_teardown" },
...         { error: "yikes", teardown: "WAT?!" },
...         { echo: "bar_step", teardown: "bar_teardown" }
...     ]
... }
WHEN user runs vmdb2 -v unhappy.vmdb
THEN exit code is 1
AND stdout contains "foo" followed by "yikes"
AND stdout contains "yikes" followed by "WAT?!"
AND stdout contains "WAT?!" followed by "foo_teardown"
AND stdout does NOT contain "bar_step"
AND stdout does NOT contain "bar_teardown"

6 IMPLEMENTS for all scenario steps

This chapter contains the implementations for all scenario steps.

IMPLEMENTS GIVEN a specification file called (\S+) containing (.+)
filename = get_next_match()
spec = get_next_match()
open(filename, 'w').write(spec)

IMPLEMENTS WHEN user runs vmdb2 (.*)
args = get_next_match()
vmdb2 = os.path.join(srcdir, 'vmdb2')
exit, out, err = cliapp.runcmd_unchecked([vmdb2] + args.split())
vars['exit'] = exit
vars['stdout'] = out.decode()
vars['stderr'] = err.decode()

IMPLEMENTS THEN exit code is (\d+)
wanted = int(get_next_match())
exit = vars['exit']
print('exit code', exit)
print('stdout:', vars['stdout'])
print('stderr:', vars['stderr'])
assertEqual(exit, wanted)

IMPLEMENTS THEN stdout contains "(.+)" followed by "(.+)"
first = get_next_match()
second = get_next_match()
stdout = vars['stdout']
first_i = stdout.find(first)
assertGreaterThan(first_i, 0)
rest = stdout[first_i + len(first):]
second_i = rest.find(second)
assertGreaterThan(second_i, -1)

IMPLEMENTS THEN stdout does NOT contain "(\S+)"
what = get_next_match()
stdout = vars['stdout']
i = stdout.find(what)
assertEqual(i, -1)