forked from snapcore/snapd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
install.go
342 lines (310 loc) · 11.9 KB
/
install.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
// -*- Mode: Go; indent-tabs-mode: t -*-
// +build !nosecboot
/*
* Copyright (C) 2019-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 install
import (
"fmt"
"os"
"path/filepath"
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/timings"
)
const (
ubuntuDataLabel = "ubuntu-data"
ubuntuSaveLabel = "ubuntu-save"
)
func deviceFromRole(lv *gadget.LaidOutVolume, role string) (device string, err error) {
for _, vs := range lv.LaidOutStructure {
// XXX: this part of the finding maybe should be a
// method on gadget.*Volume
if vs.Role == role {
device, err = gadget.FindDeviceForStructure(&vs)
if err != nil {
return "", fmt.Errorf("cannot find device for role %q: %v", role, err)
}
return gadget.ParentDiskFromMountSource(device)
}
}
return "", fmt.Errorf("cannot find role %s in gadget", role)
}
func roleOrLabelOrName(part gadget.OnDiskStructure) string {
switch {
case part.Role != "":
return part.Role
case part.Label != "":
return part.Label
case part.Name != "":
return part.Name
default:
return "unknown"
}
}
// Run bootstraps the partitions of a device, by either creating
// missing ones or recreating installed ones.
func Run(model gadget.Model, gadgetRoot, kernelRoot, device string, options Options, observer gadget.ContentObserver, perfTimings timings.Measurer) (*InstalledSystemSideData, error) {
logger.Noticef("installing a new system")
logger.Noticef(" gadget data from: %v", gadgetRoot)
if options.Encrypt {
logger.Noticef(" encryption: on")
}
if gadgetRoot == "" {
return nil, fmt.Errorf("cannot use empty gadget root directory")
}
lv, err := gadget.LaidOutSystemVolumeFromGadget(gadgetRoot, kernelRoot, model)
if err != nil {
return nil, fmt.Errorf("cannot layout the volume: %v", err)
}
// TODO: resolve content paths from gadget here
// XXX: the only situation where auto-detect is not desired is
// in (spread) testing - consider to remove forcing a device
//
// auto-detect device if no device is forced
if device == "" {
device, err = deviceFromRole(lv, gadget.SystemSeed)
if err != nil {
return nil, fmt.Errorf("cannot find device to create partitions on: %v", err)
}
}
diskLayout, err := gadget.OnDiskVolumeFromDevice(device)
if err != nil {
return nil, fmt.Errorf("cannot read %v partitions: %v", device, err)
}
// check if the current partition table is compatible with the gadget,
// ignoring partitions added by the installer (will be removed later)
if err := ensureLayoutCompatibility(lv, diskLayout); err != nil {
return nil, fmt.Errorf("gadget and %v partition table not compatible: %v", device, err)
}
// remove partitions added during a previous install attempt
if err := removeCreatedPartitions(lv, diskLayout); err != nil {
return nil, fmt.Errorf("cannot remove partitions from previous install: %v", err)
}
// at this point we removed any existing partition, nuke any
// of the existing sealed key files placed outside of the
// encrypted partitions (LP: #1879338)
sealedKeyFiles, _ := filepath.Glob(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "*.sealed-key"))
for _, keyFile := range sealedKeyFiles {
if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("cannot cleanup obsolete key file: %v", keyFile)
}
}
var created []gadget.OnDiskStructure
timings.Run(perfTimings, "create-partitions", "Create partitions", func(timings.Measurer) {
created, err = createMissingPartitions(diskLayout, lv)
})
if err != nil {
return nil, fmt.Errorf("cannot create the partitions: %v", err)
}
makeKeySet := func() (*EncryptionKeySet, error) {
key, err := secboot.NewEncryptionKey()
if err != nil {
return nil, fmt.Errorf("cannot create encryption key: %v", err)
}
rkey, err := secboot.NewRecoveryKey()
if err != nil {
return nil, fmt.Errorf("cannot create recovery key: %v", err)
}
return &EncryptionKeySet{
Key: key,
RecoveryKey: rkey,
}, nil
}
roleNeedsEncryption := func(role string) bool {
return role == gadget.SystemData || role == gadget.SystemSave
}
var keysForRoles map[string]*EncryptionKeySet
for _, part := range created {
roleFmt := ""
if part.Role != "" {
roleFmt = fmt.Sprintf("role %v", part.Role)
}
logger.Noticef("created new partition %v for structure %v (size %v) %s",
part.Node, part, part.Size.IECString(), roleFmt)
if options.Encrypt && roleNeedsEncryption(part.Role) {
var keys *EncryptionKeySet
timings.Run(perfTimings, fmt.Sprintf("make-key-set[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create encryption key set for %s", roleOrLabelOrName(part)), func(timings.Measurer) {
keys, err = makeKeySet()
})
if err != nil {
return nil, err
}
logger.Noticef("encrypting partition device %v", part.Node)
var dataPart *encryptedDevice
timings.Run(perfTimings, fmt.Sprintf("new-encrypted-device[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create encryption device for %s", roleOrLabelOrName(part)), func(timings.Measurer) {
dataPart, err = newEncryptedDevice(&part, keys.Key, part.Label)
})
if err != nil {
return nil, err
}
timings.Run(perfTimings, fmt.Sprintf("add-recovery-key[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Adding recovery key for %s", roleOrLabelOrName(part)), func(timings.Measurer) {
err = dataPart.AddRecoveryKey(keys.Key, keys.RecoveryKey)
})
if err != nil {
return nil, err
}
// update the encrypted device node
part.Node = dataPart.Node
if keysForRoles == nil {
keysForRoles = map[string]*EncryptionKeySet{}
}
keysForRoles[part.Role] = keys
logger.Noticef("encrypted device %v", part.Node)
}
// use the diskLayout.SectorSize here instead of lv.SectorSize, we check
// that if there is a sector-size specified in the gadget that it
// matches what is on the disk, but sometimes there may not be a sector
// size specified in the gadget.yaml, but we will always have the sector
// size from the physical disk device
timings.Run(perfTimings, fmt.Sprintf("make-filesystem[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create filesystem for %s", part.Node), func(timings.Measurer) {
err = makeFilesystem(&part, diskLayout.SectorSize)
})
if err != nil {
return nil, fmt.Errorf("cannot make filesystem for partition %s: %v", roleOrLabelOrName(part), err)
}
timings.Run(perfTimings, fmt.Sprintf("write-content[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Write content for %s", roleOrLabelOrName(part)), func(timings.Measurer) {
err = writeContent(&part, gadgetRoot, observer)
})
if err != nil {
return nil, err
}
if options.Mount && part.Label != "" && part.HasFilesystem() {
if err := mountFilesystem(&part, boot.InitramfsRunMntDir); err != nil {
return nil, err
}
}
}
return &InstalledSystemSideData{
KeysForRoles: keysForRoles,
}, nil
}
// isCreatableAtInstall returns whether the gadget structure would be created at
// install - currently that is only ubuntu-save, ubuntu-data, and ubuntu-boot
func isCreatableAtInstall(gv *gadget.VolumeStructure) bool {
// a structure is creatable at install if it is one of the roles for
// system-save, system-data, or system-boot
switch gv.Role {
case gadget.SystemSave, gadget.SystemData, gadget.SystemBoot:
return true
default:
return false
}
}
func ensureLayoutCompatibility(gadgetLayout *gadget.LaidOutVolume, diskLayout *gadget.OnDiskVolume) error {
eq := func(ds gadget.OnDiskStructure, gs gadget.LaidOutStructure) (bool, string) {
dv := ds.VolumeStructure
gv := gs.VolumeStructure
nameMatch := gv.Name == dv.Name
if gadgetLayout.Schema == "mbr" {
// partitions have no names in MBR so bypass the name check
nameMatch = true
}
// Previous installation may have failed before filesystem creation or
// partition may be encrypted, so if the on disk offset matches the
// gadget offset, and the gadget structure is creatable during install,
// then they are equal
// otherwise, if they are not created during installation, the
// filesystem must be the same
check := nameMatch && ds.StartOffset == gs.StartOffset && (isCreatableAtInstall(gv) || dv.Filesystem == gv.Filesystem)
sizeMatches := dv.Size == gv.Size
if gv.Role == gadget.SystemData {
// system-data may have been expanded
sizeMatches = dv.Size >= gv.Size
}
if check && sizeMatches {
return true, ""
}
switch {
case !nameMatch:
// don't return a reason if the names don't match
return false, ""
case ds.StartOffset != gs.StartOffset:
return false, fmt.Sprintf("start offsets do not match (disk: %d (%s) and gadget: %d (%s))", ds.StartOffset, ds.StartOffset.IECString(), gs.StartOffset, gs.StartOffset.IECString())
case !isCreatableAtInstall(gv) && dv.Filesystem != gv.Filesystem:
return false, "filesystems do not match and the partition is not creatable at install"
case dv.Size < gv.Size:
return false, "on disk size is smaller than gadget size"
case gv.Role != gadget.SystemData && dv.Size > gv.Size:
return false, "on disk size is larger than gadget size (and the role should not be expanded)"
default:
return false, "some other logic condition (should be impossible?)"
}
}
contains := func(haystack []gadget.LaidOutStructure, needle gadget.OnDiskStructure) (bool, string) {
reasonAbsent := ""
for _, h := range haystack {
matches, reasonNotMatches := eq(needle, h)
if matches {
return true, ""
}
// this has the effect of only returning the last non-empty reason
// string
if reasonNotMatches != "" {
reasonAbsent = reasonNotMatches
}
}
return false, reasonAbsent
}
// check size of volumes
if gadgetLayout.Size > diskLayout.Size {
return fmt.Errorf("device %v (%s) is too small to fit the requested layout (%s)", diskLayout.Device,
diskLayout.Size.IECString(), gadgetLayout.Size.IECString())
}
// check that the sizes of all structures in the gadget are multiples of
// the disk sector size (unless the structure is the MBR)
for _, ls := range gadgetLayout.LaidOutStructure {
if !gadget.IsRoleMBR(ls) {
if ls.Size%diskLayout.SectorSize != 0 {
return fmt.Errorf("gadget volume structure %v size is not a multiple of disk sector size %v",
ls, diskLayout.SectorSize)
}
}
}
// Check if top level properties match
if !isCompatibleSchema(gadgetLayout.Volume.Schema, diskLayout.Schema) {
return fmt.Errorf("disk partitioning schema %q doesn't match gadget schema %q", diskLayout.Schema, gadgetLayout.Volume.Schema)
}
if gadgetLayout.Volume.ID != "" && gadgetLayout.Volume.ID != diskLayout.ID {
return fmt.Errorf("disk ID %q doesn't match gadget volume ID %q", diskLayout.ID, gadgetLayout.Volume.ID)
}
// Check if all existing device partitions are also in gadget
for _, ds := range diskLayout.Structure {
present, reasonAbsent := contains(gadgetLayout.LaidOutStructure, ds)
if !present {
if reasonAbsent != "" {
// use the right format so that it can be
// appended to the error message
reasonAbsent = fmt.Sprintf(": %s", reasonAbsent)
}
return fmt.Errorf("cannot find disk partition %s (starting at %d) in gadget%s", ds.Node, ds.StartOffset, reasonAbsent)
}
}
return nil
}
func isCompatibleSchema(gadgetSchema, diskSchema string) bool {
switch gadgetSchema {
// XXX: "mbr,gpt" is currently unsupported
case "", "gpt":
return diskSchema == "gpt"
case "mbr":
return diskSchema == "dos"
default:
return false
}
}