github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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  	syscallMount   = syscall.Mount
    44  	syscallChroot  = syscall.Chroot
    45  )
    46  
    47  // checkChroot does a basic sanity check of the target chroot environment, e.g. makes
    48  // sure critical virtual filesystems (such as proc) are mounted. This is not meant to
    49  // be exhaustive check, but one that prevents running the tool against a wrong directory
    50  // by an accident, which would lead to hard to understand errors from snapd in preseed
    51  // mode.
    52  func checkChroot(preseedChroot string) error {
    53  	exists, isDir, err := osutil.DirExists(preseedChroot)
    54  	if err != nil {
    55  		return fmt.Errorf("cannot verify %q: %v", preseedChroot, err)
    56  	}
    57  	if !exists || !isDir {
    58  		return fmt.Errorf("cannot verify %q: is not a directory", preseedChroot)
    59  	}
    60  
    61  	if osutil.FileExists(filepath.Join(preseedChroot, dirs.SnapStateFile)) {
    62  		return fmt.Errorf("the system at %q appears to be preseeded, pass --reset flag to clean it up", preseedChroot)
    63  	}
    64  
    65  	// sanity checks of the critical mountpoints inside chroot directory.
    66  	required := map[string]bool{}
    67  	// required mountpoints are relative to the preseed chroot
    68  	for _, p := range []string{"/sys/kernel/security", "/proc", "/dev"} {
    69  		required[filepath.Join(preseedChroot, p)] = true
    70  	}
    71  	entries, err := osutil.LoadMountInfo()
    72  	if err != nil {
    73  		return fmt.Errorf("cannot parse mount info: %v", err)
    74  	}
    75  	for _, ent := range entries {
    76  		if _, ok := required[ent.MountDir]; ok {
    77  			delete(required, ent.MountDir)
    78  		}
    79  	}
    80  	// non empty required indicates missing mountpoint(s)
    81  	if len(required) > 0 {
    82  		var sorted []string
    83  		for path := range required {
    84  			sorted = append(sorted, path)
    85  		}
    86  		sort.Strings(sorted)
    87  		parts := append([]string{""}, sorted...)
    88  		return fmt.Errorf("cannot preseed without the following mountpoints:%s", strings.Join(parts, "\n - "))
    89  	}
    90  
    91  	path := filepath.Join(preseedChroot, "/sys/kernel/security/apparmor")
    92  	if exists := osutil.FileExists(path); !exists {
    93  		return fmt.Errorf("cannot preseed without access to %q", path)
    94  	}
    95  
    96  	return nil
    97  }
    98  
    99  var seedOpen = seed.Open
   100  
   101  var systemSnapFromSeed = func(rootDir string) (string, error) {
   102  	seedDir := filepath.Join(dirs.SnapSeedDirUnder(rootDir))
   103  	seed, err := seedOpen(seedDir, "")
   104  	if err != nil {
   105  		return "", err
   106  	}
   107  
   108  	// load assertions into temporary database
   109  	if err := seed.LoadAssertions(nil, nil); err != nil {
   110  		return "", err
   111  	}
   112  	model := seed.Model()
   113  
   114  	tm := timings.New(nil)
   115  	if err := seed.LoadMeta(tm); err != nil {
   116  		return "", err
   117  	}
   118  
   119  	// TODO: implement preseeding for core.
   120  	if !model.Classic() {
   121  		return "", fmt.Errorf("preseeding is only supported on classic systems")
   122  	}
   123  
   124  	var required string
   125  	if seed.UsesSnapdSnap() {
   126  		required = "snapd"
   127  	} else {
   128  		required = "core"
   129  	}
   130  
   131  	var snapPath string
   132  	ess := seed.EssentialSnaps()
   133  	if len(ess) > 0 {
   134  		// core / snapd snap is the first essential snap.
   135  		if ess[0].SnapName() == required {
   136  			snapPath = ess[0].Path
   137  		}
   138  	}
   139  
   140  	if snapPath == "" {
   141  		return "", fmt.Errorf("%s snap not found", required)
   142  	}
   143  
   144  	return snapPath, nil
   145  }
   146  
   147  const snapdPreseedSupportVer = `2.43.3+`
   148  
   149  type targetSnapdInfo struct {
   150  	path    string
   151  	version string
   152  }
   153  
   154  // chooseTargetSnapdVersion checks if the version of snapd under chroot env
   155  // is good enough for preseeding. It checks both the snapd from the deb
   156  // and from the seeded snap mounted under snapdMountPath and returns the
   157  // information (path, version) about snapd to execute as part of preseeding
   158  // (it picks the newer version of the two).
   159  // The function must be called after syscall.Chroot(..).
   160  func chooseTargetSnapdVersion() (*targetSnapdInfo, error) {
   161  	// read snapd version from the mounted core/snapd snap
   162  	infoPath := filepath.Join(snapdMountPath, dirs.CoreLibExecDir, "info")
   163  	verFromSnap, err := snapdtool.SnapdVersionFromInfoFile(infoPath)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	// read snapd version from the main fs under chroot (snapd from the deb);
   169  	// assumes running under chroot already.
   170  	infoPath = filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "info")
   171  	verFromDeb, err := snapdtool.SnapdVersionFromInfoFile(infoPath)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	res, err := strutil.VersionCompare(verFromSnap, verFromDeb)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	var whichVer, snapdPath string
   182  	if res < 0 {
   183  		// snapd from the deb under chroot is the candidate to run
   184  		whichVer = verFromDeb
   185  		snapdPath = filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "snapd")
   186  	} else {
   187  		// snapd from the mounted core/snapd snap is the candidate to run
   188  		whichVer = verFromSnap
   189  		snapdPath = filepath.Join(snapdMountPath, dirs.CoreLibExecDir, "snapd")
   190  	}
   191  
   192  	res, err = strutil.VersionCompare(whichVer, snapdPreseedSupportVer)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	if res < 0 {
   197  		return nil, fmt.Errorf("snapd %s from the target system does not support preseeding, the minimum required version is %s",
   198  			whichVer, snapdPreseedSupportVer)
   199  	}
   200  
   201  	return &targetSnapdInfo{path: snapdPath, version: whichVer}, nil
   202  }
   203  
   204  func prepareChroot(preseedChroot string) (*targetSnapdInfo, func(), error) {
   205  	if err := syscallChroot(preseedChroot); err != nil {
   206  		return nil, nil, fmt.Errorf("cannot chroot into %s: %v", preseedChroot, err)
   207  	}
   208  
   209  	if err := os.Chdir("/"); err != nil {
   210  		return nil, nil, fmt.Errorf("cannot chdir to /: %v", err)
   211  	}
   212  
   213  	// GlobalRootDir is now relative to chroot env. We assume all paths
   214  	// inside the chroot to be identical with the host.
   215  	rootDir := dirs.GlobalRootDir
   216  	if rootDir == "" {
   217  		rootDir = "/"
   218  	}
   219  
   220  	coreSnapPath, err := systemSnapFromSeed(rootDir)
   221  	if err != nil {
   222  		return nil, nil, err
   223  	}
   224  
   225  	// create mountpoint for core/snapd
   226  	where := filepath.Join(rootDir, snapdMountPath)
   227  	if err := os.MkdirAll(where, 0755); err != nil {
   228  		return nil, nil, err
   229  	}
   230  
   231  	removeMountpoint := func() {
   232  		if err := os.Remove(where); err != nil {
   233  			fmt.Fprintf(Stderr, "%v", err)
   234  		}
   235  	}
   236  
   237  	fstype, fsopts := squashfs.FsType()
   238  	cmd := exec.Command("mount", "-t", fstype, "-o", strings.Join(fsopts, ","), coreSnapPath, where)
   239  	if err := cmd.Run(); err != nil {
   240  		removeMountpoint()
   241  		return nil, nil, fmt.Errorf("cannot mount %s at %s in preseed mode: %v ", coreSnapPath, where, err)
   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  }