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 }