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