Merge lp:~jamesodhunt/snappy/bug-1412737 into lp:~snappy-dev/snappy/snappy-moved-to-github

Proposed by James Hunt on 2015-02-17
Status: Merged
Approved by: Michael Vogt on 2015-03-27
Approved revision: 186
Merged at revision: 269
Proposed branch: lp:~jamesodhunt/snappy/bug-1412737
Merge into: lp:~snappy-dev/snappy/snappy-moved-to-github
Diff against target: 1087 lines (+690/-120)
6 files modified
debian/ubuntu-snappy.install (+1/-0)
etc/grub.d/09_snappy (+410/-0)
partition/bootloader_grub.go (+6/-17)
partition/bootloader_grub_test.go (+4/-7)
partition/partition.go (+129/-61)
partition/partition_test.go (+140/-35)
To merge this branch: bzr merge lp:~jamesodhunt/snappy/bug-1412737
Reviewer Review Type Date Requested Status
Michael Vogt Approve on 2015-03-27
Sergio Schvezov 2015-02-17 Needs Information on 2015-03-26
Colin Watson (community) 2015-02-17 Approve on 2015-03-23
Steve Langasek 2015-02-17 Pending
Review via email: mp+249976@code.launchpad.net

Commit Message

Make snappy on grub systems detect a broken boot and fall back to the other root partition on power cycling.

Description of the Change

Now that we have a single shared boot partition for grub, we can use it
to store bootloader variables that store state (as is already done for
snappy u-boot systems).

Further, we no longer need to re-install grub from within the chroot to
tell grub which rootfs to boot from since the variables suffice.

