github.com/ubuntu-core/snappy@v0.0.0-20210827154228-9e584df982bb/cmd/snap-preseed/preseed_linux.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-2020 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 main
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"os/exec"
    26  	"path/filepath"
    27  	"sort"
    28  	"strings"
    29  	"syscall"
    30  
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/osutil"
    33  	"github.com/snapcore/snapd/osutil/squashfs"
    34  	"github.com/snapcore/snapd/seed"
    35  	"github.com/snapcore/snapd/snapdtool"
    36  	"github.com/snapcore/snapd/strutil"
    37  	"github.com/snapcore/snapd/timings"
    38  )
    39  
    40  var (
    41  	// snapdMountPath is where target core/snapd is going to be mounted in the target chroot
    42  	snapdMountPath = "/tmp/snapd-preseed"
    43  	syscallChroot  = syscall.Chroot
    44  )
    45  
    46  // checkChroot does a basic sanity check of the target chroot environment, e.g. makes
    47  // sure critical virtual filesystems (such as proc) are mounted. This is not meant to
    48  // be exhaustive check, but one that prevents running the tool against a wrong directory
    49  // by an accident, which would lead to hard to understand errors from snapd in preseed
    50  // mode.
    51  func checkChroot(preseedChroot string) error {
    52  	exists, isDir, err := osutil.DirExists(preseedChroot)
    53  	if err != nil {
    54  		return fmt.Errorf("cannot verify %q: %v", preseedChroot, err)
    55  	}
    56  	if !exists || !isDir {
    57  		return fmt.Errorf("cannot verify %q: is not a directory", preseedChroot)
    58  	}
    59  
    60  	if osutil.FileExists(filepath.Join(preseedChroot, dirs.SnapStateFile)) {
    61  		return fmt.Errorf("the system at %q appears to be preseeded, pass --reset flag to clean it up", preseedChroot)
    62  	}
    63  
    64  	// sanity checks of the critical mountpoints inside chroot directory.
    65  	required := map[string]bool{}
    66  	// required mountpoints are relative to the preseed chroot
    67  	for _, p := range []string{"/sys/kernel/security", "/proc", "/dev"} {
    68  		required[filepath.Join(preseedChroot, p)] = true
    69  	}
    70  	entries, err := osutil.LoadMountInfo()
    71  	if err != nil {
    72  		return fmt.Errorf("cannot parse mount info: %v", err)
    73  	}
    74  	for _, ent := range entries {
    75  		if _, ok := required[ent.MountDir]; ok {
    76  			delete(required, ent.MountDir)
    77  		}
    78  	}
    79  	// non empty required indicates missing mountpoint(s)
    80  	if len(required) > 0 {
    81  		var sorted []string
    82  		for path := range required {
    83  			sorted = append(sorted, path)
    84  		}
    85  		sort.Strings(sorted)
    86  		parts := append([]string{""}, sorted...)
    87  		return fmt.Errorf("cannot preseed without the following mountpoints:%s", strings.Join(parts, "\n - "))
    88  	}
    89  
    90  	path := filepath.Join(preseedChroot, "/sys/kernel/security/apparmor")
    91  	if exists := osutil.FileExists(path); !exists {
    92  		return fmt.Errorf("cannot preseed without access to %q", path)
    93  	}
    94  
    95  	return nil
    96  }
    97  
    98  var seedOpen = seed.Open
    99  
   100  var systemSnapFromSeed = func(rootDir string) (string, error) {
   101  	seedDir := filepath.Join(dirs.SnapSeedDirUnder(rootDir))
   102  	seed, err := seedOpen(seedDir, "")
   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  	if err := seed.LoadMeta(tm); err != nil {
   115  		return "", err
   116  	}
   117  
   118  	// TODO: implement preseeding for core.
   119  	if !model.Classic() {
   120  		return "", fmt.Errorf("preseeding is only supported on classic systems")
   121  	}
   122  
   123  	var required string
   124  	if seed.UsesSnapdSnap() {
   125  		required = "snapd"
   126  	} else {
   127  		required = "core"
   128  	}
   129  
   130  	var snapPath string
   131  	ess := seed.EssentialSnaps()
   132  	if len(ess) > 0 {
   133  		// core / snapd snap is the first essential snap.
   134  		if ess[0].SnapName() == required {
   135  			snapPath = ess[0].Path
   136  		}
   137  	}
   138  
   139  	if snapPath == "" {
   140  		return "", fmt.Errorf("%s snap not found", required)
   141  	}
   142  
   143  	return snapPath, nil
   144  }
   145  
   146  const snapdPreseedSupportVer = `2.43.3+`
   147  
   148  type targetSnapdInfo struct {
   149  	path    string
   150  	version string
   151  }
   152  
   153  // chooseTargetSnapdVersion checks if the version of snapd under chroot env
   154  // is good enough for preseeding. It checks both the snapd from the deb
   155  // and from the seeded snap mounted under snapdMountPath and returns the
   156  // information (path, version) about snapd to execute as part of preseeding
   157  // (it picks the newer version of the two).
   158  // The function must be called after syscall.Chroot(..).
   159  func chooseTargetSnapdVersion() (*targetSnapdInfo, error) {
   160  	// read snapd version from the mounted core/snapd snap
   161  	infoPath := filepath.Join(snapdMountPath, dirs.CoreLibExecDir, "info")
   162  	verFromSnap, err := snapdtool.SnapdVersionFromInfoFile(infoPath)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	// read snapd version from the main fs under chroot (snapd from the deb);
   168  	// assumes running under chroot already.
   169  	infoPath = filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "info")
   170  	verFromDeb, err := snapdtool.SnapdVersionFromInfoFile(infoPath)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	res, err := strutil.VersionCompare(verFromSnap, verFromDeb)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	var whichVer, snapdPath string
   181  	if res < 0 {
   182  		// snapd from the deb under chroot is the candidate to run
   183  		whichVer = verFromDeb
   184  		snapdPath = filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "snapd")
   185  	} else {
   186  		// snapd from the mounted core/snapd snap is the candidate to run
   187  		whichVer = verFromSnap
   188  		snapdPath = filepath.Join(snapdMountPath, dirs.CoreLibExecDir, "snapd")
   189  	}
   190  
   191  	res, err = strutil.VersionCompare(whichVer, snapdPreseedSupportVer)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	if res < 0 {
   196  		return nil, fmt.Errorf("snapd %s from the target system does not support preseeding, the minimum required version is %s",
   197  			whichVer, snapdPreseedSupportVer)
   198  	}
   199  
   200  	return &targetSnapdInfo{path: snapdPath, version: whichVer}, nil
   201  }
   202  
   203  func prepareChroot(preseedChroot string) (*targetSnapdInfo, func(), error) {
   204  	if err := syscallChroot(preseedChroot); err != nil {
   205  		return nil, nil, fmt.Errorf("cannot chroot into %s: %v", preseedChroot, err)
   206  	}
   207  
   208  	if err := os.Chdir("/"); err != nil {
   209  		return nil, nil, fmt.Errorf("cannot chdir to /: %v", err)
   210  	}
   211  
   212  	// GlobalRootDir is now relative to chroot env. We assume all paths
   213  	// inside the chroot to be identical with the host.
   214  	rootDir := dirs.GlobalRootDir
   215  	if rootDir == "" {
   216  		rootDir = "/"
   217  	}
   218  
   219  	coreSnapPath, err := systemSnapFromSeed(rootDir)
   220  	if err != nil {
   221  		return nil, nil, err
   222  	}
   223  
   224  	// create mountpoint for core/snapd
   225  	where := filepath.Join(rootDir, snapdMountPath)
   226  	if err := os.MkdirAll(where, 0755); err != nil {
   227  		return nil, nil, err
   228  	}
   229  
   230  	removeMountpoint := func() {
   231  		if err := os.Remove(where); err != nil {
   232  			fmt.Fprintf(Stderr, "%v", err)
   233  		}
   234  	}
   235  
   236  	fstype, fsopts := squashfs.FsType()
   237  	mountArgs := []string{"-t", fstype, "-o", strings.Join(fsopts, ","), coreSnapPath, where}
   238  	cmd := exec.Command("mount", mountArgs...)
   239  	if out, err := cmd.CombinedOutput(); err != nil {
   240  		removeMountpoint()
   241  		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)
   242  	}
   243  
   244  	unmount := func() {
   245  		fmt.Fprintf(Stdout, "unmounting: %s\n", snapdMountPath)
   246  		cmd := exec.Command("umount", snapdMountPath)
   247  		if err := cmd.Run(); err != nil {
   248  			fmt.Fprintf(Stderr, "%v", err)
   249  		}
   250  	}
   251  
   252  	targetSnapd, err := chooseTargetSnapdVersion()
   253  	if err != nil {
   254  		unmount()
   255  		removeMountpoint()
   256  		return nil, nil, err
   257  	}
   258  
   259  	return targetSnapd, func() {
   260  		unmount()
   261  		removeMountpoint()
   262  	}, nil
   263  }
   264  
   265  // runPreseedMode runs snapd in a preseed mode. It assumes running in a chroot.
   266  // The chroot is expected to be set-up and ready to use (critical system directories mounted).
   267  func runPreseedMode(preseedChroot string, targetSnapd *targetSnapdInfo) error {
   268  	// run snapd in preseed mode
   269  	cmd := exec.Command(targetSnapd.path)
   270  	cmd.Env = os.Environ()
   271  	cmd.Env = append(cmd.Env, "SNAPD_PRESEED=1")
   272  	cmd.Stderr = Stderr
   273  	cmd.Stdout = Stdout
   274  
   275  	// note, snapdPath is relative to preseedChroot
   276  	fmt.Fprintf(Stdout, "starting to preseed root: %s\nusing snapd binary: %s (%s)\n", preseedChroot, targetSnapd.path, targetSnapd.version)
   277  
   278  	if err := cmd.Run(); err != nil {
   279  		return fmt.Errorf("error running snapd in preseed mode: %v\n", err)
   280  	}
   281  
   282  	return nil
   283  }