github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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 mountArgs := []string{"-t", fstype, "-o", strings.Join(fsopts, ","), coreSnapPath, where} 239 cmd := exec.Command("mount", mountArgs...) 240 if out, err := cmd.CombinedOutput(); err != nil { 241 removeMountpoint() 242 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) 243 } 244 245 unmount := func() { 246 fmt.Fprintf(Stdout, "unmounting: %s\n", snapdMountPath) 247 cmd := exec.Command("umount", snapdMountPath) 248 if err := cmd.Run(); err != nil { 249 fmt.Fprintf(Stderr, "%v", err) 250 } 251 } 252 253 targetSnapd, err := chooseTargetSnapdVersion() 254 if err != nil { 255 unmount() 256 removeMountpoint() 257 return nil, nil, err 258 } 259 260 return targetSnapd, func() { 261 unmount() 262 removeMountpoint() 263 }, nil 264 } 265 266 // runPreseedMode runs snapd in a preseed mode. It assumes running in a chroot. 267 // The chroot is expected to be set-up and ready to use (critical system directories mounted). 268 func runPreseedMode(preseedChroot string, targetSnapd *targetSnapdInfo) error { 269 // run snapd in preseed mode 270 cmd := exec.Command(targetSnapd.path) 271 cmd.Env = os.Environ() 272 cmd.Env = append(cmd.Env, "SNAPD_PRESEED=1") 273 cmd.Stderr = Stderr 274 cmd.Stdout = Stdout 275 276 // note, snapdPath is relative to preseedChroot 277 fmt.Fprintf(Stdout, "starting to preseed root: %s\nusing snapd binary: %s (%s)\n", preseedChroot, targetSnapd.path, targetSnapd.version) 278 279 if err := cmd.Run(); err != nil { 280 return fmt.Errorf("error running snapd in preseed mode: %v\n", err) 281 } 282 283 return nil 284 }