github.com/rigado/snapd@v2.42.5-go-mod+incompatible/cmd/snap/cmd_auto_import.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2016 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 "strings" 32 "syscall" 33 34 "github.com/jessevdk/go-flags" 35 36 "github.com/snapcore/snapd/client" 37 "github.com/snapcore/snapd/dirs" 38 "github.com/snapcore/snapd/i18n" 39 "github.com/snapcore/snapd/logger" 40 "github.com/snapcore/snapd/osutil" 41 "github.com/snapcore/snapd/release" 42 ) 43 44 const autoImportsName = "auto-import.assert" 45 46 var mountInfoPath = "/proc/self/mountinfo" 47 48 func autoImportCandidates() ([]string, error) { 49 var cands []string 50 51 // see https://www.kernel.org/doc/Documentation/filesystems/proc.txt, 52 // sec. 3.5 53 f, err := os.Open(mountInfoPath) 54 if err != nil { 55 return nil, err 56 } 57 defer f.Close() 58 59 scanner := bufio.NewScanner(f) 60 for scanner.Scan() { 61 l := strings.Fields(scanner.Text()) 62 63 // Per proc.txt:3.5, /proc/<pid>/mountinfo looks like 64 // 65 // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue 66 // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11) 67 // 68 // and (7) has zero or more elements, find the "-" separator. 69 i := 6 70 for i < len(l) && l[i] != "-" { 71 i++ 72 } 73 if i+2 >= len(l) { 74 continue 75 } 76 77 mountSrc := l[i+2] 78 79 // skip everything that is not a device (cgroups, debugfs etc) 80 if !strings.HasPrefix(mountSrc, "/dev/") { 81 continue 82 } 83 // skip all loop devices (snaps) 84 if strings.HasPrefix(mountSrc, "/dev/loop") { 85 continue 86 } 87 // skip all ram disks (unless in tests) 88 if !osutil.GetenvBool("SNAPPY_TESTING") && strings.HasPrefix(mountSrc, "/dev/ram") { 89 continue 90 } 91 92 mountPoint := l[4] 93 cand := filepath.Join(mountPoint, autoImportsName) 94 if osutil.FileExists(cand) { 95 cands = append(cands, cand) 96 } 97 } 98 99 return cands, scanner.Err() 100 101 } 102 103 func queueFile(src string) error { 104 // refuse huge files, this is for assertions 105 fi, err := os.Stat(src) 106 if err != nil { 107 return err 108 } 109 // 640kb ought be to enough for anyone 110 if fi.Size() > 640*1024 { 111 msg := fmt.Errorf("cannot queue %s, file size too big: %v", src, fi.Size()) 112 logger.Noticef("error: %v", msg) 113 return msg 114 } 115 116 // ensure name is predictable, weak hash is ok 117 hash, _, err := osutil.FileDigest(src, crypto.SHA3_384) 118 if err != nil { 119 return err 120 } 121 122 dst := filepath.Join(dirs.SnapAssertsSpoolDir, fmt.Sprintf("%s.assert", base64.URLEncoding.EncodeToString(hash))) 123 if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { 124 return err 125 } 126 127 return osutil.CopyFile(src, dst, osutil.CopyFlagOverwrite) 128 } 129 130 func autoImportFromSpool(cli *client.Client) (added int, err error) { 131 files, err := ioutil.ReadDir(dirs.SnapAssertsSpoolDir) 132 if os.IsNotExist(err) { 133 return 0, nil 134 } 135 if err != nil { 136 return 0, err 137 } 138 139 for _, fi := range files { 140 cand := filepath.Join(dirs.SnapAssertsSpoolDir, fi.Name()) 141 if err := ackFile(cli, cand); err != nil { 142 logger.Noticef("error: cannot import %s: %s", cand, err) 143 continue 144 } else { 145 logger.Noticef("imported %s", cand) 146 added++ 147 } 148 // FIXME: only remove stuff older than N days? 149 if err := os.Remove(cand); err != nil { 150 return 0, err 151 } 152 } 153 154 return added, nil 155 } 156 157 func autoImportFromAllMounts(cli *client.Client) (int, error) { 158 cands, err := autoImportCandidates() 159 if err != nil { 160 return 0, err 161 } 162 163 added := 0 164 for _, cand := range cands { 165 err := ackFile(cli, cand) 166 // the server is not ready yet 167 if _, ok := err.(client.ConnectionError); ok { 168 logger.Noticef("queuing for later %s", cand) 169 if err := queueFile(cand); err != nil { 170 return 0, err 171 } 172 continue 173 } 174 if err != nil { 175 logger.Noticef("error: cannot import %s: %s", cand, err) 176 continue 177 } else { 178 logger.Noticef("imported %s", cand) 179 } 180 added++ 181 } 182 183 return added, nil 184 } 185 186 func tryMount(deviceName string) (string, error) { 187 tmpMountTarget, err := ioutil.TempDir("", "snapd-auto-import-mount-") 188 if err != nil { 189 err = fmt.Errorf("cannot create temporary mount point: %v", err) 190 logger.Noticef("error: %v", err) 191 return "", err 192 } 193 // udev does not provide much environment ;) 194 if os.Getenv("PATH") == "" { 195 os.Setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin") 196 } 197 // not using syscall.Mount() because we don't know the fs type in advance 198 cmd := exec.Command("mount", "-t", "ext4,vfat", "-o", "ro", "--make-private", deviceName, tmpMountTarget) 199 if output, err := cmd.CombinedOutput(); err != nil { 200 os.Remove(tmpMountTarget) 201 err = fmt.Errorf("cannot mount %s: %s", deviceName, osutil.OutputErr(output, err)) 202 logger.Noticef("error: %v", err) 203 return "", err 204 } 205 206 return tmpMountTarget, nil 207 } 208 209 func doUmount(mp string) error { 210 if err := syscall.Unmount(mp, 0); err != nil { 211 return err 212 } 213 return os.Remove(mp) 214 } 215 216 type cmdAutoImport struct { 217 clientMixin 218 Mount []string `long:"mount" arg-name:"<device path>"` 219 220 ForceClassic bool `long:"force-classic"` 221 } 222 223 var shortAutoImportHelp = i18n.G("Inspect devices for actionable information") 224 225 var longAutoImportHelp = i18n.G(` 226 The auto-import command searches available mounted devices looking for 227 assertions that are signed by trusted authorities, and potentially 228 performs system changes based on them. 229 230 If one or more device paths are provided via --mount, these are temporarily 231 mounted to be inspected as well. Even in that case the command will still 232 consider all available mounted devices for inspection. 233 234 Assertions to be imported must be made available in the auto-import.assert file 235 in the root of the filesystem. 236 `) 237 238 func init() { 239 cmd := addCommand("auto-import", 240 shortAutoImportHelp, 241 longAutoImportHelp, 242 func() flags.Commander { 243 return &cmdAutoImport{} 244 }, map[string]string{ 245 // TRANSLATORS: This should not start with a lowercase letter. 246 "mount": i18n.G("Temporarily mount device before inspecting"), 247 // TRANSLATORS: This should not start with a lowercase letter. 248 "force-classic": i18n.G("Force import on classic systems"), 249 }, nil) 250 cmd.hidden = true 251 } 252 253 func (x *cmdAutoImport) autoAddUsers() error { 254 cmd := cmdCreateUser{ 255 clientMixin: x.clientMixin, 256 Known: true, 257 Sudoer: true, 258 } 259 return cmd.Execute(nil) 260 } 261 262 func (x *cmdAutoImport) Execute(args []string) error { 263 if len(args) > 0 { 264 return ErrExtraArgs 265 } 266 267 if release.OnClassic && !x.ForceClassic { 268 fmt.Fprintf(Stderr, "auto-import is disabled on classic\n") 269 return nil 270 } 271 272 for _, path := range x.Mount { 273 // udev adds new /dev/loopX devices on the fly when a 274 // loop mount happens and there is no loop device left. 275 // 276 // We need to ignore these events because otherwise both 277 // our mount and the "mount -o loop" fight over the same 278 // device and we get nasty errors 279 if strings.HasPrefix(path, "/dev/loop") { 280 continue 281 } 282 283 mp, err := tryMount(path) 284 if err != nil { 285 continue // Error was reported. Continue looking. 286 } 287 defer doUmount(mp) 288 } 289 290 added1, err := autoImportFromSpool(x.client) 291 if err != nil { 292 return err 293 } 294 295 added2, err := autoImportFromAllMounts(x.client) 296 if err != nil { 297 return err 298 } 299 300 if added1+added2 > 0 { 301 return x.autoAddUsers() 302 } 303 304 return nil 305 }