github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/cmd/snap/cmd_auto_import.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-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 "bufio" 24 "crypto" 25 "encoding/base64" 26 "fmt" 27 "io/ioutil" 28 "os" 29 "os/exec" 30 "path/filepath" 31 "sort" 32 "strings" 33 "syscall" 34 35 "github.com/jessevdk/go-flags" 36 37 "github.com/snapcore/snapd/boot" 38 "github.com/snapcore/snapd/client" 39 "github.com/snapcore/snapd/dirs" 40 "github.com/snapcore/snapd/i18n" 41 "github.com/snapcore/snapd/logger" 42 "github.com/snapcore/snapd/osutil" 43 "github.com/snapcore/snapd/release" 44 "github.com/snapcore/snapd/snapdenv" 45 ) 46 47 const autoImportsName = "auto-import.assert" 48 49 var mountInfoPath = "/proc/self/mountinfo" 50 51 func autoImportCandidates() ([]string, error) { 52 var cands []string 53 54 // see https://www.kernel.org/doc/Documentation/filesystems/proc.txt, 55 // sec. 3.5 56 f, err := os.Open(mountInfoPath) 57 if err != nil { 58 return nil, err 59 } 60 defer f.Close() 61 62 isTesting := snapdenv.Testing() 63 64 // TODO: re-write this to use osutil.LoadMountInfo instead of doing the 65 // parsing ourselves 66 67 scanner := bufio.NewScanner(f) 68 for scanner.Scan() { 69 l := strings.Fields(scanner.Text()) 70 71 // Per proc.txt:3.5, /proc/<pid>/mountinfo looks like 72 // 73 // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue 74 // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) 75 // 76 // and (7) has zero or more elements, find the "-" separator. 77 i := 6 78 for i < len(l) && l[i] != "-" { 79 i++ 80 } 81 if i+2 >= len(l) { 82 continue 83 } 84 85 mountSrc := l[i+2] 86 87 // skip everything that is not a device (cgroups, debugfs etc) 88 if !strings.HasPrefix(mountSrc, "/dev/") { 89 continue 90 } 91 // skip all loop devices (snaps) 92 if strings.HasPrefix(mountSrc, "/dev/loop") { 93 continue 94 } 95 // skip all ram disks (unless in tests) 96 if !isTesting && strings.HasPrefix(mountSrc, "/dev/ram") { 97 continue 98 } 99 100 // TODO: should the following 2 checks try to be more smart like 101 // `snap-bootstrap initramfs-mounts` and try to find the boot disk 102 // and determine what partitions to skip using the disks package? 103 104 // skip all initramfs mounted disks on uc20 105 mountPoint := l[4] 106 if strings.HasPrefix(mountPoint, boot.InitramfsRunMntDir) { 107 continue 108 } 109 110 // skip all seed dir mount points too, as these are bind mounts to the 111 // initramfs dirs on uc20, this can show up as 112 // /writable/system-data/var/lib/snapd/seed as well as 113 // /var/lib/snapd/seed 114 if strings.HasSuffix(mountPoint, dirs.SnapSeedDir) { 115 continue 116 } 117 118 cand := filepath.Join(mountPoint, autoImportsName) 119 if osutil.FileExists(cand) { 120 cands = append(cands, cand) 121 } 122 } 123 124 return cands, scanner.Err() 125 } 126 127 func queueFile(src string) error { 128 // refuse huge files, this is for assertions 129 fi, err := os.Stat(src) 130 if err != nil { 131 return err 132 } 133 // 640kb ought be to enough for anyone 134 if fi.Size() > 640*1024 { 135 msg := fmt.Errorf("cannot queue %s, file size too big: %v", src, fi.Size()) 136 logger.Noticef("error: %v", msg) 137 return msg 138 } 139 140 // ensure name is predictable, weak hash is ok 141 hash, _, err := osutil.FileDigest(src, crypto.SHA3_384) 142 if err != nil { 143 return err 144 } 145 146 dst := filepath.Join(dirs.SnapAssertsSpoolDir, fmt.Sprintf("%s.assert", base64.URLEncoding.EncodeToString(hash))) 147 if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { 148 return err 149 } 150 151 return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite) 152 } 153 154 func autoImportFromSpool(cli *client.Client) (added int, err error) { 155 files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) 156 if os.IsNotExist(err) { 157 return 0, nil 158 } 159 if err != nil { 160 return 0, err 161 } 162 163 for _, fi := range files { 164 cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name()) 165 if err := ackFile(cli, cand); err != nil { 166 logger.Noticef("error: cannot import %s: %s", cand, err) 167 continue 168 } else { 169 logger.Noticef("imported %s", cand) 170 added++ 171 } 172 // FIXME: only remove stuff older than N days? 173 if err := os.Remove(cand); err != nil { 174 return 0, err 175 } 176 } 177 178 return added, nil 179 } 180 181 func autoImportFromAllMounts(cli *client.Client) (int, error) { 182 cands, err := autoImportCandidates() 183 if err != nil { 184 return 0, err 185 } 186 187 added := 0 188 for _, cand := range cands { 189 err := ackFile(cli, cand) 190 // the server is not ready yet 191 if _, ok := err.(client.ConnectionError); ok { 192 logger.Noticef("queuing for later %s", cand) 193 if err := queueFile(cand); err != nil { 194 return 0, err 195 } 196 continue 197 } 198 if err != nil { 199 logger.Noticef("error: cannot import %s: %s", cand, err) 200 continue 201 } else { 202 logger.Noticef("imported %s", cand) 203 } 204 added++ 205 } 206 207 return added, nil 208 } 209 210 var ioutilTempDir = ioutil.TempDir 211 212 func tryMount(deviceName string) (string, error) { 213 tmpMountTarget, err := ioutilTempDir("", "snapd-auto-import-mount-") 214 if err != nil { 215 err = fmt.Errorf("cannot create temporary mount point: %v", err) 216 logger.Noticef("error: %v", err) 217 return "", err 218 } 219 // udev does not provide much environment ;) 220 if os.Getenv("PATH") == "" { 221 os.Setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin") 222 } 223 // not using syscall.Mount() because we don't know the fs type in advance 224 cmd := exec.Command("mount", "-t", "ext4,vfat", "-o", "ro", "--make-private", deviceName, tmpMountTarget) 225 if output, err := cmd.CombinedOutput(); err != nil { 226 os.Remove(tmpMountTarget) 227 err = fmt.Errorf("cannot mount %s: %s", deviceName, osutil.OutputErr(output, err)) 228 logger.Noticef("error: %v", err) 229 return "", err 230 } 231 232 return tmpMountTarget, nil 233 } 234 235 var syscallUnmount = syscall.Unmount 236 237 func doUmount(mp string) error { 238 if err := syscallUnmount(mp, 0); err != nil { 239 return err 240 } 241 return os.Remove(mp) 242 } 243 244 type cmdAutoImport struct { 245 clientMixin 246 Mount []string `long:"mount" arg-name:"<device path>"` 247 248 ForceClassic bool `long:"force-classic"` 249 } 250 251 var shortAutoImportHelp = i18n.G("Inspect devices for actionable information") 252 253 var longAutoImportHelp = i18n.G(` 254 The auto-import command searches available mounted devices looking for 255 assertions that are signed by trusted authorities, and potentially 256 performs system changes based on them. 257 258 If one or more device paths are provided via --mount, these are temporarily 259 mounted to be inspected as well. Even in that case the command will still 260 consider all available mounted devices for inspection. 261 262 Assertions to be imported must be made available in the auto-import.assert file 263 in the root of the filesystem. 264 `) 265 266 func init() { 267 cmd := addCommand("auto-import", 268 shortAutoImportHelp, 269 longAutoImportHelp, 270 func() flags.Commander { 271 return &cmdAutoImport{} 272 }, map[string]string{ 273 // TRANSLATORS: This should not start with a lowercase letter. 274 "mount": i18n.G("Temporarily mount device before inspecting"), 275 // TRANSLATORS: This should not start with a lowercase letter. 276 "force-classic": i18n.G("Force import on classic systems"), 277 }, nil) 278 cmd.hidden = true 279 } 280 281 func (x *cmdAutoImport) autoAddUsers() error { 282 options := client.CreateUserOptions{ 283 Automatic: true, 284 } 285 results, err := x.client.CreateUsers([]*client.CreateUserOptions{&options}) 286 for _, result := range results { 287 fmt.Fprintf(Stdout, i18n.G("created user %q\n"), result.Username) 288 } 289 290 return err 291 } 292 293 func removableBlockDevices() (removableDevices []string) { 294 // eg. /sys/block/sda/removable 295 removable, err := filepath.Glob(filepath.Join(dirs.GlobalRootDir, "/sys/block/*/removable")) 296 if err != nil { 297 return nil 298 } 299 for _, removableAttr := range removable { 300 val, err := ioutil.ReadFile(removableAttr) 301 if err != nil || string(val) != "1\n" { 302 // non removable 303 continue 304 } 305 // let's see if it has partitions 306 dev := filepath.Base(filepath.Dir(removableAttr)) 307 308 pattern := fmt.Sprintf(filepath.Join(dirs.GlobalRootDir, "/sys/block/%s/%s*/partition"), dev, dev) 309 // eg. /sys/block/sda/sda1/partition 310 partitionAttrs, _ := filepath.Glob(pattern) 311 312 if len(partitionAttrs) == 0 { 313 // not partitioned? try to use the main device 314 removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", dev)) 315 continue 316 } 317 318 for _, partAttr := range partitionAttrs { 319 val, err := ioutil.ReadFile(partAttr) 320 if err != nil || string(val) != "1\n" { 321 // non partition? 322 continue 323 } 324 pdev := filepath.Base(filepath.Dir(partAttr)) 325 removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", pdev)) 326 // hasPartitions = true 327 } 328 } 329 sort.Strings(removableDevices) 330 return removableDevices 331 } 332 333 // inInstallmode returns true if it's UC20 system in install mode 334 func inInstallMode() bool { 335 mode, _, err := boot.ModeAndRecoverySystemFromKernelCommandLine() 336 if err != nil { 337 return false 338 } 339 return mode == "install" 340 } 341 342 func (x *cmdAutoImport) Execute(args []string) error { 343 if len(args) > 0 { 344 return ErrExtraArgs 345 } 346 347 if release.OnClassic && !x.ForceClassic { 348 fmt.Fprintf(Stderr, "auto-import is disabled on classic\n") 349 return nil 350 } 351 // TODO:UC20: workaround for LP: #1860231 352 if inInstallMode() { 353 fmt.Fprintf(Stderr, "auto-import is disabled in install-mode\n") 354 return nil 355 } 356 357 devices := x.Mount 358 if len(devices) == 0 { 359 // coldplug scenario, try all removable devices 360 devices = removableBlockDevices() 361 } 362 363 for _, path := range devices { 364 // udev adds new /dev/loopX devices on the fly when a 365 // loop mount happens and there is no loop device left. 366 // 367 // We need to ignore these events because otherwise both 368 // our mount and the "mount -o loop" fight over the same 369 // device and we get nasty errors 370 if strings.HasPrefix(path, "/dev/loop") { 371 continue 372 } 373 374 mp, err := tryMount(path) 375 if err != nil { 376 continue // Error was reported. Continue looking. 377 } 378 defer doUmount(mp) 379 } 380 381 added1, err := autoImportFromSpool(x.client) 382 if err != nil { 383 return err 384 } 385 386 added2, err := autoImportFromAllMounts(x.client) 387 if err != nil { 388 return err 389 } 390 391 if added1+added2 > 0 { 392 return x.autoAddUsers() 393 } 394 395 return nil 396 }