/
update.go
1798 lines (1599 loc) · 69.4 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
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// -*- 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"
"sort"
"strings"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/gadget/quantity"
"github.com/snapcore/snapd/kernel"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil/disks"
"github.com/snapcore/snapd/strutil"
)
var (
ErrNoUpdate = errors.New("nothing to update")
)
// 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
// (potentially not yet resolved) structures and returns true when the
// pair should be part of an update. It may also return a filter
// function for the resolved content when not all of the content
// should be applied as part of the update (e.g. when updating assets
// from the kernel snap).
type UpdatePolicyFunc func(from, to *LaidOutStructure) (bool, ResolvedContentFilterFunc)
// ResolvedContentFilterFunc is a callback that evaluates the given
// ResolvedContent and returns true if it should be applied as part of
// an update. This is relevant for e.g. asset updates that come from
// the kernel snap.
type ResolvedContentFilterFunc func(*ResolvedContent) 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. The role
// of the affected partition is needed as different assets are tracked
// depending on whether this is a boot or a seed partition.
//
// 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, partRole, 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
}
// searchVolumeWithTraitsAndMatchParts searches for a disk matching the given
// traits and returns the matched partitions.
func searchVolumeWithTraitsAndMatchParts(vol *Volume, traits DiskVolumeDeviceTraits, validateOpts *DiskVolumeValidationOptions) (disks.Disk, map[int]*OnDiskStructure, error) {
if validateOpts == nil {
validateOpts = &DiskVolumeValidationOptions{}
}
// iterate over the different traits, validating whether the resulting disk
// actually exists and matches the volume we have in the gadget.yaml
compatibleCandidate := func(candidate disks.Disk, method string, providedErr error) map[int]*OnDiskStructure {
if providedErr != nil {
if candidate != nil {
logger.Debugf("candidate disk %s not appropriate for volume %s because err: %v", candidate.KernelDeviceNode(), vol.Name, providedErr)
return nil
}
logger.Debugf("cannot locate disk for volume %s with method %s because err: %v", vol.Name, method, providedErr)
return nil
}
diskLayout, onDiskErr := OnDiskVolumeFromDevice(candidate.KernelDeviceNode())
if onDiskErr != nil {
// unexpected in reality, we already called one of
// DiskFromDeviceName or DiskFromDevicePath to get this reference,
// so it's unclear how those methods could return a disk that
// OnDiskVolumeFromDevice is unhappy about
logger.Debugf("cannot find on disk volume from candidate disk %s: %v", candidate.KernelDeviceNode(), onDiskErr)
return nil
}
// then try to validate it by laying out the volume
opts := &VolumeCompatibilityOptions{
AssumeCreatablePartitionsCreated: true,
AllowImplicitSystemData: validateOpts.AllowImplicitSystemData,
ExpectedStructureEncryption: validateOpts.ExpectedStructureEncryption,
}
gadgetStructToDiskStruct, ensureErr := EnsureVolumeCompatibility(vol, diskLayout, opts)
if ensureErr != nil {
logger.Debugf("candidate disk %s not appropriate for volume %s due to incompatibility: %v", candidate.KernelDeviceNode(), vol.Name, ensureErr)
return nil
}
// success, we found it
return gadgetStructToDiskStruct
}
// first try the kernel device path if it is set
if traits.OriginalDevicePath != "" {
disk, err := disks.DiskFromDevicePath(traits.OriginalDevicePath)
gadgetStructToDiskStruct := compatibleCandidate(disk, "device path", err)
if gadgetStructToDiskStruct != nil {
return disk, gadgetStructToDiskStruct, nil
}
}
// next try the kernel device node name
if traits.OriginalKernelPath != "" {
disk, err := disks.DiskFromDeviceName(traits.OriginalKernelPath)
gadgetStructToDiskStruct := compatibleCandidate(disk, "device name", err)
if gadgetStructToDiskStruct != nil {
return disk, gadgetStructToDiskStruct, nil
}
}
// next try the disk ID from the partition table
if traits.DiskID != "" {
// there isn't a way to find a disk using the disk ID directly, so we
// instead have to get all the disks and then check them all to see if
// the disk ID's match
blockdevDisks, err := disks.AllPhysicalDisks()
if err == nil {
for _, blockDevDisk := range blockdevDisks {
if blockDevDisk.DiskID() == traits.DiskID {
// found the block device for this Disk ID, get the
// disks.Disk for it
gadgetStructToDiskStruct := compatibleCandidate(blockDevDisk, "disk ID", err)
if gadgetStructToDiskStruct != nil {
return blockDevDisk, gadgetStructToDiskStruct, nil
}
// otherwise if it didn't match we keep iterating over
// the block devices, since we could have a situation
// where an attacker has cloned the disk and put their own
// content on it to attack the device and so there are two
// block devices with the same ID but non-matching
// structures
}
}
} else {
logger.Noticef("error getting all physical disks: %v", err)
}
}
// TODO: implement this final last ditch effort
// finally, try doing an inverse search using the individual
// structures to match a structure we measured previously to find a on disk
// device and then find a disk from that device and see if it matches
return nil, nil, fmt.Errorf("cannot find physical disk laid out to map with volume %s", vol.Name)
}
// 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 *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 SystemSave, SystemData, SystemBoot:
return true
default:
return false
}
}
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
}
}
func onDiskStructureIsLikelyImplicitSystemDataRole(gadgetVolume *Volume, diskLayout *OnDiskVolume, s OnDiskStructure) bool {
// in uc16/uc18 we used to allow system-data to be implicit / missing from
// the gadget.yaml in which case we won't have system-data in the laidOutVol
// but it will be in diskLayout, so we sometimes need to check if a given on
// disk partition looks like it was created implicitly by ubuntu-image as
// specified via the defaults in
// https://github.com/canonical/ubuntu-image-legacy/blob/master/ubuntu_image/parser.py#L568-L589
// namely it must meet the following conditions:
// * fs is ext4
// * partition type is "Linux filesystem data"
// * fs label is "writable"
// * this on disk structure is last on the disk
// * there is exactly one more structure on disk than partitions in the
// gadget
// * there is no system-data role in the gadget.yaml
// note: we specifically do not check the size of the structure because it
// likely was resized, but it also could have not been resized if there
// ended up being less than 10% free space as per the resize script in the
// initramfs:
// https://github.com/snapcore/core-build/blob/master/initramfs/scripts/local-premount/resize
// bare structures don't show up on disk, so we can't include them
// when calculating how many "structures" are in gadgetVolume to
// ensure that there is only one extra OnDiskStructure at the end
numPartsInGadget := 0
for _, s := range gadgetVolume.Structure {
if s.IsPartition() {
numPartsInGadget++
}
// also check for explicit system-data role
if s.Role == SystemData {
// s can't be implicit system-data since there is an explicit
// system-data
return false
}
}
numPartsOnDisk := len(diskLayout.Structure)
return s.PartitionFSType == "ext4" &&
(s.Type == "0FC63DAF-8483-4772-8E79-3D69D8477DE4" || s.Type == "83") &&
s.PartitionFSLabel == "writable" &&
// DiskIndex is 1-based
s.DiskIndex == numPartsOnDisk &&
numPartsInGadget+1 == numPartsOnDisk
}
// VolumeCompatibilityOptions is a set of options for determining how
// strict to be when evaluating whether an on-disk structure matches a laid out
// structure.
type VolumeCompatibilityOptions struct {
// AssumeCreatablePartitionsCreated will assume that all partitions such as
// ubuntu-data, ubuntu-save, etc. that are creatable in install mode have
// already been created and thus must be already exactly matching that which
// is in the gadget.yaml.
AssumeCreatablePartitionsCreated bool
// AllowImplicitSystemData allows the system-data role to be missing from
// the gadget volume as was allowed in UC18 and UC16 where the system-data
// partition would be dynamically inserted into the image at image build
// time by ubuntu-image without being mentioned in the gadget.yaml.
AllowImplicitSystemData bool
// ExpectedStructureEncryption is a map of the structure name to information
// about the encrypted partitions that can be used to validate whether a
// given structure should be accepted as an encrypted partition.
ExpectedStructureEncryption map[string]StructureEncryptionParameters
}
// EnsureVolumeCompatibility checks compatibility between a gadget volume and a
// real disk. It returns a map of the gadget structures yaml indexes to disk
// structures that was possible to match.
// TODO change to returning OnDiskAndGadgetStructurePair
func EnsureVolumeCompatibility(gadgetVolume *Volume, diskVolume *OnDiskVolume, opts *VolumeCompatibilityOptions) (map[int]*OnDiskStructure, error) {
gadgetStructIdxToOnDiskStruct := map[int]*OnDiskStructure{}
if opts == nil {
opts = &VolumeCompatibilityOptions{}
}
logger.Debugf("checking volume compatibility between gadget volume %s (partial: %v) and disk %s",
gadgetVolume.Name, gadgetVolume.Partial, diskVolume.Device)
eq := func(ds *OnDiskStructure, vss []VolumeStructure, vssIdx int) (bool, string) {
gs := &vss[vssIdx]
// name mismatch
if gs.Name != ds.Name {
// partitions have no names in MBR so bypass the name check
if gadgetVolume.Schema != "mbr" {
// don't return a reason if the names don't match
return false, ""
}
}
// start offset mismatch
if err := CheckValidStartOffset(ds.StartOffset, vss, vssIdx); err != nil {
return false, fmt.Sprintf("disk partition %q %v", ds.Name, err)
}
maxSz := effectivePartSize(gs)
switch {
// on disk size too small
case ds.Size < gs.MinSize:
return false, fmt.Sprintf("on disk size %d (%s) is smaller than gadget min size %d (%s)",
ds.Size, ds.Size.IECString(), gs.MinSize, gs.MinSize.IECString())
// on disk size too large
case ds.Size > maxSz:
// larger on disk size is allowed specifically only for system-data
if gs.Role != SystemData {
return false, fmt.Sprintf("on disk size %d (%s) is larger than gadget size %d (%s) (and the role should not be expanded)",
ds.Size, ds.Size.IECString(), maxSz, maxSz.IECString())
}
}
// If we got to this point, the structure on disk has the same
// name, and compatible size and offset, so the last thing to
// check is that the filesystem matches (or that we don't care
// about the filesystem).
// first handle the strict case where this partition was created at
// install in case it is an encrypted one
if opts.AssumeCreatablePartitionsCreated && IsCreatableAtInstall(gs) {
// only partitions that are creatable at install can be encrypted,
// check if this partition was encrypted
if encTypeParams, ok := opts.ExpectedStructureEncryption[gs.Name]; ok {
if encTypeParams.Method == "" {
return false, "encrypted structure parameter missing required parameter \"method\""
}
// for now we don't handle any other keys, but in case they show
// up in the wild for debugging purposes log off the key name
for k := range encTypeParams.unknownKeys {
if k != "method" {
logger.Noticef("ignoring unknown expected encryption structure parameter %q", k)
}
}
switch encTypeParams.Method {
case EncryptionLUKS:
// then this partition is expected to have been encrypted, the
// filesystem label on disk will need "-enc" appended
if ds.PartitionFSLabel != gs.Name+"-enc" {
return false, fmt.Sprintf("partition %[1]s is expected to be encrypted but is not named %[1]s-enc", gs.Name)
}
// the filesystem should also be "crypto_LUKS"
if ds.PartitionFSType != "crypto_LUKS" {
return false, fmt.Sprintf("partition %[1]s is expected to be encrypted but does not have an encrypted filesystem", gs.Name)
}
// at this point the partition matches
return true, ""
default:
return false, fmt.Sprintf("unsupported encrypted partition type %q", encTypeParams.Method)
}
}
// for non-encrypted partitions that were created at install, the
// below logic still applies
}
if opts.AssumeCreatablePartitionsCreated || !IsCreatableAtInstall(gs) {
// we assume that this partition has already been created
// successfully - either because this function was forced to(as is
// the case when doing gadget asset updates), or because this
// structure is not created during install
// note that we only check the filesystem if the gadget specified a
// filesystem, this is to allow cases where a structure in the
// gadget has a image, but does not specify the filesystem because
// it is some binary blob from a hardware vendor for non-Linux
// components on the device that _just so happen_ to also have a
// filesystem when the image is deployed to a partition. In this
// case we don't care about the filesystem at all because snapd does
// not touch it, unless a gadget asset update says to update that
// image file with a new binary image file. This also covers the
// partial filesystem case.
if gs.Filesystem != "" && gs.LinuxFilesystem() != ds.PartitionFSType {
// use more specific error message for structures that are
// not creatable at install when we are not being strict
if !IsCreatableAtInstall(gs) && !opts.AssumeCreatablePartitionsCreated {
return false, fmt.Sprintf("filesystems do not match (and the partition is not creatable at install): declared as %s, got %s", gs.Filesystem, ds.PartitionFSType)
}
// otherwise generic
return false, fmt.Sprintf("filesystems do not match: declared as %s, got %s", gs.Filesystem, ds.PartitionFSType)
}
}
// otherwise if we got here things are matching
return true, ""
}
gadgetContains := func(vss []VolumeStructure, ds *OnDiskStructure) (bool, string) {
reasonAbsent := ""
for vssIdx := range vss {
matches, reasonNotMatches := eq(ds, vss, vssIdx)
if matches {
return true, ""
}
// TODO: handle multiple error cases for DOS disks and fail early
// for GPT disks since we should not have multiple non-empty reasons
// for not matching for GPT disks, as that would require two YAML
// structures with the same name to be considered as candidates for
// a given on disk structure, and we do not allow duplicated
// structure names in the YAML at all via ValidateVolume.
//
// For DOS, since we cannot check the partition names, there will
// always be a reason if there was not a match, in which case we
// only want to return an error after we have finished searching the
// full haystack and didn't find any matches whatsoever. Note that
// the YAML structure that "should" have matched the on disk one we
// are looking for but doesn't because of some problem like wrong
// size or wrong filesystem may not be the last one, so returning
// only the last error like we do here is wrong. We should include
// all reasons why so the user can see which structure was the
// "closest" to what we were searching for so they can fix their
// gadget.yaml or on disk partitions so that it matches.
if reasonNotMatches != "" {
reasonAbsent = reasonNotMatches
}
}
if opts.AllowImplicitSystemData {
// Handle the case of an implicit system-data role before giving up;
// we used to allow system-data to be implicit from the gadget.yaml.
// In that case we won't have system-data in the gadget volume but it
// could be on the disk, so if after searching all the gadget
// structures we don't find the disk structure, check if we might
// be dealing with a structure that looks like the implicit
// system-data that ubuntu-image would have created.
if onDiskStructureIsLikelyImplicitSystemDataRole(gadgetVolume, diskVolume, *ds) {
return true, ""
}
}
return false, reasonAbsent
}
onDiskContains := func(dss []OnDiskStructure, vss []VolumeStructure, vssIdx int) (*OnDiskStructure, string) {
reasonAbsent := ""
for _, ds := range dss {
matches, reasonNotMatches := eq(&ds, vss, vssIdx)
if matches {
return &ds, ""
}
// this has the effect of only returning the last non-empty reason
// string
if reasonNotMatches != "" {
reasonAbsent = reasonNotMatches
}
}
return nil, reasonAbsent
}
// check size of volumes
lastUsableByte := quantity.Size(diskVolume.UsableSectorsEnd) * diskVolume.SectorSize
if gadgetVolume.MinSize() > lastUsableByte {
return nil, fmt.Errorf("device %v (last usable byte at %s) is too small to fit the requested minimal size (%s)", diskVolume.Device,
lastUsableByte.IECString(), gadgetVolume.MinSize().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 _, vs := range gadgetVolume.Structure {
if !vs.IsRoleMBR() {
for _, sz := range []quantity.Size{vs.MinSize, vs.Size} {
if sz%diskVolume.SectorSize != 0 {
return nil, fmt.Errorf("gadget volume structure %q size is not a multiple of disk sector size %v",
vs.Name, diskVolume.SectorSize)
}
}
}
}
// Check if gadget schema is compatible with the disk, when defined
if (!gadgetVolume.HasPartial(PartialSchema) || gadgetVolume.Schema != "") &&
!isCompatibleSchema(gadgetVolume.Schema, diskVolume.Schema) {
return nil, fmt.Errorf("disk partitioning schema %q doesn't match gadget schema %q", diskVolume.Schema, gadgetVolume.Schema)
}
// Check disk ID if defined in gadget
if gadgetVolume.ID != "" && gadgetVolume.ID != diskVolume.ID {
return nil, fmt.Errorf("disk ID %q doesn't match gadget volume ID %q", diskVolume.ID, gadgetVolume.ID)
}
// Check if all existing device partitions are also in gadget
// (unless partial strucuture).
if !gadgetVolume.HasPartial(PartialStructure) {
for _, ds := range diskVolume.Structure {
present, reasonAbsent := gadgetContains(gadgetVolume.Structure, &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 nil, fmt.Errorf("cannot find disk partition %s (starting at %d) in gadget%s", ds.Node, ds.StartOffset, reasonAbsent)
}
}
}
// check all structures in the gadget are present on the disk, or have a
// valid excuse for absence (i.e. mbr or creatable structures at install)
var prevDs *OnDiskStructure
for vssIdx, gs := range gadgetVolume.Structure {
// we ignore reasonAbsent here since if there was an extra on disk
// structure that didn't match something in the YAML, we would have
// caught it above, this loop can only ever identify structures in the
// YAML that are not on disk at all
if ds, _ := onDiskContains(diskVolume.Structure, gadgetVolume.Structure, vssIdx); ds != nil {
gadgetStructIdxToOnDiskStruct[gs.YamlIndex] = ds
prevDs = ds
continue
}
// otherwise not present, figure out if it has a valid excuse
if !gs.IsPartition() {
// Raw structures like mbr or other "bare" type will not be
// identified by linux and thus should be skipped as they will not
// show up on the disk. However, we insert a value in the map,
// assuming they are where expected.
offset := gs.Offset
if offset == nil {
// Case only possible if min-size is being used so we are in
// an update. We will always have prevDs set because as a
// minimum the first partition will have offset defined.
// In any case, if using bare partitions, it is not a great
// idea to have some previous partition with a valid range
// of sizes.
offsetV := prevDs.StartOffset + quantity.Offset(prevDs.Size)
offset = &offsetV
}
ds := &OnDiskStructure{
Name: gs.Name,
Type: gs.Type,
StartOffset: *offset,
Size: gs.Size,
}
gadgetStructIdxToOnDiskStruct[gs.YamlIndex] = ds
prevDs = ds
continue
}
// allow structures that are creatable during install if we don't assume
// created partitions to already exist
if IsCreatableAtInstall(&gs) && !opts.AssumeCreatablePartitionsCreated {
continue
}
return nil, fmt.Errorf("cannot find gadget structure %q on disk", gs.Name)
}
// finally ensure that all encrypted partitions mentioned in the options are
// present in the gadget.yaml (and thus will also need to have been present
// on the disk)
for gadgetLabel := range opts.ExpectedStructureEncryption {
found := false
for _, gs := range gadgetVolume.Structure {
if gs.Name == gadgetLabel {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("expected encrypted structure %s not present in gadget", gadgetLabel)
}
}
return gadgetStructIdxToOnDiskStruct, nil
}
// TODO:ICE: remove this as we only support LUKS (and ICE is a variant of LUKS now)
type DiskEncryptionMethod string
const (
// values for the "method" key of encrypted structure information
// standard LUKS as it is used for automatic FDE using SecureBoot and TPM
// 2.0 in UC20+
EncryptionLUKS DiskEncryptionMethod = "LUKS"
)
// DiskVolumeValidationOptions is a set of options on how to validate a disk to
// volume mapping for a specific disk/volume pair. It is closely related to the
// options provided to EnsureVolumeCompatibility via
// EnsureVolumeCompatibilityOptions.
type DiskVolumeValidationOptions struct {
// AllowImplicitSystemData has the same meaning as the eponymously named
// field in VolumeCompatibilityOptions.
AllowImplicitSystemData bool
// ExpectedEncryptedPartitions is a map of the names (gadget structure
// names) of partitions that are encrypted on the volume and information
// about that encryption.
ExpectedStructureEncryption map[string]StructureEncryptionParameters
}
// DiskTraitsFromDeviceAndValidate takes a gadget volume and an
// expected disk device path and confirms that they are compatible,
// and then builds up the disk volume traits for that device. If the
// laid out volume is not compatible with the disk structure for the
// specified device an error is returned.
func DiskTraitsFromDeviceAndValidate(vol *Volume, dev string, opts *DiskVolumeValidationOptions) (res DiskVolumeDeviceTraits, err error) {
if opts == nil {
opts = &DiskVolumeValidationOptions{}
}
// get the disk layout for this device
diskLayout, err := OnDiskVolumeFromDevice(dev)
if err != nil {
return res, fmt.Errorf("cannot read %v partitions for candidate volume %s: %v", dev, vol.Name, err)
}
// ensure that the on disk volume and the gadget volume are actually
// compatible
volCompatOpts := &VolumeCompatibilityOptions{
// at this point all partitions should be created
AssumeCreatablePartitionsCreated: true,
// provide the other opts as we were provided
AllowImplicitSystemData: opts.AllowImplicitSystemData,
ExpectedStructureEncryption: opts.ExpectedStructureEncryption,
}
gadgetToDiskStruct, err := EnsureVolumeCompatibility(vol, diskLayout, volCompatOpts)
if err != nil {
return res, fmt.Errorf("volume %s is not compatible with disk %s: %v", vol.Name, dev, err)
}
// also get a Disk{} interface for this device
disk, err := disks.DiskFromDeviceName(dev)
if err != nil {
return res, fmt.Errorf("cannot get disk for device %s: %v", dev, err)
}
diskPartitions, err := disk.Partitions()
if err != nil {
return res, fmt.Errorf("cannot get partitions for disk device %s: %v", dev, err)
}
// make a map of start offsets to partitions for lookup
diskPartitionsByOffset := make(map[uint64]disks.Partition, len(diskPartitions))
for _, p := range diskPartitions {
diskPartitionsByOffset[p.StartInBytes] = p
}
mappedStructures := make([]DiskStructureDeviceTraits, 0, len(diskLayout.Structure))
// create the traits for each structure looping over the gadget structures
// to ensure that extra partitions don't sneak in - we double check things
// again below this loop
for _, structure := range vol.Structure {
// don't create traits for non-partitions, there is nothing we can
// measure on the disk about bare structures other than perhaps reading
// their content - the fact that bare structures do not overlap with
// real partitions will have been validated when the YAML was validated
// previously
if !structure.IsPartition() {
continue
}
ds, ok := gadgetToDiskStruct[structure.YamlIndex]
if !ok {
return res, fmt.Errorf("internal error: all disk structures should have been matched")
}
part, ok := diskPartitionsByOffset[uint64(ds.StartOffset)]
if !ok {
// unexpected error - somehow this structure's start offset is not
// present in the OnDiskVolume, which is unexpected because we
// validated that the gadget volume structure matches the on disk
// volume
return res, fmt.Errorf("internal error: inconsistent disk structures from gadget and disks.Disk: structure starting at %d missing on disk", ds.StartOffset)
}
ms := DiskStructureDeviceTraits{
Size: quantity.Size(part.SizeInBytes),
Offset: quantity.Offset(part.StartInBytes),
PartitionUUID: part.PartitionUUID,
OriginalKernelPath: part.KernelDeviceNode,
OriginalDevicePath: part.KernelDevicePath,
PartitionType: part.PartitionType,
PartitionLabel: part.PartitionLabel, // this will be empty on dos disks
FilesystemLabel: part.FilesystemLabel, // blkid encoded
FilesystemUUID: part.FilesystemUUID, // blkid encoded
FilesystemType: part.FilesystemType,
}
mappedStructures = append(mappedStructures, ms)
// delete this partition from the map
delete(diskPartitionsByOffset, uint64(ds.StartOffset))
}
// We should have deleted all structures from diskPartitionsByOffset that
// are in the gadget.yaml volume, however there is a small
// possibility (mainly due to bugs) where we could still have partitions in
// diskPartitionsByOffset. So we check to make sure there are no partitions
// left over.
// However, the one notable exception to this is in the case of legacy UC16
// or UC18 gadgets where the system-data role could have been left out and
// ubuntu-image would dynamically create the partition. In this case, we
// ought to just ignore this on-disk structure since it is not in the
// gadget.yaml, and the primary use case of tracking disks and structures is
// for gadget asset update, but by definition something which is not in the
// gadget.yaml cannot be updated via gadget asset updates.
switch len(diskPartitionsByOffset) {
case 0:
// expected, no implicit system-data
break
case 1:
// could be implicit system-data
if opts.AllowImplicitSystemData {
var part disks.Partition
for _, part = range diskPartitionsByOffset {
break
}
s, err := OnDiskStructureFromPartition(part)
if err != nil {
return res, err
}
if onDiskStructureIsLikelyImplicitSystemDataRole(vol, diskLayout, s) {
// it is likely the implicit system-data
logger.Debugf("Identified implicit system-data role on system as %s", s.Node)
break
}
}
fallthrough
default:
// we for sure have left over partitions that should have been in the
// gadget.yaml - make a nice string with what partitions are leftover
leftovers := []string{}
for _, part := range diskPartitionsByOffset {
leftovers = append(leftovers, part.KernelDeviceNode)
}
if vol.HasPartial(PartialStructure) {
logger.Debugf("additional partitions on disk %s ignored as the gadget has partial structures: %v", disk.KernelDeviceNode(), leftovers)
} else {
// this is an internal error because to get here we would have had to
// pass validation in EnsureVolumeCompatibility but then still have
// extra partitions - the only non-buggy situation where that function
// passes validation but leaves partitions on disk not in the YAML is
// the implicit system-data role handled above
return res, fmt.Errorf("internal error: unexpected additional partitions on disk %s not present in the gadget layout: %v", disk.KernelDeviceNode(), leftovers)
}
}
return DiskVolumeDeviceTraits{
OriginalDevicePath: disk.KernelDevicePath(),
OriginalKernelPath: dev,
DiskID: diskLayout.ID,
Structure: mappedStructures,
Size: diskLayout.Size,
SectorSize: diskLayout.SectorSize,
Schema: disk.Schema(),
StructureEncryption: opts.ExpectedStructureEncryption,
}, nil
}
// unable to proceed with the gadget asset update, but not fatal to the refresh
// operation itself
var errSkipUpdateProceedRefresh = errors.New("cannot identify disk for gadget asset update")
// buildNewVolumeToDeviceMapping builds a DiskVolumeDeviceTraits for only the
// volume containing the system-boot role, when we cannot load an existing
// traits object from disk-mapping.json. It is meant to be used only with all
// UC16/UC18 installs as well as UC20 installs from before we started writing
// disk-mapping.json during install mode.
func buildNewVolumeToDeviceMapping(mod Model, old GadgetData, vols map[string]*Volume) (map[string]DiskVolumeDeviceTraits, error) {
var likelySystemBootVolume string
isPreUC20 := (mod.Grade() == asserts.ModelGradeUnset)
if len(old.Info.Volumes) == 1 {
// If we only have one volume, then that is the volume we are concerned
// with, we do not validate that it has a system-boot role on it like
// we do in the multi-volume case below, this is because we used to
// allow installation of gadgets that have no system-boot role on them
// at all
// then we only have one volume to be concerned with
for volName := range old.Info.Volumes {
likelySystemBootVolume = volName
}
} else {
// we need to pick the volume, since updates for this setup are best
// effort and mainly focused on the main volume with system-* roles
// on it, we need to pick the volume with that role
volumeLoop:
for volName, vol := range old.Info.Volumes {
for _, structure := range vol.Structure {
if structure.Role == SystemBoot {
// this is the volume
likelySystemBootVolume = volName
break volumeLoop
}
}
}
}
if likelySystemBootVolume == "" {
// this is only possible in the case where there is more than one volume
// and we didn't find system-boot anywhere, in this case for pre-UC20
// we use a non-fatal error and just don't perform any update - this was
// always the old behavior so we are not regressing here
if isPreUC20 {
logger.Noticef("WARNING: cannot identify disk for gadget asset update of volume %s: unable to find any volume with system-boot role on it", likelySystemBootVolume)
return nil, errSkipUpdateProceedRefresh
}
// on UC20 and later however this is a fatal error, we should never have
// allowed installation of a gadget which does not have the system-boot
// role on it
return nil, fmt.Errorf("cannot find any volume with system-boot, gadget is broken")
}
vol := vols[likelySystemBootVolume]
// search for matching devices that correspond to the gadget volume
dev := ""
for i := range vol.Structure {
// here it is okay that we require there to be either a partition label
// or a filesystem label since we require there to be a system-boot role
// on this volume which by definition must have a filesystem
structureDevice, err := FindDeviceForStructure(&vol.Structure[i])
if err == ErrDeviceNotFound {
continue
}
if err != nil {
// TODO: should this be a fatal error?
return nil, err
}
// we found a device for this structure, get the parent disk
// and save that as the device for this volume
disk, err := disks.DiskFromPartitionDeviceNode(structureDevice)
if err != nil {
// TODO: should we keep looping instead and try again with
// another structure? it probably wouldn't work because we found
// something on disk with the same name as something from the
// gadget.yaml, but then we failed to make a disk from that
// partition which suggests something is inconsistent with the
// state of the disk/udev database
return nil, err
}
dev = disk.KernelDeviceNode()
break
}
if dev == "" {
// couldn't find a disk at all, pre-UC20 we just warn about this
// but let the update continue
if isPreUC20 {
logger.Noticef("WARNING: cannot identify disk for gadget asset update of volume %s", likelySystemBootVolume)
return nil, errSkipUpdateProceedRefresh
}
// fatal error on UC20+
return nil, fmt.Errorf("cannot identify disk for gadget asset update of volume %s", likelySystemBootVolume)
}
// we found the device, construct the traits with validation options
validateOpts := &DiskVolumeValidationOptions{
// allow implicit system-data on pre-uc20 only
AllowImplicitSystemData: isPreUC20,
}
// setup encrypted structure information to perform validation if this
// device used encryption
if !isPreUC20 {
// TODO: this needs to check if the specified partitions are ICE when
// we support ICE too
// check if there is a marker file written, that will indicate if
// encryption was turned on
if device.HasEncryptedMarkerUnder(dirs.SnapFDEDir) {
// then we have the crypto marker file for encryption
// cross-validation between ubuntu-data and ubuntu-save stored from
// install mode, so mark ubuntu-save and data as expected to be
// encrypted
validateOpts.ExpectedStructureEncryption = map[string]StructureEncryptionParameters{
"ubuntu-data": {Method: EncryptionLUKS},
"ubuntu-save": {Method: EncryptionLUKS},
}
}
}
traits, err := DiskTraitsFromDeviceAndValidate(vol, dev, validateOpts)
if err != nil {
if isPreUC20 {
logger.Noticef("WARNING: not applying gadget asset updates on main system-boot volume due to error while finding disk traits: %v", err)
return nil, errSkipUpdateProceedRefresh
}
return nil, err
}
// TODO: should we save the traits here so they can be re-used in another
// future update routine?
return map[string]DiskVolumeDeviceTraits{
likelySystemBootVolume: traits,
}, nil
}
// StructureLocation represents the location of a structure for updating
// purposes. Either Device + Offset must be set for a raw structure without a
// filesystem, or RootMountPoint must be set for structures with a
// filesystem.
type StructureLocation struct {
// Device is the kernel device node path such as /dev/vda1 for the
// structure's backing physical disk.
Device string
// Offset is the offset from 0 for the physical disk that this structure
// starts at.
Offset quantity.Offset
// RootMountPoint is the directory where the root directory of the structure
// is mounted read/write. There may be other mount points for this structure
// on the system, but this one is guaranteed to be writable and thus
// suitable for gadget asset updates.
RootMountPoint string
}
// buildVolumeStructureToLocation builds a map of gadget volumes to
// locations and to matched disk structures.
func buildVolumeStructureToLocation(mod Model,
old GadgetData,
vols map[string]*Volume,
volToDeviceMapping map[string]DiskVolumeDeviceTraits,
missingInitialMapping bool,
) (map[string]map[int]StructureLocation, map[string]map[int]*OnDiskStructure, error) {
isPreUC20 := (mod.Grade() == asserts.ModelGradeUnset)
// helper function for handling non-fatal errors on pre-UC20
maybeFatalError := func(err error) error {
if missingInitialMapping && isPreUC20 {
// this is not a fatal error on pre-UC20
logger.Noticef("WARNING: not applying gadget asset updates on main system-boot volume due to error mapping volume to physical disk: %v", err)
return errSkipUpdateProceedRefresh
}
return err
}
volumeStructureToLocation := make(map[string]map[int]StructureLocation, len(old.Info.Volumes))
gadgetVolToPartMap := make(map[string]map[int]*OnDiskStructure, len(old.Info.Volumes))
// now for each volume, iterate over the structures, putting the
// necessary info into the map for that volume as we iterate