/
update.go
442 lines (392 loc) · 14 KB
/
update.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
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* 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 gadget
import (
"errors"
"fmt"
"github.com/snapcore/snapd/gadget/quantity"
"github.com/snapcore/snapd/logger"
)
var (
ErrNoUpdate = errors.New("nothing to update")
)
var (
// default positioning constraints that match ubuntu-image
defaultConstraints = LayoutConstraints{
NonMBRStartOffset: 1 * quantity.OffsetMiB,
SectorSize: 512,
}
)
// GadgetData holds references to a gadget revision metadata and its data directory.
type GadgetData struct {
// Info is the gadget metadata
Info *Info
// XXX: should be GadgetRootDir
// RootDir is the root directory of gadget snap data
RootDir string
// KernelRootDir is the root directory of kernel snap data
KernelRootDir string
}
// UpdatePolicyFunc is a callback that evaluates the provided pair of structures
// and returns true when the pair should be part of an update.
type UpdatePolicyFunc func(from, to *LaidOutStructure) bool
// ContentChange carries paths to files containing the content data being
// modified by the operation.
type ContentChange struct {
// Before is a path to a file containing the original data before the
// operation takes place (or took place in case of ContentRollback).
Before string
// After is a path to a file location of the data applied by the operation.
After string
}
type ContentOperation int
type ContentChangeAction int
const (
ContentWrite ContentOperation = iota
ContentUpdate
ContentRollback
ChangeAbort ContentChangeAction = iota
ChangeApply
ChangeIgnore
)
// ContentObserver allows for observing operations on the content of the gadget
// structures.
type ContentObserver interface {
// Observe is called to observe an pending or completed action, related
// to content being written, updated or being rolled back. In each of
// the scenarios, the target path is relative under the root.
//
// For a file write or update, the source path points to the content
// that will be written. When called during rollback, observe call
// happens after the original file has been restored (or removed if the
// file was added during the update), the source path is empty.
//
// Returning ChangeApply indicates that the observer agrees for a given
// change to be applied. When called with a ContentUpdate or
// ContentWrite operation, returning ChangeIgnore indicates that the
// change shall be ignored. ChangeAbort is expected to be returned along
// with a non-nil error.
Observe(op ContentOperation, sourceStruct *LaidOutStructure,
targetRootDir, relativeTargetPath string, dataChange *ContentChange) (ContentChangeAction, error)
}
// ContentUpdateObserver allows for observing update (and potentially a
// rollback) of the gadget structure content.
type ContentUpdateObserver interface {
ContentObserver
// BeforeWrite is called when the backups of content that will get
// modified during the update are complete and update is ready to be
// applied.
BeforeWrite() error
// Canceled is called when the update has been canceled, or if changes
// were written and the update has been reverted.
Canceled() error
}
// Update applies the gadget update given the gadget information and data from
// old and new revisions. It errors out when the update is not possible or
// illegal, or a failure occurs at any of the steps. When there is no update, a
// special error ErrNoUpdate is returned.
//
// Only structures selected by the update policy are part of the update. When
// the policy is nil, a default one is used. The default policy selects
// structures in an opt-in manner, only tructures with a higher value of Edition
// field in the new gadget definition are part of the update.
//
// Data that would be modified during the update is first backed up inside the
// rollback directory. Should the apply step fail, the modified data is
// recovered.
func Update(old, new GadgetData, rollbackDirPath string, updatePolicy UpdatePolicyFunc, observer ContentUpdateObserver) error {
// TODO: support multi-volume gadgets. But for now we simply
// do not do any gadget updates on those. We cannot error
// here because this would break refreshes of gadgets even
// when they don't require any updates.
if len(new.Info.Volumes) != 1 || len(old.Info.Volumes) != 1 {
logger.Noticef("WARNING: gadget assests cannot be updated yet when multiple volumes are used")
return nil
}
oldVol, newVol, err := resolveVolume(old.Info, new.Info)
if err != nil {
return err
}
if oldVol.Schema == "" || newVol.Schema == "" {
return fmt.Errorf("internal error: unset volume schemas: old: %q new: %q", oldVol.Schema, newVol.Schema)
}
// layout old partially, without going deep into the layout of structure
// content
pOld, err := LayoutVolumePartially(oldVol, defaultConstraints)
if err != nil {
return fmt.Errorf("cannot lay out the old volume: %v", err)
}
// layout new
pNew, err := LayoutVolume(new.RootDir, new.KernelRootDir, newVol, defaultConstraints)
if err != nil {
return fmt.Errorf("cannot lay out the new volume: %v", err)
}
if err := canUpdateVolume(pOld, pNew); err != nil {
return fmt.Errorf("cannot apply update to volume: %v", err)
}
if updatePolicy == nil {
updatePolicy = defaultPolicy
}
// now we know which structure is which, find which ones need an update
updates, err := resolveUpdate(pOld, pNew, updatePolicy)
if err != nil {
return err
}
if len(updates) == 0 {
// nothing to update
return ErrNoUpdate
}
// can update old layout to new layout
for _, update := range updates {
if err := canUpdateStructure(update.from, update.to, pNew.Schema); err != nil {
return fmt.Errorf("cannot update volume structure %v: %v", update.to, err)
}
}
return applyUpdates(new, updates, rollbackDirPath, observer)
}
func resolveVolume(old *Info, new *Info) (oldVol, newVol *Volume, err error) {
// support only one volume
if len(new.Volumes) != 1 || len(old.Volumes) != 1 {
return nil, nil, errors.New("cannot update with more than one volume")
}
var name string
for n := range old.Volumes {
name = n
break
}
oldV := old.Volumes[name]
newV, ok := new.Volumes[name]
if !ok {
return nil, nil, fmt.Errorf("cannot find entry for volume %q in updated gadget info", name)
}
return oldV, newV, nil
}
func isSameOffset(one *quantity.Offset, two *quantity.Offset) bool {
if one == nil && two == nil {
return true
}
if one != nil && two != nil {
return *one == *two
}
return false
}
func isSameRelativeOffset(one *RelativeOffset, two *RelativeOffset) bool {
if one == nil && two == nil {
return true
}
if one != nil && two != nil {
return *one == *two
}
return false
}
func isLegacyMBRTransition(from *LaidOutStructure, to *LaidOutStructure) bool {
// legacy MBR could have been specified by setting type: mbr, with no
// role
return from.Type == schemaMBR && to.Role == schemaMBR
}
func canUpdateStructure(from *LaidOutStructure, to *LaidOutStructure, schema string) error {
if schema == schemaGPT && from.Name != to.Name {
// partition names are only effective when GPT is used
return fmt.Errorf("cannot change structure name from %q to %q", from.Name, to.Name)
}
if from.Size != to.Size {
return fmt.Errorf("cannot change structure size from %v to %v", from.Size, to.Size)
}
if !isSameOffset(from.Offset, to.Offset) {
return fmt.Errorf("cannot change structure offset from %v to %v", from.Offset, to.Offset)
}
if from.StartOffset != to.StartOffset {
return fmt.Errorf("cannot change structure start offset from %v to %v", from.StartOffset, to.StartOffset)
}
// TODO: should this limitation be lifted?
if !isSameRelativeOffset(from.OffsetWrite, to.OffsetWrite) {
return fmt.Errorf("cannot change structure offset-write from %v to %v", from.OffsetWrite, to.OffsetWrite)
}
if from.Role != to.Role {
return fmt.Errorf("cannot change structure role from %q to %q", from.Role, to.Role)
}
if from.Type != to.Type {
if !isLegacyMBRTransition(from, to) {
return fmt.Errorf("cannot change structure type from %q to %q", from.Type, to.Type)
}
}
if from.ID != to.ID {
return fmt.Errorf("cannot change structure ID from %q to %q", from.ID, to.ID)
}
if to.HasFilesystem() {
if !from.HasFilesystem() {
return fmt.Errorf("cannot change a bare structure to filesystem one")
}
if from.Filesystem != to.Filesystem {
return fmt.Errorf("cannot change filesystem from %q to %q",
from.Filesystem, to.Filesystem)
}
if from.Label != to.Label {
return fmt.Errorf("cannot change filesystem label from %q to %q",
from.Label, to.Label)
}
} else {
if from.HasFilesystem() {
return fmt.Errorf("cannot change a filesystem structure to a bare one")
}
}
return nil
}
func canUpdateVolume(from *PartiallyLaidOutVolume, to *LaidOutVolume) error {
if from.ID != to.ID {
return fmt.Errorf("cannot change volume ID from %q to %q", from.ID, to.ID)
}
if from.Schema != to.Schema {
return fmt.Errorf("cannot change volume schema from %q to %q", from.Schema, to.Schema)
}
if len(from.LaidOutStructure) != len(to.LaidOutStructure) {
return fmt.Errorf("cannot change the number of structures within volume from %v to %v", len(from.LaidOutStructure), len(to.LaidOutStructure))
}
return nil
}
type updatePair struct {
from *LaidOutStructure
to *LaidOutStructure
}
func defaultPolicy(from, to *LaidOutStructure) bool {
return to.Update.Edition > from.Update.Edition
}
// RemodelUpdatePolicy implements the update policy of a remodel scenario. The
// policy selects all non-MBR structures for the update.
func RemodelUpdatePolicy(from, _ *LaidOutStructure) bool {
if from.Role == schemaMBR {
return false
}
return true
}
func resolveUpdate(oldVol *PartiallyLaidOutVolume, newVol *LaidOutVolume, policy UpdatePolicyFunc) (updates []updatePair, err error) {
if len(oldVol.LaidOutStructure) != len(newVol.LaidOutStructure) {
return nil, errors.New("internal error: the number of structures in new and old volume definitions is different")
}
for j, oldStruct := range oldVol.LaidOutStructure {
newStruct := newVol.LaidOutStructure[j]
// update only when new edition is higher than the old one; boot
// assets are assumed to be backwards compatible, once deployed
// are not rolled back or replaced unless a higher edition is
// available
if policy(&oldStruct, &newStruct) {
updates = append(updates, updatePair{
from: &oldVol.LaidOutStructure[j],
to: &newVol.LaidOutStructure[j],
})
}
}
return updates, nil
}
type Updater interface {
// Update applies the update or errors out on failures. When no actual
// update was applied because the new content is identical a special
// ErrNoUpdate is returned.
Update() error
// Backup prepares a backup copy of data that will be modified by
// Update()
Backup() error
// Rollback restores data modified by update
Rollback() error
}
func applyUpdates(new GadgetData, updates []updatePair, rollbackDir string, observer ContentUpdateObserver) error {
updaters := make([]Updater, len(updates))
for i, one := range updates {
up, err := updaterForStructure(one.to, new.RootDir, rollbackDir, observer)
if err != nil {
return fmt.Errorf("cannot prepare update for volume structure %v: %v", one.to, err)
}
updaters[i] = up
}
var backupErr error
for i, one := range updaters {
if err := one.Backup(); err != nil {
backupErr = fmt.Errorf("cannot backup volume structure %v: %v", updates[i].to, err)
break
}
}
if backupErr != nil {
if observer != nil {
if err := observer.Canceled(); err != nil {
logger.Noticef("cannot observe canceled prepare update: %v", err)
}
}
return backupErr
}
if observer != nil {
if err := observer.BeforeWrite(); err != nil {
return fmt.Errorf("cannot observe prepared update: %v", err)
}
}
var updateErr error
var updateLastAttempted int
var skipped int
for i, one := range updaters {
updateLastAttempted = i
if err := one.Update(); err != nil {
if err == ErrNoUpdate {
skipped++
continue
}
updateErr = fmt.Errorf("cannot update volume structure %v: %v", updates[i].to, err)
break
}
}
if skipped == len(updaters) {
// all updates were a noop
return ErrNoUpdate
}
if updateErr == nil {
// all good, updates applied successfully
return nil
}
logger.Noticef("cannot update gadget: %v", updateErr)
// not so good, rollback ones that got applied
for i := 0; i <= updateLastAttempted; i++ {
one := updaters[i]
if err := one.Rollback(); err != nil {
// TODO: log errors to oplog
logger.Noticef("cannot rollback volume structure %v update: %v", updates[i].to, err)
}
}
if observer != nil {
if err := observer.Canceled(); err != nil {
logger.Noticef("cannot observe canceled update: %v", err)
}
}
return updateErr
}
var updaterForStructure = updaterForStructureImpl
func updaterForStructureImpl(ps *LaidOutStructure, newRootDir, rollbackDir string, observer ContentUpdateObserver) (Updater, error) {
var updater Updater
var err error
if !ps.HasFilesystem() {
updater, err = newRawStructureUpdater(newRootDir, ps, rollbackDir, findDeviceForStructureWithFallback)
} else {
updater, err = newMountedFilesystemUpdater(ps, rollbackDir, findMountPointForStructure, observer)
}
return updater, err
}
// MockUpdaterForStructure replace internal call with a mocked one, for use in tests only
func MockUpdaterForStructure(mock func(ps *LaidOutStructure, rootDir, rollbackDir string, observer ContentUpdateObserver) (Updater, error)) (restore func()) {
old := updaterForStructure
updaterForStructure = mock
return func() {
updaterForStructure = old
}
}