Note though that snappy still needs to call update-grub from within the
chroot since that chroot has the correct grub config files for that
particular rootfs (/etc/grub.d/*, etc).

Since the grub code is run from within a chroot (where the "other"
rootfs is mounted), it might _appear_ that the logic is inverted, but
this is not the case since the grub snippet queries the chroot
environment only to set up the grub menu (the grub snippet should work
correctly outside a chroot, although it would not be valid to do this on
a snappy system).

Specifically, the grub snippet performs 2 functions:

1) Create the grub menuentry's for the 2 rootfs's *at update-grub time*
   and within the context of the "other" chroot.

2) Arrange for grub to toggle the rootfs *at boot time*, based on the
   value of the grub environment block variables.

Remaining work:

- update the logic in ubuntu-core-snappy to also mount the current rootfs r/o at
  /writable/cache/system within the chroot (since we are planning to ship 2 implementations
  of snappy for the next release and the behaviour needs to match).

_________

* etc/grub.d/09_snappy: Grub snippet that:
  - Creates menuentry's for both rootfs's.
  - Boots using the rootfs specified by the snappy_ab variable
    (which replaces the boot-by-uuid config auto-generated by
     grub-install [which is no longer called]).
  - Toggles the rootfs if the last boot failed (LP: #1412737).
* partition/bootloader_grub.go:
  - ToggleRootFS: Remove call to grub-install now that
    ubuntu-device-flash creates the shared boot partition for grub.
* partition/partition.go:
  - bindmountRequiredFilesystems(): Call new function
    bindmountThisRootfsRO() so that menu entries can be generated for
    for both rootfs's on grub systems.

To post a comment you must log in.
James Hunt (jamesodhunt) wrote :

It is also worth pointing out here that the included etc/grub.d/09_snappy makes /etc/grub.d/10_linux redundant. The fact that 10_linux will be run as part of update-grub does not cause problems - it simply creates an additional menuentry in /boot/grub/grub.cfg. So we could consider removing /etc/grub.d/10_linux from the images via a follow-on livecd-rootfs change.

James Hunt (jamesodhunt) wrote :

Changes for the snappy python implementation are here:

lp:~jamesodhunt/ubuntu/vivid/ubuntu-core-snappy/mount-current-rootfs-in-chroot-for-bug-1412737 (I'll raise an MP once this branch lands).

Michael Vogt (mvo) wrote :

Just a quick note without having looked at the code itself yet.

I feel a bit uneasy about the amount of code in 09_snappy that looks like its duplication from 10_linux. I wonder if there is anything we could do here? Maybe share code with 10_linux, maybe even generate from snappy-go directly instead of via shell (snappy-go knows about the partition layout for example already).

The other thing that does not feel ideal is the fact that we chroot and then bindmount the original back. The chroot also seems to not allow lsblk to work. I wonder if we could do something like make update-grub honor a GRUB_SYSCONF_DIR=/path/to/other/etc environment.

This may all be crazy, but I wanted to throw it out as ideas anyway :)

But in any case, merging this into trunk and running:
$ ./run-checks
fails for me so that needs addressing.

James Hunt (jamesodhunt) wrote :

Hi Michael,

> I feel a bit uneasy about the amount of code in 09_snappy that looks
> like its duplication from 10_linux.
> I wonder if there is anything we
> could do here? Maybe share code with 10_linux,
Well, yes 09_snappy is currently a very cleaned up version of 10_linux :)

I understand the concerns. Regarding 09_snappy and 10_linux, my plan is
to do the following (and get it upstreamed) to minimise the delta we
have to maintain:

1) Move the functions in 09_snappy to /usr/lib/grub/grub-mkconfig_lib
   (or maybe /usr/lib/grub/grub-lib-linux as it is linux-specific).
2) Add linux_entry() to grub-lib-linux.
3) Make linux_entry() call linux_entry_ext().
4) Rework 10_linux to call the functions in grub-lib-linux.

Assuming this can be upstreamed, we could then collapse 09_snappy down
an ~75 line script.

> maybe even generate from
> snappy-go directly instead of via shell (snappy-go knows about the
> partition layout for example already).
I'm not super happy about the amount of shell in grub either tbh.
However, that is the current standard it seems and I'm trying to conform
to that. I like the idea of snappy-go generating the config but would
value input from others as to the safety of such an approach given the
number of other scripts and config files grub has here and there; if we
It feels rather dangerous to start generating the config entirely on our
own as we should be able to rely on grub library routines to handle any
internal grub changes. If we go it alone, that seems higher risk than
simply writing a new grub snippet such as this MP adds.

> The other thing that does not feel ideal is the fact that we chroot and
> then bindmount the original back.
Yes, the chroot'ing is a pita but is required to:

1) Ensure we're calling the correct grub commands for the "other"
   rootfs.
2) Ensure that the command(s) in (1) have the correct config
(/etc/grub.d/* and /etc/default/grub).

> The chroot also seems to not allow lsblk to work.
Right - I haven't looked at the lsblk source, but it fails in a chroot,
only reporting "/".

>I wonder if we could do something like make update-grub
> honor a GRUB_SYSCONF_DIR=/path/to/other/etc environment.
That would be neat if upstream would agree to that. Again, if we're only
going to make these changes locally for snappy, which is worse? A delta
on grub itself or a delta on a packaged grub config file? Also, would
that variable be sufficient (I don't know yet I'm afraid).

> This may all be crazy, but I wanted to throw it out as ideas anyway :)
Definately good to get it into the open! :)

> But in any case, merging this into trunk and running: $ ./run-checks
> fails for me so that needs addressing.
Oops, sorry! I still cannot run the snappy tests due to the click issue
so once the new versions lands in vivid I'll fix that.

I think that if this branch is unlikely to land today (and that's
looking very likely :-), that we should postpone landing it (in any
form) until after the next promotion.

Michael Vogt (mvo) wrote :

Just a quite note for the "./run-checks". The new click is in the beta PPA since yesterday. You can also run the partition tests by doing: "go test ./partition", this part of the code does not use click so that should work.

Sergio Schvezov (sergiusens) wrote :

I just want to note that etc/grub.d/09_snappy will be created in both variants (u-boot and grub) if it sits in here.

Also added a couple of inline comments.

review: Abstain
James Hunt (jamesodhunt) wrote :

Thanks - added rc check.

Alexander Sack (asac) wrote :

whats the state of this one? Are we doing this? Is this zombied?

James Hunt (jamesodhunt) wrote :

We still need the code to be reviewed by someone familiar with the grub package. The current behaviour:

If snappy fails to boot (hangs in early boot):

- uboot systems: will revert automatically to the "other" rootfs on next boot.

- grub systems: no rootfs switch happens on next boot due to this branch not having landed yet. A work-around currently would be enter the grub menu and modifying the cmdline to specify manually "root=system-a" or "root=system-b" (the user would need to try both as they won't know which is the current default).

Note that as well as bringing grub systems up to feature parity with uboot system, this MP also adds "boot by label" ("root=system-a") for simplicity.

Colin Watson (cjwatson) wrote :

The duplication isn't especially pleasant, but it also isn't a huge problem: this is well-contained within snappy and so isn't going to break anything else. And I certainly agree with James that generating this entirely from Go code and thus ignoring all of GRUB's library functions is unwise. The rest looks reasonable enough from a skim-read.

review: Approve
Alexander Sack (asac) wrote :

whats the plan for testing this? I think if we knew how to test this manually or even better automatically, we should land and get miles on this asap.

Michael Vogt (mvo) wrote :

Top-level approving based on the ACK from Colin.

Snappy Tarmac (snappydevtarmac) wrote :

Attempt to merge into lp:snappy failed due to conflicts:

text conflict in debian/ubuntu-snappy.install
text conflict in partition/bootloader_grub.go

Sergio Schvezov (sergiusens) wrote :

I don't see any a/b layout handling here, can you explain how that will work?

review: Needs Information
James Hunt (jamesodhunt) wrote :

That's handled at the very end of etc/grub.d/09_snappy (see "Toggle rootfs if previous boot failed.").

(I'm currently reworking the snappy-go changes on this branch so it's no longer ready to land, hence the status change).

Sergio Schvezov (sergiusens) wrote :

On jueves 26 de marzo de 2015 10h'33:50 ART, James Hunt wrote:
> That's handled at the very end of etc/grub.d/09_snappy (see
> "Toggle rootfs if previous boot failed.").
>
> (I'm currently reworking the snappy-go changes on this branch
> so it's no longer ready to land, hence the status change).

So the kernel lives in system-a and system-b and not system-boot?

James Hunt (jamesodhunt) wrote :

Currently snappy systems using grub mirror standard Ubuntu systems, so yes.

James Hunt (jamesodhunt) wrote :

Branch updated but not tested due to current devel-proposed issues.

James Hunt (jamesodhunt) wrote :

Tested .go changes performing a "snappy-go update".

lp:~jamesodhunt/snappy/bug-1412737 updated on 2015-03-27
186. By James Hunt on 2015-03-27

* Sync with lp:snappy.

Michael Vogt (mvo) wrote :

I have some inline comments, but I think its more important to land this than to do any of this so I will approve and we can followup in a different MP.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/ubuntu-snappy.install'
2--- debian/ubuntu-snappy.install 2015-03-25 20:44:44 +0000
3+++ debian/ubuntu-snappy.install 2015-03-27 08:58:19 +0000
4@@ -1,2 +1,3 @@
5 debian/*.service /lib/systemd/system/
6+etc
7 debian/*.timer /lib/systemd/system/
8
9=== added directory 'etc'
10=== added directory 'etc/grub.d'
11=== added file 'etc/grub.d/09_snappy'
12--- etc/grub.d/09_snappy 1970-01-01 00:00:00 +0000
13+++ etc/grub.d/09_snappy 2015-03-27 08:58:19 +0000
14@@ -0,0 +1,410 @@
15+#!/bin/sh
16+#---------------------------------------------------------------------
17+# Summary: Grub bootloader logic for Ubuntu Snappy systems.
18+# Description: This is a heavily modified "10_linux" grub snippet that
19+# deals with Snappy dual-rootfs systems.
20+#
21+# XXX: Note that this script is called from within a chroot environment
22+# on snappy systems!
23+#
24+#---------------------------------------------------------------------
25+
26+set -e
27+
28+prefix="/usr"
29+exec_prefix="/usr"
30+datarootdir="/usr/share"
31+
32+# Utility functions
33+. "${datarootdir}/grub/grub-mkconfig_lib"
34+
35+# Globals
36+machine=`uname -m`
37+
38+SNAPPY_OS="Ubuntu Core Snappy"
39+SNAPPY_TYPE=simple
40+SNAPPY_ARGS="${GRUB_CMDLINE_LINUX} ${GRUB_CMDLINE_LINUX_DEFAULT}"
41+
42+#---------------------------------------------------------------------
43+
44+# Display message and exit
45+die()
46+{
47+ msg="$*"
48+ echo "ERROR: $msg" >&2
49+ exit 1
50+}
51+
52+# Create a grub menu entry by writing a menuentry to stdout.
53+linux_entry_ext()
54+{
55+ local name="$1"
56+ local os="$2"
57+ local version="$3"
58+ local type="$4"
59+ local args="$5"
60+ local device="$6"
61+ local kernel="$7"
62+ local initrd="$8"
63+
64+ local boot_device_id=
65+ local prepare_root_cache=
66+ local prepare_boot_cache=
67+
68+ if [ -z "$boot_device_id" ]; then
69+ boot_device_id="$(grub_get_device_id "${device}")"
70+ fi
71+
72+ echo "menuentry '$name' ${CLASS} \$menuentry_id_option 'gnulinux-simple-$boot_device_id' {" | sed "s/^/$submenu_indentation/"
73+
74+ if [ "$quick_boot" = 1 ]; then
75+ echo " recordfail" | sed "s/^/$submenu_indentation/"
76+ fi
77+ if [ x$type != xrecovery ] ; then
78+ save_default_entry | grub_add_tab
79+ fi
80+
81+ # Use ELILO's generic "efifb" when it's known to be available.
82+ # FIXME: We need an interface to select vesafb in case efifb can't be used.
83+ if [ "x$GRUB_GFXPAYLOAD_LINUX" = x ]; then
84+ echo " load_video" | sed "s/^/$submenu_indentation/"
85+ else
86+ if [ "x$GRUB_GFXPAYLOAD_LINUX" != xtext ]; then
87+ echo " load_video" | sed "s/^/$submenu_indentation/"
88+ fi
89+ fi
90+ if ([ "$ubuntu_recovery" = 0 ] || [ x$type != xrecovery ]) && \
91+ ([ "x$GRUB_GFXPAYLOAD_LINUX" != x ] || [ "$gfxpayload_dynamic" = 1 ]); then
92+ echo " gfxmode \$linux_gfx_mode" | sed "s/^/$submenu_indentation/"
93+ fi
94+
95+ echo " insmod gzio" | sed "s/^/$submenu_indentation/"
96+ echo " if [ x\$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi" | sed "s/^/$submenu_indentation/"
97+
98+ # device may be a label (LABEL=name), so convert back to full path
99+ label_name=$(echo "$device"|sed 's/^LABEL=//g')
100+ if [ "$device" = "$label_name" ]
101+ then
102+ device_path="$device"
103+ else
104+ # found a label
105+ device_path=$(get_partition_from_label "$label_name")
106+ fi
107+
108+ if [ x$dirname = x/ ]; then
109+ if [ -z "${prepare_root_cache}" ]; then
110+
111+ prepare_root_cache="$(prepare_grub_to_access_device ${device_path} | grub_add_tab)"
112+ fi
113+ printf '%s\n' "${prepare_root_cache}" | sed "s/^/$submenu_indentation/"
114+ else
115+ if [ -z "${prepare_boot_cache}" ]; then
116+ prepare_boot_cache="$(prepare_grub_to_access_device ${device_path} | grub_add_tab)"
117+ fi
118+ printf '%s\n' "${prepare_boot_cache}" | sed "s/^/$submenu_indentation/"
119+ fi
120+
121+ if [ x"$quiet_boot" = x0 ] || [ x"$type" != xsimple ]; then
122+ message="$(gettext_printf "Loading Linux %s ..." ${version})"
123+ sed "s/^/$submenu_indentation/" << EOF
124+ echo '$(echo "$message" | grub_quote)'
125+EOF
126+ fi
127+
128+ sed "s/^/$submenu_indentation/" << EOF
129+ linux ${kernel} root=${device} ro ${args}
130+EOF
131+
132+ if test -n "${initrd}"; then
133+ # TRANSLATORS: ramdisk isn't identifier. Should be translated.
134+ if [ x"$quiet_boot" = x0 ] || [ x"$type" != xsimple ]; then
135+ message="$(gettext_printf "Loading initial ramdisk ...")"
136+ sed "s/^/$submenu_indentation/" << EOF
137+ echo '$(echo "$message" | grub_quote)'
138+EOF
139+ fi
140+ sed "s/^/$submenu_indentation/" << EOF
141+ initrd ${initrd}
142+EOF
143+ fi
144+
145+ sed "s/^/$submenu_indentation/" << EOF
146+}
147+EOF
148+}
149+
150+# Returns a list of the currently available kernels.
151+# $1: If set, look for kernel below "$1/boot/".
152+get_kernels()
153+{
154+ local prefix_dir="$1"
155+ local list
156+
157+ case "x$machine" in
158+ xi?86 | xx86_64)
159+ list=`for i in $prefix_dir/boot/vmlinuz-* \
160+ $prefix_dir/vmlinuz-* \
161+ $prefix_dir/boot/kernel-* ; do
162+ if grub_file_is_not_garbage "$i" ; then echo -n "$i " ; fi
163+ done` ;;
164+ *)
165+ list=`for i in $prefix_dir/boot/vmlinuz-* \
166+ $prefix_dir/boot/vmlinux-* \
167+ $prefix_dir/vmlinuz-* \
168+ $prefix_dir/vmlinux-* \
169+ $prefix_dir/boot/kernel-* ; do
170+ if grub_file_is_not_garbage "$i" ; then echo -n "$i " ; fi
171+ done` ;;
172+ esac
173+ echo "$list"
174+}
175+
176+# Returns the path to the initrd for the kernel specified by $1.
177+# $1: kernel version.
178+# $2: directory to look in.
179+get_initrd()
180+{
181+ local version="$1"
182+ local dir="$2"
183+
184+ local alt_version=`echo $version | sed -e "s,\.old$,,g"`
185+ local initrd=
186+ local i=
187+
188+ for i in "initrd.img-${version}" "initrd-${version}.img" "initrd-${version}.gz" \
189+ "initrd-${version}" "initramfs-${version}.img" \
190+ "initrd.img-${alt_version}" "initrd-${alt_version}.img" \
191+ "initrd-${alt_version}" "initramfs-${alt_version}.img" \
192+ "initramfs-genkernel-${version}" \
193+ "initramfs-genkernel-${alt_version}" \
194+ "initramfs-genkernel-${GENKERNEL_ARCH}-${version}" \
195+ "initramfs-genkernel-${GENKERNEL_ARCH}-${alt_version}"; do
196+ if test -e "${dir}/${i}" ; then
197+ initrd="${dir}/${i}"
198+ break
199+ fi
200+ done
201+ echo "$initrd"
202+}
203+
204+# Determine full path to disk partition given a filesystem label.
205+get_partition_from_label()
206+{
207+ local label="$1"
208+ local part=
209+ local path=
210+
211+ [ -n "$label" ] || grub_warn "need FS label"
212+
213+ part=$(find /dev -name "$label"|tail -1)
214+ [ -z "$part" ] && return
215+ path=$(readlink -f "$part")
216+ [ -n "$path" ] && echo "$path"
217+}
218+
219+# Return the partition label for the given partition device.
220+# $1: full path to partition device.
221+get_label_from_device()
222+{
223+ local root="$1"
224+
225+ local label=
226+ local std_label=
227+ local label_rootfs=
228+
229+ for std_label in system-a system-b; do
230+ label_rootfs=$(findfs "PARTLABEL=$std_label" 2>/dev/null || :)
231+ if [ "$label_rootfs" = "$root" ]; then
232+ label="$std_label"
233+ break
234+ fi
235+ done
236+
237+ echo "$label"
238+}
239+
240+# Return the full path to the device corresponding to the given
241+# partition label.
242+#
243+# $1: partition label.
244+get_device_from_label()
245+{
246+ local label="$1"
247+ local device=
248+
249+ device=$(findfs "PARTLABEL=$label" 2>/dev/null || :)
250+ echo "$device"
251+}
252+
253+# Given a rootfs label, return the rootfs label corresponding to the
254+# "other" rootfs partition.
255+get_other_rootfs_label()
256+{
257+ local label="$1"
258+
259+ if [ "$label" = "system-a" ]; then
260+ echo "system-b"
261+ else
262+ echo "system-a"
263+ fi
264+}
265+
266+# Given a mountpoint, return the corresponding device path
267+# $1: mountpoint.
268+get_device_from_mountpoint()
269+{
270+ local mountpoint="$1"
271+ local device=
272+
273+ # XXX: Parse mount output rather than looking in /proc/mounts to
274+ # avoid seeing the mounts outside the chroot.
275+ device=$(/bin/mount | grep "^/dev/.* on ${mountpoint}[[:space:]]" 2>/dev/null |\
276+ awk '{print $1}' || :)
277+
278+ echo "$device"
279+}
280+
281+# Convert a partition label name to a menuentry name
282+make_name()
283+{
284+ local label="$1"
285+
286+ echo "$SNAPPY_OS $label rootfs" | grub_quote
287+}
288+
289+# Arrange for a grub menuentry to be created for the given partition.
290+#
291+# $1: full path to rootfs partition device
292+# $2: partition label associated with $1
293+# $3: mountpoint of $1.
294+handle_menu_entry()
295+{
296+ local rootfs_device="$1"
297+ local label="$2"
298+ local mountpoint="$3"
299+
300+ local name=
301+ local device=
302+ local mount_prefix=
303+ local list=
304+ local linux=
305+ local basename=
306+ local dirname=
307+ local rel_dirname=
308+ local version=
309+ local initrd=
310+
311+ # boot by label
312+ device="LABEL=$label"
313+
314+ name=$(make_name "$label")
315+
316+ # avoid double-leading slashes and the subsequent need to call
317+ # 'readlink -f'.
318+ if [ "$mountpoint" = "/" ]; then
319+ mount_prefix=""
320+ else
321+ mount_prefix="$mountpoint"
322+ fi
323+ list=$(get_kernels "$mount_prefix")
324+
325+ linux=$(version_find_latest "$list")
326+ basename=$(basename "$linux")
327+ dirname=$(dirname "$linux")
328+ rel_dirname=$(make_system_path_relative_to_its_root "$dirname")
329+ version=$(echo "$basename" | sed -e "s,^[^0-9]*-,,g")
330+
331+ initrd=$(get_initrd "$version" "$dirname")
332+
333+ # convert the path to the mounted "other" rootfs back to a
334+ # a standard one by removing the mountpoint prefix.
335+ if [ "$mountpoint" != "/" ]; then
336+ linux=$(echo "$linux" | sed "s!^${mountpoint}!!g")
337+ initrd=$(echo "$initrd" | sed "s!^${mountpoint}!!g")
338+ fi
339+
340+ # Create menu entries for the 2 snappy rootfs's.
341+ linux_entry_ext "$name" "$SNAPPY_OS" "$version" \
342+ "$SNAPPY_TYPE" "$SNAPPY_ARGS" "$device" \
343+ "$linux" "$initrd"
344+}
345+
346+#---------------------------------------------------------------------
347+# main
348+
349+case "$machine" in
350+ i?86) GENKERNEL_ARCH="x86" ;;
351+ mips|mips64) GENKERNEL_ARCH="mips" ;;
352+ mipsel|mips64el) GENKERNEL_ARCH="mipsel" ;;
353+ arm*) GENKERNEL_ARCH="arm" ;;
354+ *) GENKERNEL_ARCH="$machine" ;;
355+esac
356+
357+# Determine which partition label is being used for the current root
358+# directory. This is slightly convoluted but required since this code
359+# runs within a chroot environment (where lsblk does not work).
360+#
361+# XXX: Note that since this code is run from within a chroot (where the
362+# "other" rootfs is mounted), it might appear that the logic is
363+# inverted. However, the code below simply
364+this_mountpoint="/"
365+this_root=$(get_device_from_mountpoint "$this_mountpoint")
366+[ -z "$this_root" ] && {
367+ die "cannot determine rootfs for $this_mountpoint"
368+}
369+
370+this_label=$(get_label_from_device "$this_root")
371+[ -z "$this_label" ] && {
372+ die "cannot determine partition label for rootfs $this_root"
373+}
374+
375+handle_menu_entry "$this_root" "$this_label" "$this_mountpoint"
376+
377+other_mountpoint="/writable/cache/system"
378+if ! $(mountpoint -q "$other_mountpoint"); then
379+ die "$other_mountpoint is not a mountpoint"
380+fi
381+
382+other_label=$(get_other_rootfs_label "$this_label")
383+
384+other_root=$(get_device_from_label "$other_label")
385+[ -z "$other_root" ] && {
386+ die "cannot determine rootfs"
387+}
388+
389+handle_menu_entry "$other_root" "$other_label" "$other_mountpoint"
390+
391+# Toggle rootfs if previous boot failed.
392+#
393+# Since grub sets recordfail, if it is _already_ set when grub starts
394+# and we're in try mode, the previous boot must have failed to unset
395+# recordfail, hence toggle the rootfs.
396+sed "s/^/$submenu_indentation/" << EOF
397+ if [ -z "\$snappy_mode" ]; then
398+ set snappy_mode=default
399+ save_env snappy_mode
400+ fi
401+ if [ -z "\$snappy_ab" ]; then
402+ set snappy_ab=a
403+ save_env snappy_ab
404+ fi
405+
406+ if [ "\$snappy_mode" = "try" ]; then
407+ if [ "\$recordfail" = "1" ]; then
408+ if [ "\$snappy_ab" = "a" ]; then
409+ set default="$(make_name system-b)"
410+ set snappy_ab=b
411+ else
412+ set snappy_ab=a
413+ set default="$(make_name system-a)"
414+ fi
415+ save_env snappy_ab
416+ fi
417+ else
418+ if [ "\$snappy_ab" = "a" ]; then
419+ set default="$(make_name system-a)"
420+ else
421+ set default="$(make_name system-b)"
422+ fi
423+ fi
424+EOF
425
426=== modified file 'partition/bootloader_grub.go'
427--- partition/bootloader_grub.go 2015-03-26 09:05:27 +0000
428+++ partition/bootloader_grub.go 2015-03-27 08:58:19 +0000
429@@ -30,9 +30,8 @@
430 bootloaderGrubConfigFileReal = "/boot/grub/grub.cfg"
431 bootloaderGrubEnvFileReal = "/boot/grub/grubenv"
432
433- bootloaderGrubEnvCmdReal = "/usr/bin/grub-editenv"
434- bootloaderGrubInstallCmdReal = "/usr/sbin/grub-install"
435- bootloaderGrubUpdateCmdReal = "/usr/sbin/update-grub"
436+ bootloaderGrubEnvCmdReal = "/usr/bin/grub-editenv"
437+ bootloaderGrubUpdateCmdReal = "/usr/sbin/update-grub"
438 )
439
440 // var to make it testable
441@@ -41,9 +40,8 @@
442 bootloaderGrubConfigFile = bootloaderGrubConfigFileReal
443 bootloaderGrubEnvFile = bootloaderGrubEnvFileReal
444
445- bootloaderGrubEnvCmd = bootloaderGrubEnvCmdReal
446- bootloaderGrubInstallCmd = bootloaderGrubInstallCmdReal
447- bootloaderGrubUpdateCmd = bootloaderGrubUpdateCmdReal
448+ bootloaderGrubEnvCmd = bootloaderGrubEnvCmdReal
449+ bootloaderGrubUpdateCmd = bootloaderGrubUpdateCmdReal
450 )
451
452 type grub struct {
453@@ -54,7 +52,7 @@
454
455 // newGrub create a new Grub bootloader object
456 func newGrub(partition *Partition) bootLoader {
457- if !helpers.FileExists(bootloaderGrubConfigFile) || !helpers.FileExists(bootloaderGrubInstallCmd) {
458+ if !helpers.FileExists(bootloaderGrubConfigFile) || !helpers.FileExists(bootloaderGrubUpdateCmd) {
459 return nil
460 }
461 b := newBootLoader(partition)
462@@ -74,18 +72,9 @@
463 //
464 // Approach:
465 //
466-// Re-install grub each time the rootfs is toggled by running
467-// grub-install chrooted into the other rootfs. Also update the grub
468-// configuration.
469+// Update the grub configuration.
470 func (g *grub) ToggleRootFS() (err error) {
471
472- other := g.partition.otherRootPartition()
473-
474- // install grub
475- if err := runInChroot(g.partition.MountTarget(), bootloaderGrubInstallCmd, other.parentName); err != nil {
476- return err
477- }
478-
479 // create the grub config
480 if err := runInChroot(g.partition.MountTarget(), bootloaderGrubUpdateCmd); err != nil {
481 return err
482
483=== modified file 'partition/bootloader_grub_test.go'
484--- partition/bootloader_grub_test.go 2015-03-26 09:12:58 +0000
485+++ partition/bootloader_grub_test.go 2015-03-27 08:58:19 +0000
486@@ -38,7 +38,6 @@
487 // these files just needs to exist
488 mockGrubFile(c, bootloaderGrubConfigFile, 0644)
489 mockGrubFile(c, bootloaderGrubEnvFile, 0644)
490- mockGrubFile(c, bootloaderGrubInstallCmd, 0755)
491 mockGrubFile(c, bootloaderGrubUpdateCmd, 0755)
492
493 // do not run commands for real
494@@ -94,20 +93,18 @@
495 mp := singleCommand{"/bin/mountpoint", "/writable/cache/system"}
496 c.Assert(allCommands[0], DeepEquals, mp)
497
498- expectedGrubInstall := singleCommand{"/usr/sbin/chroot", "/writable/cache/system", bootloaderGrubInstallCmd, "/dev/sda"}
499- c.Assert(allCommands[1], DeepEquals, expectedGrubInstall)
500-
501 expectedGrubUpdate := singleCommand{"/usr/sbin/chroot", "/writable/cache/system", bootloaderGrubUpdateCmd}
502- c.Assert(allCommands[2], DeepEquals, expectedGrubUpdate)
503+ c.Assert(allCommands[1], DeepEquals, expectedGrubUpdate)
504
505 expectedGrubSet := singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile, "set", "snappy_mode=try"}
506- c.Assert(allCommands[3], DeepEquals, expectedGrubSet)
507+ c.Assert(allCommands[2], DeepEquals, expectedGrubSet)
508
509 // the https://developer.ubuntu.com/en/snappy/porting guide says
510 // we always use the short names
511 expectedGrubSet = singleCommand{bootloaderGrubEnvCmd, bootloaderGrubEnvFile, "set", "snappy_ab=b"}
512- c.Assert(allCommands[4], DeepEquals, expectedGrubSet)
513+ c.Assert(allCommands[3], DeepEquals, expectedGrubSet)
514
515+ c.Assert(len(allCommands), Equals, 4)
516 }
517
518 func mockGrubEditenvList(cmd ...string) (string, error) {
519
520=== modified file 'partition/partition.go'
521--- partition/partition.go 2015-03-26 09:05:27 +0000
522+++ partition/partition.go 2015-03-27 08:58:19 +0000
523@@ -15,12 +15,6 @@
524 *
525 */
526
527-// TODO:
528-//
529-// - logging
530-// - SNAPPY_DEBUG
531-// - locking (sync.Mutex)
532-
533 // Package partition manipulate snappy disk partitions
534 package partition
535
536@@ -32,6 +26,7 @@
537 "os/signal"
538 "path"
539 "regexp"
540+ "sort"
541 "strings"
542 "syscall"
543
544@@ -108,17 +103,6 @@
545 // to the disk (such as uBoot, MLO)
546 const flashAssetsDir = "flashtool-assets"
547
548-//--------------------------------------------------------------------
549-// FIXME: Globals
550-
551-// list of current mounts that this module has created
552-var mounts []string
553-
554-// list of current bindmounts this module has created
555-var bindMounts []string
556-
557-//--------------------------------------------------------------------
558-
559 // MountOption represents how the partition should be mounted, currently
560 // RO (read-only) and RW (read-write) are supported
561 type MountOption int
562@@ -144,6 +128,24 @@
563 RunWithOther(rw MountOption, f func(otherRoot string) (err error)) (err error)
564 }
565
566+// mountEntry represents a mount this package has created.
567+type mountEntry struct {
568+ source string
569+ target string
570+
571+ options string
572+
573+ // true if target refers to a bind mount. We could derive this
574+ // from options, but this field saves the effort.
575+ bindMount bool
576+}
577+
578+// mountEntryArray represents an array of mountEntry objects.
579+type mountEntryArray []mountEntry
580+
581+// current mounts that this package has created.
582+var mounts mountEntryArray
583+
584 // Partition is the type to interact with the partition
585 type Partition struct {
586 // all partitions
587@@ -179,6 +181,37 @@
588 Bootloader bootloaderName `yaml:"bootloader"`
589 }
590
591+// Len is part of the sort interface, required to allow sort to work
592+// with an array of Mount objects.
593+func (mounts mountEntryArray) Len() int {
594+ return len(mounts)
595+}
596+
597+// Less is part of the sort interface, required to allow sort to work
598+// with an array of Mount objects.
599+func (mounts mountEntryArray) Less(i, j int) bool {
600+ return mounts[i].target < mounts[j].target
601+}
602+
603+// Swap is part of the sort interface, required to allow sort to work
604+// with an array of Mount objects.
605+func (mounts mountEntryArray) Swap(i, j int) {
606+ mounts[i], mounts[j] = mounts[j], mounts[i]
607+}
608+
609+// removeMountByTarget removes the Mount specified by the target from
610+// the global mounts array.
611+func removeMountByTarget(mnts mountEntryArray, target string) (results mountEntryArray) {
612+
613+ for _, m := range mnts {
614+ if m.target != target {
615+ results = append(results, m)
616+ }
617+ }
618+
619+ return results
620+}
621+
622 func init() {
623 if os.Getenv("SNAPPY_DEBUG") != "" {
624 debug = true
625@@ -190,20 +223,34 @@
626 }
627 }
628
629-func undoMounts(mounts []string) (err error) {
630+// undoMounts unmounts all mounts this package has mounted optionally
631+// only unmounting bind mounts and leaving all remaining mounts.
632+func undoMounts(bindMountsOnly bool) error {
633+
634+ mountsCopy := make(mountEntryArray, len(mounts), cap(mounts))
635+ copy(mountsCopy, mounts)
636+
637+ // reverse sort to ensure unmounts are handled in the correct
638+ // order.
639+ sort.Sort(sort.Reverse(mountsCopy))
640+
641 // Iterate backwards since we want a reverse-sorted list of
642 // mounts to ensure we can unmount in order.
643- for i := range mounts {
644- if err := unmountAndRemoveFromGlobalMountList(mounts[len(mounts)-i-1]); err != nil {
645+ for _, mount := range mountsCopy {
646+ if bindMountsOnly && !mount.bindMount {
647+ continue
648+ }
649+
650+ if err := unmountAndRemoveFromGlobalMountList(mount.target); err != nil {
651 return err
652 }
653 }
654
655- return err
656+ return nil
657 }
658
659 func signalHandler(sig os.Signal) {
660- err := undoMounts(mounts)
661+ err := undoMounts(false)
662 if err != nil {
663 // FIXME: use logger
664 fmt.Fprintf(os.Stderr, "ERROR: failed to unmount: %s", err)
665@@ -257,45 +304,31 @@
666 return runCommand(args...)
667 }
668
669-// Mount the given directory and add it to the "mounts" slice
670-func mountAndAddToGlobalMountList(source, target, options string) (err error) {
671- err = mount(source, target, options)
672+// Mount the given directory and add it to the global mounts slice
673+func mountAndAddToGlobalMountList(m mountEntry) (err error) {
674+
675+ err = mount(m.source, m.target, m.options)
676 if err == nil {
677- mounts = append(mounts, target)
678+ mounts = append(mounts, m)
679 }
680
681 return err
682 }
683
684-// Remove the given string from the string slice
685-func stringSliceRemove(slice []string, needle string) (res []string) {
686- // FIXME: so this is golang slice remove?!?! really?
687- if pos := stringInSlice(slice, needle); pos >= 0 {
688- slice = append(slice[:pos], slice[pos+1:]...)
689- }
690- return slice
691-}
692-
693-// FIXME: would it make sense to rename to something like
694-// "UmountAndRemoveFromMountList" to indicate it has side-effects?
695 // Unmount the given directory and remove it from the global "mounts" slice
696 func unmountAndRemoveFromGlobalMountList(target string) (err error) {
697 err = runCommand("/bin/umount", target)
698- if err == nil {
699- mounts = stringSliceRemove(mounts, target)
700- }
701-
702- return err
703-}
704-
705-func bindmountAndAddToGlobalMountList(source, target string) (err error) {
706- err = mountAndAddToGlobalMountList(source, target, "bind")
707-
708- if err == nil {
709- bindMounts = append(bindMounts, target)
710- }
711-
712- return err
713+ if err != nil {
714+ return err
715+
716+ }
717+
718+ results := removeMountByTarget(mounts, target)
719+
720+ // Update global
721+ mounts = results
722+
723+ return nil
724 }
725
726 // Run fsck(8) on specified device.
727@@ -409,9 +442,12 @@
728 return partitions, nil
729 }
730
731+var makeDirectory = func(path string, mode os.FileMode) error {
732+ return os.MkdirAll(path, mode)
733+}
734+
735 func (p *Partition) makeMountPoint() (err error) {
736-
737- return os.MkdirAll(p.MountTarget(), dirMode)
738+ return makeDirectory(p.MountTarget(), dirMode)
739 }
740
741 // New creates a new partition type
742@@ -643,19 +679,31 @@
743
744 other = p.otherRootPartition()
745
746+ m := mountEntry{source: other.device, target: p.MountTarget()}
747+
748 if readOnly {
749- err = mountAndAddToGlobalMountList(other.device, p.MountTarget(), "ro")
750+ m.options = "ro"
751+ err = mountAndAddToGlobalMountList(m)
752 } else {
753- err = fsck(other.device)
754+ err = fsck(m.source)
755 if err != nil {
756 return err
757 }
758- err = mountAndAddToGlobalMountList(other.device, p.MountTarget(), "")
759+ err = mountAndAddToGlobalMountList(m)
760 }
761
762 return err
763 }
764
765+// Create a read-only bindmount of the currently-mounted rootfs at the
766+// specified mountpoint location (which must already exist).
767+func (p *Partition) bindmountThisRootfsRO(target string) (err error) {
768+ return mountAndAddToGlobalMountList(mountEntry{source: "/",
769+ target: target,
770+ options: "bind,ro",
771+ bindMount: true})
772+}
773+
774 // Ensure the other partition is mounted read-only.
775 func (p *Partition) ensureOtherMountedRO() (err error) {
776 mountpoint := p.MountTarget()
777@@ -690,7 +738,9 @@
778 return err
779 }
780
781- return mount(other.device, p.MountTarget(), "")
782+ return mountAndAddToGlobalMountList(mountEntry{
783+ source: other.device,
784+ target: p.MountTarget()})
785 }
786 // r/w -> r/o: no fsck required.
787 return mount(other.device, p.MountTarget(), "remount,ro")
788@@ -724,18 +774,36 @@
789 for _, fs := range requiredChrootMounts {
790 target := path.Join(p.MountTarget(), fs)
791
792- err := bindmountAndAddToGlobalMountList(fs, target)
793+ err := mountAndAddToGlobalMountList(mountEntry{source: fs,
794+ target: target,
795+ options: "bind",
796+ bindMount: true})
797 if err != nil {
798 return err
799 }
800 }
801
802- return nil
803+ // Grub also requires access to both rootfs's when run from
804+ // within a chroot (to allow it to create menu entries for
805+ // both), so bindmount the real rootfs.
806+ targetInChroot := path.Join(p.MountTarget(), p.MountTarget())
807+
808+ // FIXME: we should really remove this after the unmount
809+
810+ if err = makeDirectory(targetInChroot, dirMode); err != nil {
811+ return err
812+ }
813+
814+ return p.bindmountThisRootfsRO(targetInChroot)
815 }
816
817 // Undo the effects of BindmountRequiredFilesystems()
818 func (p *Partition) unmountRequiredFilesystems() (err error) {
819- return undoMounts(bindMounts)
820+ if err = undoMounts(true); err != nil {
821+ return err
822+ }
823+
824+ return nil
825 }
826
827 func (p *Partition) toggleBootloaderRootfs() (err error) {
828
829=== modified file 'partition/partition_test.go'
830--- partition/partition_test.go 2015-03-26 09:12:58 +0000
831+++ partition/partition_test.go 2015-03-27 08:58:19 +0000
832@@ -42,6 +42,10 @@
833 return err
834 }
835
836+func mockMakeDirectory(path string, mode os.FileMode) error {
837+ return nil
838+}
839+
840 func (s *PartitionTestSuite) SetUpTest(c *C) {
841 s.tempdir = c.MkDir()
842 runLsblk = mockRunLsblkDualSnappy
843@@ -50,13 +54,14 @@
844 bootloaderGrubDir = filepath.Join(s.tempdir, "boot", "grub")
845 bootloaderGrubConfigFile = filepath.Join(bootloaderGrubDir, "grub.cfg")
846 bootloaderGrubEnvFile = filepath.Join(bootloaderGrubDir, "grubenv")
847- bootloaderGrubInstallCmd = filepath.Join(s.tempdir, "grub-install")
848 bootloaderGrubUpdateCmd = filepath.Join(s.tempdir, "update-grub")
849
850 // and uboot
851 bootloaderUbootDir = filepath.Join(s.tempdir, "boot", "uboot")
852 bootloaderUbootConfigFile = filepath.Join(bootloaderUbootDir, "uEnv.txt")
853 bootloaderUbootEnvFile = filepath.Join(bootloaderUbootDir, "uEnv.txt")
854+
855+ c.Assert(mounts, DeepEquals, mountEntryArray(nil))
856 }
857
858 func (s *PartitionTestSuite) TearDownTest(c *C) {
859@@ -70,7 +75,6 @@
860 // grub vars
861 bootloaderGrubConfigFile = bootloaderGrubConfigFileReal
862 bootloaderGrubEnvFile = bootloaderGrubEnvFileReal
863- bootloaderGrubInstallCmd = bootloaderGrubInstallCmdReal
864 bootloaderGrubUpdateCmd = bootloaderGrubUpdateCmdReal
865
866 // uboot vars
867@@ -78,6 +82,7 @@
868 bootloaderUbootConfigFile = bootloaderUbootConfigFileReal
869 bootloaderUbootEnvFile = bootloaderUbootEnvFileReal
870
871+ c.Assert(mounts, DeepEquals, mountEntryArray(nil))
872 }
873
874 func makeHardwareYaml(c *C, hardwareYaml string) (outPath string) {
875@@ -127,6 +132,46 @@
876 return strings.Split(dualData, "\n"), err
877 }
878
879+func (s *PartitionTestSuite) TestMountEntryArray(c *C) {
880+ mea := mountEntryArray{}
881+
882+ c.Assert(mea.Len(), Equals, 0)
883+
884+ me := mountEntry{source: "/dev",
885+ target: "/dev",
886+ options: "bind",
887+ bindMount: true}
888+
889+ mea = append(mea, me)
890+ c.Assert(mea.Len(), Equals, 1)
891+
892+ me = mountEntry{source: "/foo",
893+ target: "/foo",
894+ options: "",
895+ bindMount: false}
896+
897+ mea = append(mea, me)
898+ c.Assert(mea.Len(), Equals, 2)
899+
900+ c.Assert(mea.Less(0, 1), Equals, true)
901+ c.Assert(mea.Less(1, 0), Equals, false)
902+
903+ mea.Swap(0, 1)
904+ c.Assert(mea.Less(0, 1), Equals, false)
905+ c.Assert(mea.Less(1, 0), Equals, true)
906+
907+ results := removeMountByTarget(mea, "invalid")
908+
909+ // No change expected
910+ c.Assert(results, DeepEquals, mea)
911+
912+ results = removeMountByTarget(mea, "/dev")
913+
914+ c.Assert(len(results), Equals, 1)
915+ c.Assert(results[0], Equals, mountEntry{source: "/foo",
916+ target: "/foo", options: "", bindMount: false})
917+}
918+
919 func (s *PartitionTestSuite) TestSnappyDualRoot(c *C) {
920 p := New()
921 c.Assert(p.dualRootPartitions(), Equals, true)
922@@ -173,7 +218,10 @@
923 }
924
925 func (s *PartitionTestSuite) TestRunWithOtherDualParitionRWFuncErr(c *C) {
926+ c.Assert(mounts, DeepEquals, mountEntryArray(nil))
927+
928 runCommand = mockRunCommand
929+ makeDirectory = mockMakeDirectory
930
931 p := New()
932 err := p.RunWithOther(RW, func(otherRoot string) (err error) {
933@@ -186,8 +234,16 @@
934
935 // ensure cleanup happend
936
937- // FIXME: mounts is global
938- c.Assert(mounts, DeepEquals, []string{})
939+ // FIXME: mounts are global
940+ expected := mountEntry{source: "/dev/sda4",
941+ target: "/writable/cache/system", options: "", bindMount: false}
942+
943+ // At program exit, "other" should still be mounted
944+ c.Assert(mounts, DeepEquals, mountEntryArray{expected})
945+
946+ undoMounts(false)
947+
948+ c.Assert(mounts, DeepEquals, mountEntryArray(nil))
949 }
950
951 func (s *PartitionTestSuite) TestRunWithOtherSingleParitionRO(c *C) {
952@@ -235,48 +291,94 @@
953 c.Assert(p, NotNil)
954
955 p.mountOtherRootfs(false)
956- c.Assert(mounts, DeepEquals, []string{p.MountTarget()})
957+ expected := mountEntry{source: "/dev/sda4",
958+ target: "/writable/cache/system", options: "", bindMount: false}
959+
960+ c.Assert(mounts, DeepEquals, mountEntryArray{expected})
961+
962 p.unmountOtherRootfs()
963- c.Assert(mounts, DeepEquals, []string{})
964-}
965-
966-func (s *PartitionTestSuite) TestStringSliceRemoveExisting(c *C) {
967- haystack := []string{"one", "two", "three"}
968-
969- newSlice := stringSliceRemove(haystack, "two")
970- c.Assert(newSlice, DeepEquals, []string{"one", "three"})
971-
972- // note here that haystack is no longer "valid", i.e. the
973- // underlying array is modified (which is fine)
974- c.Assert(haystack, DeepEquals, []string{"one", "three", "three"})
975-}
976-
977-func (s *PartitionTestSuite) TestStringSliceRemoveNoexistingNoOp(c *C) {
978- haystack := []string{"6", "28", "496", "8128"}
979- newSlice := stringSliceRemove(haystack, "99")
980- c.Assert(newSlice, DeepEquals, []string{"6", "28", "496", "8128"})
981-}
982-
983-func (s *PartitionTestSuite) TestUndoMounts(c *C) {
984+ c.Assert(mounts, DeepEquals, mountEntryArray(nil))
985+}
986+
987+func (s *PartitionTestSuite) TestUnmountRequiredFilesystems(c *C) {
988 runCommand = mockRunCommand
989 s.makeFakeGrubEnv(c)
990
991 p := New()
992 c.Assert(c, NotNil)
993
994- // FIXME: mounts is global
995- c.Assert(mounts, DeepEquals, []string{})
996 p.bindmountRequiredFilesystems()
997- c.Assert(mounts, DeepEquals, []string{
998- p.MountTarget() + "/dev",
999- p.MountTarget() + "/proc",
1000- p.MountTarget() + "/sys",
1001- p.MountTarget() + "/boot/efi",
1002+ c.Assert(mounts, DeepEquals, mountEntryArray{
1003+ mountEntry{source: "/dev", target: p.MountTarget() + "/dev",
1004+ options: "bind", bindMount: true},
1005+
1006+ mountEntry{source: "/proc", target: p.MountTarget() + "/proc",
1007+ options: "bind", bindMount: true},
1008+
1009+ mountEntry{source: "/sys", target: p.MountTarget() + "/sys",
1010+ options: "bind", bindMount: true},
1011+ mountEntry{source: "/boot/efi", target: p.MountTarget() + "/boot/efi",
1012+ options: "bind", bindMount: true},
1013+
1014 // this comes from the grub bootloader via AdditionalBindMounts
1015- p.MountTarget() + "/boot/grub",
1016+ mountEntry{source: "/boot/grub", target: p.MountTarget() + "/boot/grub",
1017+ options: "bind", bindMount: true},
1018+
1019+ // Required to allow grub inside the chroot to access
1020+ // the "current" rootfs outside the chroot (used
1021+ // to generate the grub menuitems).
1022+ mountEntry{source: "/",
1023+ target: p.MountTarget() + p.MountTarget(),
1024+ options: "bind,ro", bindMount: true},
1025 })
1026 p.unmountRequiredFilesystems()
1027- c.Assert(mounts, DeepEquals, []string{})
1028+ c.Assert(mounts, DeepEquals, mountEntryArray(nil))
1029+}
1030+
1031+func (s *PartitionTestSuite) TestUndoMounts(c *C) {
1032+ runCommand = mockRunCommand
1033+
1034+ p := New()
1035+ c.Assert(c, NotNil)
1036+
1037+ err := p.remountOther(RW)
1038+ c.Assert(err, IsNil)
1039+
1040+ p.bindmountRequiredFilesystems()
1041+ c.Assert(mounts, DeepEquals, mountEntryArray{
1042+
1043+ mountEntry{source: "/dev/sda4", target: "/writable/cache/system",
1044+ options: "", bindMount: false},
1045+
1046+ mountEntry{source: "/dev", target: p.MountTarget() + "/dev",
1047+ options: "bind", bindMount: true},
1048+
1049+ mountEntry{source: "/proc", target: p.MountTarget() + "/proc",
1050+ options: "bind", bindMount: true},
1051+
1052+ mountEntry{source: "/sys", target: p.MountTarget() + "/sys",
1053+ options: "bind", bindMount: true},
1054+
1055+ mountEntry{source: "/boot/efi", target: p.MountTarget() + "/boot/efi",
1056+ options: "bind", bindMount: true},
1057+
1058+ mountEntry{source: "/",
1059+ target: p.MountTarget() + p.MountTarget(),
1060+ options: "bind,ro", bindMount: true},
1061+ })
1062+
1063+ // should leave non-bind mounts
1064+ undoMounts(true)
1065+
1066+ c.Assert(mounts, DeepEquals, mountEntryArray{
1067+ mountEntry{source: "/dev/sda4", target: "/writable/cache/system",
1068+ options: "", bindMount: false},
1069+ })
1070+
1071+ // should unmount everything
1072+ undoMounts(false)
1073+
1074+ c.Assert(mounts, DeepEquals, mountEntryArray(nil))
1075 }
1076
1077 func mockRunLsblkNoSnappy() (output []string, err error) {
1078@@ -361,6 +463,9 @@
1079 c.Assert(err, IsNil)
1080 c.Assert(b.ToggleRootFSCalled, Equals, true)
1081 c.Assert(b.HandleAssetsCalled, Equals, true)
1082+
1083+ p.unmountOtherRootfs()
1084+ c.Assert(mounts, DeepEquals, mountEntryArray(nil))
1085 }
1086
1087 func (s *PartitionTestSuite) TestMarkBootSuccessful(c *C) {

Subscribers

People subscribed via source and target branches