gitee.com/mysnapcore/mysnapd@v0.1.0/image/preseed/preseed_linux.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-2022 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package preseed
    21  
    22  import (
    23  	"crypto"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io/ioutil"
    28  	"os"
    29  	"os/exec"
    30  	"path/filepath"
    31  	"sort"
    32  	"strings"
    33  	"syscall"
    34  
    35  	"gitee.com/mysnapcore/mysnapd/dirs"
    36  	"gitee.com/mysnapcore/mysnapd/osutil"
    37  	"gitee.com/mysnapcore/mysnapd/osutil/squashfs"
    38  	"gitee.com/mysnapcore/mysnapd/snapdtool"
    39  	"gitee.com/mysnapcore/mysnapd/strutil"
    40  	"gitee.com/mysnapcore/mysnapd/timings"
    41  )
    42  
    43  var (
    44  	// snapdMountPath is where target core/snapd is going to be mounted in the target chroot
    45  	snapdMountPath = "/tmp/snapd-preseed"
    46  	syscallChroot  = syscall.Chroot
    47  )
    48  
    49  // checkChroot does a basic validity check of the target chroot environment, e.g. makes
    50  // sure critical virtual filesystems (such as proc) are mounted. This is not meant to
    51  // be exhaustive check, but one that prevents running the tool against a wrong directory
    52  // by an accident, which would lead to hard to understand errors from snapd in preseed
    53  // mode.
    54  func checkChroot(preseedChroot string) error {
    55  	exists, isDir, err := osutil.DirExists(preseedChroot)
    56  	if err != nil {
    57  		return fmt.Errorf("cannot verify %q: %v", preseedChroot, err)
    58  	}
    59  	if !exists || !isDir {
    60  		return fmt.Errorf("cannot verify %q: is not a directory", preseedChroot)
    61  	}
    62  
    63  	if osutil.FileExists(filepath.Join(preseedChroot, dirs.SnapStateFile)) {
    64  		return fmt.Errorf("the system at %q appears to be preseeded, pass --reset flag to clean it up", preseedChroot)
    65  	}
    66  
    67  	// validity checks of the critical mountpoints inside chroot directory.
    68  	required := map[string]bool{}
    69  	// required mountpoints are relative to the preseed chroot
    70  	for _, p := range []string{"/sys/kernel/security", "/proc", "/dev"} {
    71  		required[filepath.Join(preseedChroot, p)] = true
    72  	}
    73  	entries, err := osutil.LoadMountInfo()
    74  	if err != nil {
    75  		return fmt.Errorf("cannot parse mount info: %v", err)
    76  	}
    77  	for _, ent := range entries {
    78  		if _, ok := required[ent.MountDir]; ok {
    79  			delete(required, ent.MountDir)
    80  		}
    81  	}
    82  	// non empty required indicates missing mountpoint(s)
    83  	if len(required) > 0 {
    84  		var sorted []string
    85  		for path := range required {
    86  			sorted = append(sorted, path)
    87  		}
    88  		sort.Strings(sorted)
    89  		parts := append([]string{""}, sorted...)
    90  		return fmt.Errorf("cannot preseed without the following mountpoints:%s", strings.Join(parts, "\n - "))
    91  	}
    92  
    93  	path := filepath.Join(preseedChroot, "/sys/kernel/security/apparmor")
    94  	if exists := osutil.FileExists(path); !exists {
    95  		return fmt.Errorf("cannot preseed without access to %q", path)
    96  	}
    97  
    98  	return nil
    99  }
   100  
   101  var systemSnapFromSeed = func(seedDir, sysLabel string) (systemSnap string, baseSnap string, err error) {
   102  	seed, err := seedOpen(seedDir, sysLabel)
   103  	if err != nil {
   104  		return "", "", err
   105  	}
   106  
   107  	// load assertions into temporary database
   108  	if err := seed.LoadAssertions(nil, nil); err != nil {
   109  		return "", "", err
   110  	}
   111  	model := seed.Model()
   112  
   113  	tm := timings.New(nil)
   114  
   115  	if err := seed.LoadEssentialMeta(nil, tm); err != nil {
   116  		return "", "", err
   117  	}
   118  
   119  	if model.Classic() {
   120  		fmt.Fprintf(Stdout, "ubuntu classic preseeding\n")
   121  	} else {
   122  		if model.Base() == "core20" {
   123  			fmt.Fprintf(Stdout, "UC20+ preseeding\n")
   124  		} else {
   125  			// TODO: support uc20+
   126  			return "", "", fmt.Errorf("preseeding of ubuntu core with base %s is not supported", model.Base())
   127  		}
   128  	}
   129  
   130  	var required string
   131  	if seed.UsesSnapdSnap() {
   132  		required = "snapd"
   133  	} else {
   134  		required = "core"
   135  	}
   136  
   137  	var systemSnapPath, baseSnapPath string
   138  	for _, ess := range seed.EssentialSnaps() {
   139  		if ess.SnapName() == required {
   140  			systemSnapPath = ess.Path
   141  		}
   142  		if ess.EssentialType == "base" {
   143  			baseSnapPath = ess.Path
   144  		}
   145  	}
   146  
   147  	if systemSnapPath == "" {
   148  		return "", "", fmt.Errorf("%s snap not found", required)
   149  	}
   150  
   151  	return systemSnapPath, baseSnapPath, nil
   152  }
   153  
   154  const snapdPreseedSupportVer = `2.43.3+`
   155  
   156  // chooseTargetSnapdVersion checks if the version of snapd under chroot env
   157  // is good enough for preseeding. It checks both the snapd from the deb
   158  // and from the seeded snap mounted under snapdMountPath and returns the
   159  // information (path, version) about snapd to execute as part of preseeding
   160  // (it picks the newer version of the two).
   161  // The function must be called after syscall.Chroot(..).
   162  func chooseTargetSnapdVersion() (*targetSnapdInfo, error) {
   163  	// read snapd version from the mounted core/snapd snap
   164  	snapdInfoDir := filepath.Join(snapdMountPath, dirs.CoreLibExecDir)
   165  	verFromSnap, _, err := snapdtool.SnapdVersionFromInfoFile(snapdInfoDir)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	// read snapd version from the main fs under chroot (snapd from the deb);
   171  	// assumes running under chroot already.
   172  	hostInfoDir := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir)
   173  	verFromDeb, _, err := snapdtool.SnapdVersionFromInfoFile(hostInfoDir)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	res, err := strutil.VersionCompare(verFromSnap, verFromDeb)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	var whichVer, snapdPath string
   184  	if res < 0 {
   185  		// snapd from the deb under chroot is the candidate to run
   186  		whichVer = verFromDeb
   187  		snapdPath = filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "snapd")
   188  	} else {
   189  		// snapd from the mounted core/snapd snap is the candidate to run
   190  		whichVer = verFromSnap
   191  		snapdPath = filepath.Join(snapdMountPath, dirs.CoreLibExecDir, "snapd")
   192  	}
   193  
   194  	res, err = strutil.VersionCompare(whichVer, snapdPreseedSupportVer)
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	if res < 0 {
   199  		return nil, fmt.Errorf("snapd %s from the target system does not support preseeding, the minimum required version is %s",
   200  			whichVer, snapdPreseedSupportVer)
   201  	}
   202  
   203  	return &targetSnapdInfo{path: snapdPath, version: whichVer}, nil
   204  }
   205  
   206  func prepareCore20Mountpoints(prepareImageDir, tmpPreseedChrootDir, snapdSnapBlob, baseSnapBlob, aaFeaturesDir, writable string) (cleanupMounts func(), err error) {
   207  	underPreseed := func(path string) string {
   208  		return filepath.Join(tmpPreseedChrootDir, path)
   209  	}
   210  
   211  	if err := os.MkdirAll(filepath.Join(writable, "system-data", "etc"), 0755); err != nil {
   212  		return nil, err
   213  	}
   214  	where := filepath.Join(snapdMountPath)
   215  	if err := os.MkdirAll(where, 0755); err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	var mounted []string
   220  
   221  	doUnmount := func(mnt string) {
   222  		cmd := exec.Command("umount", mnt)
   223  		if out, err := cmd.CombinedOutput(); err != nil {
   224  			fmt.Fprintf(Stdout, "cannot unmount: %v\n'umount %s' failed with: %s", err, mnt, out)
   225  		}
   226  	}
   227  
   228  	cleanupMounts = func() {
   229  		// unmount all the mounts but the first one, which is the base
   230  		// and it is cleaned up last
   231  		for i := len(mounted) - 1; i > 0; i-- {
   232  			mnt := mounted[i]
   233  			doUnmount(mnt)
   234  		}
   235  
   236  		entries, err := osutil.LoadMountInfo()
   237  		if err != nil {
   238  			fmt.Fprintf(Stdout, "cannot parse mount info when cleaning up mount points: %v", err)
   239  			return
   240  		}
   241  		// cleanup after handle-writable-paths
   242  		for _, ent := range entries {
   243  			if ent.MountDir != tmpPreseedChrootDir && strings.HasPrefix(ent.MountDir, tmpPreseedChrootDir) {
   244  				doUnmount(ent.MountDir)
   245  			}
   246  		}
   247  
   248  		// finally, umount the base snap
   249  		if len(mounted) > 0 {
   250  			doUnmount(mounted[0])
   251  		}
   252  	}
   253  
   254  	cleanupOnError := func() {
   255  		if err == nil {
   256  			return
   257  		}
   258  
   259  		if cleanupMounts != nil {
   260  			cleanupMounts()
   261  		}
   262  	}
   263  	defer cleanupOnError()
   264  
   265  	mounts := [][]string{
   266  		{"-o", "loop", baseSnapBlob, tmpPreseedChrootDir},
   267  		{"-o", "loop", snapdSnapBlob, snapdMountPath},
   268  		{"-t", "tmpfs", "tmpfs", underPreseed("run")},
   269  		{"-t", "tmpfs", "tmpfs", underPreseed("var/tmp")},
   270  		{"--bind", underPreseed("var/tmp"), underPreseed("tmp")},
   271  		{"-t", "proc", "proc", underPreseed("proc")},
   272  		{"-t", "sysfs", "sysfs", underPreseed("sys")},
   273  		{"-t", "devtmpfs", "udev", underPreseed("dev")},
   274  		{"-t", "securityfs", "securityfs", underPreseed("sys/kernel/security")},
   275  		{"--bind", writable, underPreseed("writable")},
   276  	}
   277  
   278  	var out []byte
   279  	for _, mountArgs := range mounts {
   280  		cmd := exec.Command("mount", mountArgs...)
   281  		if out, err = cmd.CombinedOutput(); err != nil {
   282  			return nil, fmt.Errorf("cannot prepare mountpoint in preseed mode: %v\n'mount %s' failed with: %s", err, strings.Join(mountArgs, " "), out)
   283  		}
   284  		mounted = append(mounted, mountArgs[len(mountArgs)-1])
   285  	}
   286  
   287  	cmd := exec.Command(underPreseed("/usr/lib/core/handle-writable-paths"), tmpPreseedChrootDir)
   288  	if out, err = cmd.CombinedOutput(); err != nil {
   289  		return nil, fmt.Errorf("handle-writable-paths failed with: %v\n%s", err, out)
   290  	}
   291  
   292  	for _, dir := range []string{
   293  		"etc/udev/rules.d", "etc/systemd/system", "etc/dbus-1/session.d",
   294  		"var/lib/snapd/seed", "var/cache/snapd", "var/cache/apparmor",
   295  		"var/snap", "snap", "var/lib/extrausers",
   296  	} {
   297  		if err = os.MkdirAll(filepath.Join(writable, dir), 0755); err != nil {
   298  			return nil, err
   299  		}
   300  	}
   301  
   302  	underWritable := func(path string) string {
   303  		return filepath.Join(writable, path)
   304  	}
   305  	mounts = [][]string{
   306  		{"--bind", underWritable("system-data/var/lib/snapd"), underPreseed("var/lib/snapd")},
   307  		{"--bind", underWritable("system-data/var/cache/snapd"), underPreseed("var/cache/snapd")},
   308  		{"--bind", underWritable("system-data/var/cache/apparmor"), underPreseed("var/cache/apparmor")},
   309  		{"--bind", underWritable("system-data/var/snap"), underPreseed("var/snap")},
   310  		{"--bind", underWritable("system-data/snap"), underPreseed("snap")},
   311  		{"--bind", underWritable("system-data/etc/systemd"), underPreseed("etc/systemd")},
   312  		{"--bind", underWritable("system-data/etc/dbus-1"), underPreseed("etc/dbus-1")},
   313  		{"--bind", underWritable("system-data/etc/udev/rules.d"), underPreseed("etc/udev/rules.d")},
   314  		{"--bind", underWritable("system-data/var/lib/extrausers"), underPreseed("var/lib/extrausers")},
   315  		{"--bind", filepath.Join(snapdMountPath, "/usr/lib/snapd"), underPreseed("/usr/lib/snapd")},
   316  		{"--bind", filepath.Join(prepareImageDir, "system-seed"), underPreseed("var/lib/snapd/seed")},
   317  	}
   318  
   319  	if aaFeaturesDir != "" {
   320  		mounts = append(mounts, []string{"--bind", aaFeaturesDir, underPreseed("sys/kernel/security/apparmor/features")})
   321  	}
   322  
   323  	for _, mountArgs := range mounts {
   324  		cmd := exec.Command("mount", mountArgs...)
   325  		if out, err = cmd.CombinedOutput(); err != nil {
   326  			return nil, fmt.Errorf("cannot prepare mountpoint in preseed mode: %v\n'mount %s' failed with: %s", err, strings.Join(mountArgs, " "), out)
   327  		}
   328  		mounted = append(mounted, mountArgs[len(mountArgs)-1])
   329  	}
   330  
   331  	return cleanupMounts, nil
   332  }
   333  
   334  func systemForPreseeding(systemsDir string) (label string, err error) {
   335  	systemLabels, err := filepath.Glob(filepath.Join(systemsDir, "systems", "*"))
   336  	if err != nil && !os.IsNotExist(err) {
   337  		return "", fmt.Errorf("cannot list available systems: %v", err)
   338  	}
   339  	if len(systemLabels) != 1 {
   340  		return "", fmt.Errorf("expected a single system for preseeding, found %d", len(systemLabels))
   341  	}
   342  	return filepath.Base(systemLabels[0]), nil
   343  }
   344  
   345  var makePreseedTempDir = func() (string, error) {
   346  	return ioutil.TempDir("", "preseed-")
   347  }
   348  
   349  var makeWritableTempDir = func() (string, error) {
   350  	return ioutil.TempDir("", "writable-")
   351  }
   352  
   353  func prepareCore20Chroot(prepareImageDir, aaFeaturesDir string) (preseed *preseedOpts, cleanup func(), err error) {
   354  	sysDir := filepath.Join(prepareImageDir, "system-seed")
   355  	sysLabel, err := systemForPreseeding(sysDir)
   356  	if err != nil {
   357  		return nil, nil, err
   358  	}
   359  	snapdSnapPath, baseSnapPath, err := systemSnapFromSeed(sysDir, sysLabel)
   360  	if err != nil {
   361  		return nil, nil, err
   362  	}
   363  
   364  	if snapdSnapPath == "" {
   365  		return nil, nil, fmt.Errorf("snapd snap not found")
   366  	}
   367  	if baseSnapPath == "" {
   368  		return nil, nil, fmt.Errorf("base snap not found")
   369  	}
   370  
   371  	tmpPreseedChrootDir, err := makePreseedTempDir()
   372  	if err != nil {
   373  		return nil, nil, fmt.Errorf("cannot prepare uc20 chroot: %v", err)
   374  	}
   375  	writableTmpDir, err := makeWritableTempDir()
   376  	if err != nil {
   377  		return nil, nil, fmt.Errorf("cannot prepare uc20 chroot: %v", err)
   378  	}
   379  
   380  	cleanupMounts, err := prepareCore20Mountpoints(prepareImageDir, tmpPreseedChrootDir, snapdSnapPath, baseSnapPath, aaFeaturesDir, writableTmpDir)
   381  	if err != nil {
   382  		return nil, nil, fmt.Errorf("cannot prepare uc20 mountpoints: %v", err)
   383  	}
   384  
   385  	cleanup = func() {
   386  		cleanupMounts()
   387  		if err := os.RemoveAll(tmpPreseedChrootDir); err != nil {
   388  			fmt.Fprintf(Stdout, "%v", err)
   389  		}
   390  		if err := os.RemoveAll(writableTmpDir); err != nil {
   391  			fmt.Fprintf(Stdout, "%v", err)
   392  		}
   393  		if err := os.RemoveAll(snapdMountPath); err != nil {
   394  			fmt.Fprintf(Stdout, "%v", err)
   395  		}
   396  	}
   397  
   398  	opts := &preseedOpts{
   399  		PrepareImageDir:  prepareImageDir,
   400  		PreseedChrootDir: tmpPreseedChrootDir,
   401  		SystemLabel:      sysLabel,
   402  		WritableDir:      writableTmpDir,
   403  	}
   404  	return opts, cleanup, nil
   405  }
   406  
   407  func prepareClassicChroot(preseedChroot string) (*targetSnapdInfo, func(), error) {
   408  	if err := syscallChroot(preseedChroot); err != nil {
   409  		return nil, nil, fmt.Errorf("cannot chroot into %s: %v", preseedChroot, err)
   410  	}
   411  
   412  	if err := os.Chdir("/"); err != nil {
   413  		return nil, nil, fmt.Errorf("cannot chdir to /: %v", err)
   414  	}
   415  
   416  	// GlobalRootDir is now relative to chroot env. We assume all paths
   417  	// inside the chroot to be identical with the host.
   418  	rootDir := dirs.GlobalRootDir
   419  	if rootDir == "" {
   420  		rootDir = "/"
   421  	}
   422  
   423  	coreSnapPath, _, err := systemSnapFromSeed(dirs.SnapSeedDirUnder(rootDir), "")
   424  	if err != nil {
   425  		return nil, nil, err
   426  	}
   427  
   428  	// create mountpoint for core/snapd
   429  	where := filepath.Join(rootDir, snapdMountPath)
   430  	if err := os.MkdirAll(where, 0755); err != nil {
   431  		return nil, nil, err
   432  	}
   433  
   434  	removeMountpoint := func() {
   435  		if err := os.Remove(where); err != nil {
   436  			fmt.Fprintf(Stderr, "%v", err)
   437  		}
   438  	}
   439  
   440  	fstype, fsopts := squashfs.FsType()
   441  	mountArgs := []string{"-t", fstype, "-o", strings.Join(fsopts, ","), coreSnapPath, where}
   442  	cmd := exec.Command("mount", mountArgs...)
   443  	if out, err := cmd.CombinedOutput(); err != nil {
   444  		removeMountpoint()
   445  		return nil, nil, fmt.Errorf("cannot mount %s at %s in preseed mode: %v\n'mount %s' failed with: %s", coreSnapPath, where, err, strings.Join(mountArgs, " "), out)
   446  	}
   447  
   448  	unmount := func() {
   449  		fmt.Fprintf(Stdout, "unmounting: %s\n", snapdMountPath)
   450  		cmd := exec.Command("umount", snapdMountPath)
   451  		if err := cmd.Run(); err != nil {
   452  			fmt.Fprintf(Stderr, "%v", err)
   453  		}
   454  	}
   455  
   456  	targetSnapd, err := chooseTargetSnapdVersion()
   457  	if err != nil {
   458  		unmount()
   459  		removeMountpoint()
   460  		return nil, nil, err
   461  	}
   462  
   463  	return targetSnapd, func() {
   464  		unmount()
   465  		removeMountpoint()
   466  	}, nil
   467  }
   468  
   469  type preseedFilePatterns struct {
   470  	Exclude []string `json:"exclude"`
   471  	Include []string `json:"include"`
   472  }
   473  
   474  func createPreseedArtifact(opts *preseedOpts) (digest []byte, err error) {
   475  	artifactPath := filepath.Join(opts.PrepareImageDir, "system-seed", "systems", opts.SystemLabel, "preseed.tgz")
   476  	systemData := filepath.Join(opts.WritableDir, "system-data")
   477  
   478  	patternsFile := filepath.Join(opts.PreseedChrootDir, "usr/lib/snapd/preseed.json")
   479  	pf, err := os.Open(patternsFile)
   480  	if err != nil {
   481  		return nil, err
   482  	}
   483  
   484  	var patterns preseedFilePatterns
   485  	dec := json.NewDecoder(pf)
   486  	if err := dec.Decode(&patterns); err != nil {
   487  		return nil, err
   488  	}
   489  
   490  	args := []string{"-czf", artifactPath, "-p", "-C", systemData}
   491  	for _, excl := range patterns.Exclude {
   492  		args = append(args, "--exclude", excl)
   493  	}
   494  	for _, incl := range patterns.Include {
   495  		// tar doesn't support globs for files to include, since we are not using shell we need to
   496  		// handle globs explicitly.
   497  		matches, err := filepath.Glob(filepath.Join(systemData, incl))
   498  		if err != nil {
   499  			return nil, err
   500  		}
   501  		for _, m := range matches {
   502  			relPath, err := filepath.Rel(systemData, m)
   503  			if err != nil {
   504  				return nil, err
   505  			}
   506  			args = append(args, relPath)
   507  		}
   508  	}
   509  
   510  	cmd := exec.Command("tar", args...)
   511  	if out, err := cmd.CombinedOutput(); err != nil {
   512  		return nil, fmt.Errorf("%v (%s)", err, out)
   513  	}
   514  
   515  	sha3_384, _, err := osutil.FileDigest(artifactPath, crypto.SHA3_384)
   516  	return sha3_384, err
   517  }
   518  
   519  // runPreseedMode runs snapd in a preseed mode. It assumes running in a chroot.
   520  // The chroot is expected to be set-up and ready to use (critical system directories mounted).
   521  func runPreseedMode(preseedChroot string, targetSnapd *targetSnapdInfo) error {
   522  	// run snapd in preseed mode
   523  	cmd := exec.Command(targetSnapd.path)
   524  	cmd.Env = os.Environ()
   525  	cmd.Env = append(cmd.Env, "SNAPD_PRESEED=1")
   526  	cmd.Stderr = Stderr
   527  	cmd.Stdout = Stdout
   528  
   529  	// note, snapdPath is relative to preseedChroot
   530  	fmt.Fprintf(Stdout, "starting to preseed root: %s\nusing snapd binary: %s (%s)\n", preseedChroot, targetSnapd.path, targetSnapd.version)
   531  
   532  	if err := cmd.Run(); err != nil {
   533  		return fmt.Errorf("error running snapd in preseed mode: %v\n", err)
   534  	}
   535  
   536  	return nil
   537  }
   538  
   539  func runUC20PreseedMode(opts *preseedOpts) error {
   540  	cmd := exec.Command("chroot", opts.PreseedChrootDir, "/usr/lib/snapd/snapd")
   541  	cmd.Env = os.Environ()
   542  	cmd.Env = append(cmd.Env, "SNAPD_PRESEED=1")
   543  	cmd.Stderr = Stderr
   544  	cmd.Stdout = Stdout
   545  	fmt.Fprintf(Stdout, "starting to preseed UC20+ system: %s\n", opts.PreseedChrootDir)
   546  
   547  	if err := cmd.Run(); err != nil {
   548  		var errno syscall.Errno
   549  		if errors.As(err, &errno) && errno == syscall.ENOEXEC {
   550  			return fmt.Errorf(`error running snapd, please try installing the "qemu-user-static" package: %v`, err)
   551  		}
   552  
   553  		return fmt.Errorf("error running snapd in preseed mode: %v\n", err)
   554  	}
   555  
   556  	digest, err := createPreseedArtifact(opts)
   557  	if err != nil {
   558  		return fmt.Errorf("cannot create preseed.tgz: %v", err)
   559  	}
   560  
   561  	if err := writePreseedAssertion(digest, opts); err != nil {
   562  		return fmt.Errorf("cannot create preseed assertion: %v", err)
   563  	}
   564  
   565  	return nil
   566  }
   567  
   568  // Core20 runs preseeding of UC20 system prepared by prepare-image in prepareImageDir
   569  // and stores the resulting preseed preseed.tgz file in system-seed/systems/<systemlabel>/preseed.tgz.
   570  // Expects single systemlabel under systems directory.
   571  func Core20(prepareImageDir, preseedSignKey, aaFeaturesDir string) error {
   572  	var err error
   573  	prepareImageDir, err = filepath.Abs(prepareImageDir)
   574  	if err != nil {
   575  		return err
   576  	}
   577  
   578  	popts, cleanup, err := prepareCore20Chroot(prepareImageDir, aaFeaturesDir)
   579  	if err != nil {
   580  		return err
   581  	}
   582  	defer cleanup()
   583  
   584  	popts.PreseedSignKey = preseedSignKey
   585  	return runUC20PreseedMode(popts)
   586  }
   587  
   588  // Classic runs preseeding of a classic ubuntu system pointed by chrootDir.
   589  func Classic(chrootDir string) error {
   590  	var err error
   591  	chrootDir, err = filepath.Abs(chrootDir)
   592  	if err != nil {
   593  		return err
   594  	}
   595  
   596  	if err := checkChroot(chrootDir); err != nil {
   597  		return err
   598  	}
   599  
   600  	var targetSnapd *targetSnapdInfo
   601  
   602  	// XXX: if prepareClassicChroot & runPreseedMode were refactored to
   603  	// use "chroot" inside runPreseedMode (and not syscall.Chroot at the
   604  	// beginning of prepareClassicChroot), then we could have a single
   605  	// runPreseedMode/runUC20PreseedMode function that handles both classic
   606  	// and core20.
   607  	targetSnapd, cleanup, err := prepareClassicChroot(chrootDir)
   608  	if err != nil {
   609  		return err
   610  	}
   611  	defer cleanup()
   612  
   613  	// executing inside the chroot
   614  	return runPreseedMode(chrootDir, targetSnapd)
   615  }
   616  
   617  func MockSyscallChroot(f func(string) error) (restore func()) {
   618  	osutil.MustBeTestBinary("mocking can be done only in tests")
   619  
   620  	oldSyscallChroot := syscallChroot
   621  	syscallChroot = f
   622  	return func() { syscallChroot = oldSyscallChroot }
   623  }
   624  
   625  func MockSnapdMountPath(path string) (restore func()) {
   626  	osutil.MustBeTestBinary("mocking can be done only in tests")
   627  
   628  	oldMountPath := snapdMountPath
   629  	snapdMountPath = path
   630  	return func() { snapdMountPath = oldMountPath }
   631  }
   632  
   633  func MockSystemSnapFromSeed(f func(rootDir, sysLabel string) (string, string, error)) (restore func()) {
   634  	osutil.MustBeTestBinary("mocking can be done only in tests")
   635  
   636  	oldSystemSnapFromSeed := systemSnapFromSeed
   637  	systemSnapFromSeed = f
   638  	return func() { systemSnapFromSeed = oldSystemSnapFromSeed }
   639  }
   640  
   641  func MockMakePreseedTempDir(f func() (string, error)) (restore func()) {
   642  	osutil.MustBeTestBinary("mocking can be done only in tests")
   643  
   644  	old := makePreseedTempDir
   645  	makePreseedTempDir = f
   646  	return func() {
   647  		makePreseedTempDir = old
   648  	}
   649  }
   650  
   651  func MockMakeWritableTempDir(f func() (string, error)) (restore func()) {
   652  	osutil.MustBeTestBinary("mocking can be done only in tests")
   653  
   654  	old := makeWritableTempDir
   655  	makeWritableTempDir = f
   656  	return func() {
   657  		makeWritableTempDir = old
   658  	}
   659  }