diff --git a/gadget/device_darwin.go b/gadget/device_darwin.go index 471a5f559e1..b0a10598524 100644 --- a/gadget/device_darwin.go +++ b/gadget/device_darwin.go @@ -28,3 +28,7 @@ var errNotImplemented = errors.New("not implemented") func FindDeviceForStructure(vs *VolumeStructure) (string, error) { return "", errNotImplemented } + +func ResolveDeviceForStructure(device string) (string, error) { + return "", errNotImplemented +} diff --git a/gadget/device_linux.go b/gadget/device_linux.go index 70deec70013..dd125e39624 100644 --- a/gadget/device_linux.go +++ b/gadget/device_linux.go @@ -31,38 +31,10 @@ import ( var evalSymlinks = filepath.EvalSymlinks -// FindDeviceForStructure attempts to find an existing block device matching -// given volume structure, by inspecting its name and, optionally, the -// filesystem label. Assumes that the host's udev has set up device symlinks -// correctly. -func FindDeviceForStructure(vs *VolumeStructure) (string, error) { - var candidates []string - - if vs.Name != "" { - byPartlabel := filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel/", disks.BlkIDEncodeLabel(vs.Name)) - candidates = append(candidates, byPartlabel) - } - if vs.HasFilesystem() { - fsLabel := vs.Label - if fsLabel == "" && vs.Name != "" { - // when image is built and the structure has no - // filesystem label, the structure name will be used by - // default as the label - fsLabel = vs.Name - } - if fsLabel != "" { - candLabel, err := disks.CandidateByLabelPath(fsLabel) - if err == nil { - candidates = append(candidates, candLabel) - } else { - logger.Debugf("no by-label candidate for %q: %v", fsLabel, err) - } - } - } - +func resolveMaybeDiskPaths(diskPaths []string) (string, error) { var found string var match string - for _, candidate := range candidates { + for _, candidate := range diskPaths { if !osutil.FileExists(candidate) { continue } @@ -91,3 +63,48 @@ func FindDeviceForStructure(vs *VolumeStructure) (string, error) { return found, nil } + +func discoverDeviceDiskCandidatesForStructure(vs *VolumeStructure) (candidates []string) { + if vs.Name != "" { + byPartlabel := filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel/", disks.BlkIDEncodeLabel(vs.Name)) + candidates = append(candidates, byPartlabel) + } + if vs.HasFilesystem() { + fsLabel := vs.Label + if fsLabel == "" && vs.Name != "" { + // when image is built and the structure has no + // filesystem label, the structure name will be used by + // default as the label + fsLabel = vs.Name + } + if fsLabel != "" { + candLabel, err := disks.CandidateByLabelPath(fsLabel) + if err == nil { + candidates = append(candidates, candLabel) + } else { + logger.Debugf("no by-label candidate for %q: %v", fsLabel, err) + } + } + } + return candidates +} + +// FindDeviceForStructure attempts to find an existing block device matching +// given volume structure, by inspecting its name and, optionally, the +// filesystem label. Assumes that the host's udev has set up device symlinks +// correctly. +func FindDeviceForStructure(vs *VolumeStructure) (string, error) { + candidates := discoverDeviceDiskCandidatesForStructure(vs) + return resolveMaybeDiskPaths(candidates) +} + +// ResolveDeviceForStructure is an opposite to FindDeviceForStructure that allows +// supplying a device path to resolve a specific disk. Calling this without a filter +// (i.e device == ""), will return an error +// The device path must be a path into /dev/disk/** +func ResolveDeviceForStructure(device string) (string, error) { + if device == "" { + return "", fmt.Errorf("internal error: device must be supplied") + } + return resolveMaybeDiskPaths([]string{device}) +} diff --git a/gadget/device_test.go b/gadget/device_test.go index c7a54695ac2..59b28c42e1b 100644 --- a/gadget/device_test.go +++ b/gadget/device_test.go @@ -21,6 +21,7 @@ package gadget_test import ( "errors" + "fmt" "os" "path/filepath" @@ -244,3 +245,47 @@ func (d *deviceSuite) TestDeviceFindBadEvalSymlinks(c *C) { c.Check(err, ErrorMatches, `cannot read device link: failed`) c.Check(found, Equals, "") } + +func (d *deviceSuite) TestResolveDeviceEmptyBase(c *C) { + found, err := gadget.ResolveDeviceForStructure("") + c.Check(err, ErrorMatches, `internal error: device must be supplied`) + c.Check(found, Equals, "") +} + +func (d *deviceSuite) TestResolveDeviceBaseDeviceNotSymlink(c *C) { + // only the by-filesystem-label symlink + fakedevice := filepath.Join(d.dir, "/dev/fakedevice") + c.Assert(os.Symlink(fakedevice, filepath.Join(d.dir, "/dev/disk/by-label/foo")), IsNil) + c.Assert(os.Symlink("../../fakedevice", filepath.Join(d.dir, "/dev/disk/by-partlabel/relative")), IsNil) + + found, err := gadget.ResolveDeviceForStructure(fakedevice) + c.Check(err, ErrorMatches, fmt.Sprintf(`candidate %s/dev/fakedevice is not a symlink`, d.dir)) + c.Check(found, Equals, "") +} + +func (d *deviceSuite) TestResolveDeviceMatchesBaseDeviceAsSymlink(c *C) { + // only the by-filesystem-label symlink + fakedevice := filepath.Join(d.dir, "/dev/fakedevice") + c.Assert(os.Symlink(fakedevice, filepath.Join(d.dir, "/dev/disk/by-label/foo")), IsNil) + c.Assert(os.Symlink("../../fakedevice", filepath.Join(d.dir, "/dev/disk/by-partlabel/relative")), IsNil) + + found, err := gadget.ResolveDeviceForStructure(filepath.Join(d.dir, "/dev/disk/by-label/foo")) + c.Check(err, IsNil) + c.Check(found, Equals, filepath.Join(d.dir, "/dev/fakedevice")) +} + +func (d *deviceSuite) TestResolveDeviceNoMatchesBaseDevice(c *C) { + // fake two devices + // only the by-filesystem-label symlink + fakedevice0 := filepath.Join(d.dir, "/dev/fakedevice") + fakedevice1 := filepath.Join(d.dir, "/dev/fakedevice1") + + c.Assert(os.Symlink(fakedevice0, filepath.Join(d.dir, "/dev/disk/by-label/foo")), IsNil) + c.Assert(os.Symlink(fakedevice1, filepath.Join(d.dir, "/dev/disk/by-label/bar")), IsNil) + + c.Assert(os.Symlink("../../fakedevice", filepath.Join(d.dir, "/dev/disk/by-partlabel/relative")), IsNil) + + found, err := gadget.ResolveDeviceForStructure(fakedevice1) + c.Check(err, ErrorMatches, `device not found`) + c.Check(found, Equals, "") +} diff --git a/gadget/export_test.go b/gadget/export_test.go index e570631290b..9e17f3c7a0a 100644 --- a/gadget/export_test.go +++ b/gadget/export_test.go @@ -19,7 +19,10 @@ package gadget -import "github.com/snapcore/snapd/gadget/quantity" +import ( + "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/testutil" +) type ( MountedFilesystemUpdater = mountedFilesystemUpdater @@ -97,3 +100,9 @@ func NewInvalidOffsetError(offset, lowerBound, upperBound quantity.Offset) *Inva func (v *Volume) YamlIdxToStructureIdx(yamlIdx int) (int, error) { return v.yamlIdxToStructureIdx(yamlIdx) } + +func MockFindVolumesMatchingDeviceAssignment(f func(gi *Info) (map[string]DeviceVolume, error)) (restore func()) { + r := testutil.Backup(&FindVolumesMatchingDeviceAssignment) + FindVolumesMatchingDeviceAssignment = f + return r +} diff --git a/gadget/gadget.go b/gadget/gadget.go index 05d226fbc5b..248d0687af1 100644 --- a/gadget/gadget.go +++ b/gadget/gadget.go @@ -27,6 +27,7 @@ import ( "fmt" "math" "os" + "path" "path/filepath" "regexp" "sort" @@ -35,6 +36,7 @@ import ( "gopkg.in/yaml.v2" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget/edition" "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/logger" @@ -117,7 +119,8 @@ type KernelCmdline struct { } type Info struct { - Volumes map[string]*Volume `yaml:"volumes,omitempty"` + Volumes map[string]*Volume `yaml:"volumes,omitempty"` + VolumeAssignments []*VolumeAssignment `yaml:"volume-assignments,omitempty"` // Default configuration for snaps (snap-id => key => value). Defaults map[string]map[string]interface{} `yaml:"defaults,omitempty"` @@ -531,6 +534,89 @@ type VolumeUpdate struct { Preserve []string `yaml:"preserve" json:"preserve"` } +// VolumeAssignment is an optional set of volume-to-disk assignments +// that can be specified. This is a nice way of reusing the same gadget +// for multiple devices, or a way to map multiple volumes to the same disk +// for cases like eMMC. Each assignment in this structure refers to a volume +// and a device path (/dev/disk/** for now). +type VolumeAssignment struct { + Name string `yaml:"name"` + Assignments map[string]*DeviceAssignment `yaml:"assignment"` +} + +func (va *VolumeAssignment) validate(volumes map[string]*Volume) error { + if len(va.Assignments) == 0 { + return fmt.Errorf("no assignments specified") + } + + for name, asv := range va.Assignments { + if v := volumes[name]; v == nil { + return fmt.Errorf("volume %q is mentioned in assignment but has not been defined", name) + } + if err := asv.validate(); err != nil { + return fmt.Errorf("%q: %v", name, err) + } + } + return nil +} + +// DeviceAssignment is the device data for each volume assignment. Currently +// just keeps the device the volume should be assigned to. +type DeviceAssignment struct { + Device string `yaml:"device"` +} + +func (da *DeviceAssignment) validate() error { + if !strings.HasPrefix(da.Device, "/dev/disk/") { + return fmt.Errorf("unsupported device path %q, for now only paths under /dev/disk are valid", da.Device) + } + return nil +} + +type DeviceVolume struct { + // Device is optional, and may mean that no mapping has been set + // for the volume + Device string + Volume *Volume +} + +func areAssignmentsMatchingCurrentDevice(assignments map[string]*DeviceAssignment) bool { + for _, va := range assignments { + if _, err := os.Stat(path.Join(dirs.GlobalRootDir, va.Device)); err != nil { + // XXX: expect this to mean no path, consider actually + // ensuring the error is not-exists + logger.Debugf("failed to stat device %s: %v", va.Device, err) + return false + } + } + return true +} + +// Indirection here for mocking +var FindVolumesMatchingDeviceAssignment = findVolumesMatchingDeviceAssignmentImpl + +// findVolumesMatchingDeviceAssignmentImpl does a best effort match of the volume-assignments +// against the current device. We find the first assignment that has all device paths matching +func findVolumesMatchingDeviceAssignmentImpl(gi *Info) (map[string]DeviceVolume, error) { + for _, vas := range gi.VolumeAssignments { + if !areAssignmentsMatchingCurrentDevice(vas.Assignments) { + continue + } + + // build a list of volumes matching assignment + logger.Noticef("found valid device-assignment: %s", vas.Name) + volumes := make(map[string]DeviceVolume) + for vol := range vas.Assignments { + volumes[vol] = DeviceVolume{ + Device: vas.Assignments[vol].Device, + Volume: gi.Volumes[vol], + } + } + return volumes, nil + } + return nil, fmt.Errorf("no matching volume-assignment for current device") +} + // DiskVolumeDeviceTraits is a set of traits about a disk that were measured at // a previous point in time on the same device, and is used primarily to try and // map a volume in the gadget.yaml to a physical device on the system after the @@ -696,6 +782,34 @@ func LoadDiskVolumesDeviceTraits(dir string) (map[string]DiskVolumeDeviceTraits, return mapping, nil } +func deviceNodeForStructure(device string, vs *VolumeStructure) (string, error) { + if device != "" { + disk, err := disks.DiskFromDeviceName(device) + if err != nil { + return "", err + } + + return disk.KernelDeviceNode(), nil + } + + structureDevice, err := FindDeviceForStructure(vs) + if err != nil && err != ErrDeviceNotFound { + return "", err + } + + if structureDevice != "" { + // 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 { + return "", err + } + + return disk.KernelDeviceNode(), nil + } + return "", nil +} + // AllDiskVolumeDeviceTraits takes a mapping of volume name to Volume // and produces a map of volume name to DiskVolumeDeviceTraits. Since // doing so uses DiskVolumeDeviceTraitsForDevice, it will also @@ -703,16 +817,19 @@ func LoadDiskVolumesDeviceTraits(dir string) (map[string]DiskVolumeDeviceTraits, // and matching before returning. func AllDiskVolumeDeviceTraits(allVols map[string]*Volume, optsPerVolume map[string]*DiskVolumeValidationOptions) (map[string]DiskVolumeDeviceTraits, error) { // build up the mapping of volumes to disk device traits - - allTraits := map[string]DiskVolumeDeviceTraits{} - // find all devices which map to volumes to save the current state of the // system + allTraits := map[string]DiskVolumeDeviceTraits{} for name, vol := range allVols { // try to find a device for a structure inside the volume, we have a // loop to attempt to use all structures in the volume in case there are // partitions we can't map to a device directly at first using the // device symlinks that FindDeviceForStructure uses + opts := optsPerVolume[name] + if opts == nil { + opts = &DiskVolumeValidationOptions{} + } + dev := "" for _, vs := range vol.Structure { // TODO: This code works for volumes that have at least one @@ -725,26 +842,16 @@ func AllDiskVolumeDeviceTraits(allVols map[string]*Volume, optsPerVolume map[str // locations, aside from potentially reading and comparing the bytes // at the expected locations, but that is probably fragile and very // non-performant. - if !vs.IsPartition() { // skip trying to find non-partitions on disk, it won't work continue } - structureDevice, err := FindDeviceForStructure(&vs) - if err != nil && err != ErrDeviceNotFound { + devNode, err := deviceNodeForStructure(opts.Device, &vs) + if err != nil { return nil, err - } - if structureDevice != "" { - // 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 { - return nil, err - } - - dev = disk.KernelDeviceNode() - break + } else if devNode != "" { + dev = devNode } } @@ -755,10 +862,6 @@ func AllDiskVolumeDeviceTraits(allVols map[string]*Volume, optsPerVolume map[str // now that we have a candidate device for this disk, build up the // traits for it, this will also validate concretely that the // device we picked and the volume are compatible - opts := optsPerVolume[name] - if opts == nil { - opts = &DiskVolumeValidationOptions{} - } traits, err := DiskTraitsFromDeviceAndValidate(vol, dev, opts) if err != nil { return nil, fmt.Errorf("cannot gather disk traits for device %s to use with volume %s: %v", dev, name, err) @@ -992,6 +1095,9 @@ func InfoFromGadgetYaml(gadgetYaml []byte, model Model) (*Info, error) { } // basic validation + // XXX: With the addition of volume-assignments we could even do per + // "device" validation here and check that atleast one bootloader + // assignment has been set for each of the assignments var bootloadersFound int for name := range gi.Volumes { v := gi.Volumes[name] @@ -1039,6 +1145,13 @@ func InfoFromGadgetYaml(gadgetYaml []byte, model Model) (*Info, error) { return nil, fmt.Errorf("too many (%d) bootloaders declared", bootloadersFound) } + // do basic validation for volume-assignments + for _, va := range gi.VolumeAssignments { + if err := va.validate(gi.Volumes); err != nil { + return nil, fmt.Errorf("invalid volume-assignment for %q: %v", va.Name, err) + } + } + return &gi, nil } diff --git a/gadget/gadget_test.go b/gadget/gadget_test.go index bd0d4f104c4..10b32063688 100644 --- a/gadget/gadget_test.go +++ b/gadget/gadget_test.go @@ -5247,3 +5247,373 @@ func (s *gadgetYamlTestSuite) TestGadgetInfoHasRole(c *C) { c.Check(info.HasRole(gadget.SystemBoot), Equals, true) c.Check(info.HasRole(gadget.SystemSeedNull), Equals, false) } + +type gadgetYamlVolumeAssignmentSuite struct { + testutil.BaseTest + + dir0 string + dir1 string + gadget0YamlPath string + gadget1YamlPath string +} + +var _ = Suite(&gadgetYamlVolumeAssignmentSuite{}) + +var mockVolumeAssignmentGadgetYamlBase = []byte(` +volumes: + lun-0: + schema: mbr + bootloader: u-boot + id: 0C + structure: + - filesystem-label: system-boot + offset: 12345 + offset-write: 777 + size: 88888 + type: 0C + filesystem: vfat + content: + - source: foo + target: / + lun-1: + schema: mbr + structure: + - name: system-test + type: bare + size: 16M + content: + - image: content.img +`) + +var mockVolumeAssignmentGadget0Yaml = string(mockVolumeAssignmentGadgetYamlBase) + ` +volume-assignments: +- name: foo-device + assignment: + lun-0: + device: /dev/disk/by-path/pci-0000:02:00.1-ata-5 +- name: bar-device + assignment: + lun-0: + device: /dev/disk/by-diskseq/1 + lun-1: + device: /dev/disk/by-id/wwm1234 +` + +var mockInvalidAssignmentGadgetYaml = string(mockVolumeAssignmentGadget0Yaml) + ` +- name: baz-device + assignment: + lun-1: + device: /dev/by-sda +` + +var mockNonExistingAssignmentGadgetYaml = string(mockVolumeAssignmentGadget0Yaml) + ` +- name: baz-device + assignment: + lun-2: + device: /dev/disk/by-diskseq/1 +` + +var mockNoAssignmentGadgetYaml = string(mockVolumeAssignmentGadget0Yaml) + ` +- name: baz-device +` + +var mockChangedAssignmentGadget1Yaml = string(mockVolumeAssignmentGadgetYamlBase) + ` +volume-assignments: +- name: foo-device + assignment: + lun-0: + device: /dev/disk/by-id/foz1234 +- name: bar-device + assignment: + lun-0: + device: /dev/disk/by-diskseq/1 + lun-1: + device: /dev/disk/by-path/pci-0000:02:00.1-ata-5 +` + +func (s *gadgetYamlVolumeAssignmentSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + dirs.SetRootDir(c.MkDir()) + + // This will act as the "old" gadget + s.dir0 = c.MkDir() + c.Assert(os.MkdirAll(filepath.Join(s.dir0, "meta"), 0755), IsNil) + s.gadget0YamlPath = filepath.Join(s.dir0, "meta", "gadget.yaml") + + // This will act as the new gadget + s.dir1 = c.MkDir() + c.Assert(os.MkdirAll(filepath.Join(s.dir1, "meta"), 0755), IsNil) + s.gadget1YamlPath = filepath.Join(s.dir1, "meta", "gadget.yaml") +} + +func (s *gadgetYamlVolumeAssignmentSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} + +func (s *gadgetYamlVolumeAssignmentSuite) TestReadGadgetYamlInvalidDevicePath(c *C) { + err := os.WriteFile(s.gadget0YamlPath, []byte(mockInvalidAssignmentGadgetYaml), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir0, coreMod) + c.Assert(err, ErrorMatches, `invalid volume-assignment for \"baz-device\": \"lun-1\": unsupported device path \"/dev/by-sda\", for now only paths under /dev/disk are valid`) +} + +func (s *gadgetYamlVolumeAssignmentSuite) TestReadGadgetYamlInvalidVolume(c *C) { + err := os.WriteFile(s.gadget0YamlPath, []byte(mockNonExistingAssignmentGadgetYaml), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir0, coreMod) + c.Assert(err, ErrorMatches, `invalid volume-assignment for \"baz-device\": volume \"lun-2\" is mentioned in assignment but has not been defined`) +} + +func (s *gadgetYamlVolumeAssignmentSuite) TestReadGadgetYamlNoAssignments(c *C) { + err := os.WriteFile(s.gadget0YamlPath, []byte(mockNoAssignmentGadgetYaml), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir0, coreMod) + c.Assert(err, ErrorMatches, `invalid volume-assignment for \"baz-device\": no assignments specified`) +} + +func (s *gadgetYamlVolumeAssignmentSuite) TestReadGadgetYamlHappy(c *C) { + err := os.WriteFile(s.gadget0YamlPath, []byte(mockVolumeAssignmentGadget0Yaml), 0644) + c.Assert(err, IsNil) + + ginfo, err := gadget.ReadInfo(s.dir0, coreMod) + c.Assert(err, IsNil) + expected := &gadget.Info{ + Volumes: map[string]*gadget.Volume{ + "lun-0": { + Name: "lun-0", + Schema: "mbr", + Bootloader: "u-boot", + ID: "0C", + Structure: []gadget.VolumeStructure{ + { + VolumeName: "lun-0", + Label: "system-boot", + Role: "system-boot", // implicit + Offset: asOffsetPtr(12345), + OffsetWrite: mustParseGadgetRelativeOffset(c, "777"), + Size: 88888, + MinSize: 88888, + Type: "0C", + Filesystem: "vfat", + Content: []gadget.VolumeContent{ + { + UnresolvedSource: "foo", + Target: "/", + Unpack: false, + }, + }, + }, + }, + }, + "lun-1": { + Name: "lun-1", + Schema: "mbr", + Structure: []gadget.VolumeStructure{ + { + VolumeName: "lun-1", + Name: "system-test", + Type: "bare", + Offset: asOffsetPtr(quantity.OffsetMiB), + Size: 16 * 1024 * 1024, + MinSize: 16 * 1024 * 1024, + Content: []gadget.VolumeContent{ + { + Image: "content.img", + }, + }, + YamlIndex: 0, + }, + }, + }, + }, + VolumeAssignments: []*gadget.VolumeAssignment{ + { + Name: "foo-device", + Assignments: map[string]*gadget.DeviceAssignment{ + "lun-0": { + Device: "/dev/disk/by-path/pci-0000:02:00.1-ata-5", + }, + }, + }, + { + Name: "bar-device", + Assignments: map[string]*gadget.DeviceAssignment{ + "lun-0": { + Device: "/dev/disk/by-diskseq/1", + }, + "lun-1": { + Device: "/dev/disk/by-id/wwm1234", + }, + }, + }, + }, + } + gadget.SetEnclosingVolumeInStructs(expected.Volumes) + + c.Check(ginfo, DeepEquals, expected) +} + +func (s *gadgetYamlVolumeAssignmentSuite) TestUpdateApplyNoMatchingAssignment(c *C) { + // Without any mocking of current devices - it wont find any matching + // for the fake assignments, and should report an error + c.Assert(os.WriteFile(s.gadget0YamlPath, []byte(mockVolumeAssignmentGadget0Yaml), 0644), IsNil) + c.Assert(os.WriteFile(s.gadget1YamlPath, []byte(mockVolumeAssignmentGadget0Yaml), 0644), IsNil) + + oldInfo, err := gadget.ReadInfo(s.dir0, coreMod) + c.Assert(err, IsNil) + oldRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(oldRootDir, "content.img"), 10*quantity.SizeMiB, nil) + oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir} + + newInfo, err := gadget.ReadInfo(s.dir1, coreMod) + c.Assert(err, IsNil) + + newRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(newRootDir, "content.img"), 11*quantity.SizeMiB, nil) + newData := gadget.GadgetData{Info: newInfo, RootDir: newRootDir} + + rollbackDir := c.MkDir() + + muo := &mockUpdateProcessObserver{} + updaterForStructureCalls := 0 + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, fromPs, ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + fmt.Println("update-for-structure", loc, ps, fromPs) + updaterForStructureCalls++ + mu := &mockUpdater{} + + return mu, nil + }) + defer restore() + + err = gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) + c.Check(err, ErrorMatches, `cannot update gadget assets: no matching volume-assignment for current device`) + c.Check(updaterForStructureCalls, Equals, 0) +} + +func (s *gadgetYamlVolumeAssignmentSuite) TestUpdateApplyAssignmentChanged(c *C) { + // Create matchings - but now let us say that the once provided in the gadget + // has changed in an update, this must fail + restore := gadget.MockFindVolumesMatchingDeviceAssignment(func(gi *gadget.Info) (map[string]gadget.DeviceVolume, error) { + return map[string]gadget.DeviceVolume{ + "lun-0": { + Device: gi.VolumeAssignments[1].Assignments["lun-0"].Device, + Volume: gi.Volumes["lun-0"], + }, + "lun-1": { + Device: gi.VolumeAssignments[1].Assignments["lun-1"].Device, + Volume: gi.Volumes["lun-1"], + }, + }, nil + }) + defer restore() + + c.Assert(os.WriteFile(s.gadget0YamlPath, []byte(mockVolumeAssignmentGadget0Yaml), 0644), IsNil) + c.Assert(os.WriteFile(s.gadget1YamlPath, []byte(mockChangedAssignmentGadget1Yaml), 0644), IsNil) + + oldInfo, err := gadget.ReadInfo(s.dir0, coreMod) + c.Assert(err, IsNil) + oldRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(oldRootDir, "content.img"), 10*quantity.SizeMiB, nil) + oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir} + + newInfo, err := gadget.ReadInfo(s.dir1, coreMod) + c.Assert(err, IsNil) + + newRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(newRootDir, "content.img"), 11*quantity.SizeMiB, nil) + newData := gadget.GadgetData{Info: newInfo, RootDir: newRootDir} + + rollbackDir := c.MkDir() + + muo := &mockUpdateProcessObserver{} + updaterForStructureCalls := 0 + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, fromPs, ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + fmt.Println("update-for-structure", loc, ps, fromPs) + updaterForStructureCalls++ + mu := &mockUpdater{} + + return mu, nil + }) + defer restore() + + err = gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) + c.Check(err, ErrorMatches, `cannot update gadget assets: device assignment is not identical for \"lun-1\"`) + c.Check(updaterForStructureCalls, Equals, 0) +} + +func (s *gadgetYamlVolumeAssignmentSuite) TestUpdateApplyHappy(c *C) { + c.Assert(os.WriteFile(s.gadget0YamlPath, []byte(mockVolumeAssignmentGadget0Yaml), 0644), IsNil) + c.Assert(os.WriteFile(s.gadget1YamlPath, []byte(mockVolumeAssignmentGadget0Yaml), 0644), IsNil) + + oldInfo, err := gadget.ReadInfo(s.dir0, coreMod) + c.Assert(err, IsNil) + oldRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(oldRootDir, "content.img"), 10*quantity.SizeMiB, nil) + oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir} + + newInfo, err := gadget.ReadInfo(s.dir1, coreMod) + c.Assert(err, IsNil) + // pretend we have an update + newInfo.Volumes["lun-1"].Structure[0].Update.Edition = 1 + + newRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(newRootDir, "content.img"), 11*quantity.SizeMiB, nil) + newData := gadget.GadgetData{Info: newInfo, RootDir: newRootDir} + + rollbackDir := c.MkDir() + + restore := gadget.MockFindVolumesMatchingDeviceAssignment(func(gi *gadget.Info) (map[string]gadget.DeviceVolume, error) { + return map[string]gadget.DeviceVolume{ + "lun-0": { + Device: "/dev/disk/by-diskseq/1", + Volume: gi.Volumes["lun-0"], + }, + "lun-1": { + Device: "/dev/disk/by-id/wwm1234", + Volume: gi.Volumes["lun-1"], + }, + }, nil + }) + defer restore() + + restore = gadget.MockVolumeStructureToLocationMap(func(gm gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + return map[string]map[int]gadget.StructureLocation{ + "lun-0": { + 0: { + Device: oldVolumes["lun-0"].Device, + Offset: quantity.OffsetMiB, + RootMountPoint: "/run/mnt/ubuntu-boot", + }, + }, + "lun-1": { + 0: { + Device: oldVolumes["lun-1"].Device, + Offset: quantity.OffsetMiB, + RootMountPoint: "/run/mnt/ubuntu-test", + }, + }, + }, map[string]map[int]*gadget.OnDiskStructure{ + "lun-0": gadget.OnDiskStructsFromGadget(oldVolumes["lun-0"].Volume), + "lun-1": gadget.OnDiskStructsFromGadget(oldVolumes["lun-1"].Volume), + }, nil + }) + defer restore() + + muo := &mockUpdateProcessObserver{} + updaterForStructureCalls := 0 + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, fromPs, ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + fmt.Println("update-for-structure", loc, ps, fromPs) + updaterForStructureCalls++ + mu := &mockUpdater{} + + return mu, nil + }) + defer restore() + + err = gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) + c.Check(err, IsNil) + c.Check(updaterForStructureCalls, Equals, 1) +} diff --git a/gadget/gadgettest/examples.go b/gadget/gadgettest/examples.go index 0e48ea618d5..151ff27d9a6 100644 --- a/gadget/gadgettest/examples.go +++ b/gadget/gadgettest/examples.go @@ -98,6 +98,85 @@ volumes: size: 256M ` +const RaspiSimplifiedMultiVolumeAssignmentYaml = ` +volumes: + pi: + bootloader: u-boot + schema: mbr + structure: + - filesystem: vfat + name: ubuntu-seed + role: system-seed + size: 1200M + type: 0C + - filesystem: vfat + name: ubuntu-boot + role: system-boot + size: 750M + type: 0C + - filesystem: ext4 + name: ubuntu-save + role: system-save + size: 16M + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + - filesystem: ext4 + name: ubuntu-data + role: system-data + size: 1500M + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + backup: + schema: mbr + structure: + - filesystem: ext4 + name: system-backup + size: 127M + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 +volume-assignments: + - name: raspi + assignment: + pi: + device: /dev/disk/by-path/pci-42:0 + backup: + device: /dev/disk/by-path/pci-43:0 +` + +const RaspiSimplifiedMultiVolumeAssignmentNoSaveYaml = ` +volumes: + pi: + bootloader: u-boot + schema: mbr + structure: + - filesystem: vfat + name: ubuntu-seed + role: system-seed + size: 1200M + type: 0C + - filesystem: vfat + name: ubuntu-boot + role: system-boot + size: 750M + type: 0C + - filesystem: ext4 + name: ubuntu-data + role: system-data + size: 1500M + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + backup: + schema: mbr + structure: + - filesystem: ext4 + name: system-backup + size: 127M + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 +volume-assignments: + - name: raspi + assignment: + pi: + device: /dev/disk/by-path/pci-42:0 + backup: + device: /dev/disk/by-path/pci-43:0 +` + var expPiSeedStructureTraits = gadget.DiskStructureDeviceTraits{ OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", OriginalKernelPath: "/dev/mmcblk0p1", @@ -265,6 +344,30 @@ var ExpectedRaspiUC18DiskVolumeDeviceTraits = gadget.DiskVolumeDeviceTraits{ }, } +var expPiBackupStructureTraits = gadget.DiskStructureDeviceTraits{ + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc1/mmc1:0001/block/mmcblk1/mmcblk1p1", + OriginalKernelPath: "/dev/mmcblk1p1", + PartitionUUID: "7c301cbe-01", + PartitionType: "83", + FilesystemUUID: "1cdd5826-e9de-4d27-83f7-20249e710591", + FilesystemLabel: "system-backup", + FilesystemType: "ext4", + Offset: quantity.OffsetMiB, + Size: 127 * quantity.SizeMiB, +} + +var ExpectedRaspiDiskVolumeDeviceBackupTraits = gadget.DiskVolumeDeviceTraits{ + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc1/mmc1:0001/block/mmcblk1", + OriginalKernelPath: "/dev/mmcblk1", + DiskID: "7c301cbe", + Size: 128 * quantity.SizeMiB, + SectorSize: 512, + Schema: "dos", + Structure: []gadget.DiskStructureDeviceTraits{ + expPiBackupStructureTraits, + }, +} + var mockSeedPartition = disks.Partition{ PartitionUUID: "7c301cbd-01", PartitionType: "0C", @@ -367,6 +470,33 @@ var ExpectedRaspiMockDiskMappingNoSave = &disks.MockDiskMapping{ }, } +var ExpectedRaspiMockBackupDiskMapping = &disks.MockDiskMapping{ + DevNode: "/dev/mmcblk1", + DevPath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc1/mmc1:0001/block/mmcblk1", + DevNum: "180:0", + DiskUsableSectorEnd: 128 * oneMeg / 512, + DiskSizeInBytes: 128 * oneMeg, + SectorSizeBytes: 512, + DiskSchema: "dos", + ID: "7c301cbe", + Structure: []disks.Partition{ + { + PartitionUUID: "7c301cbe-01", + PartitionType: "83", + FilesystemLabel: "system-backup", + FilesystemUUID: "1cdd5826-e9de-4d27-83f7-20249e710591", + FilesystemType: "ext4", + Major: 180, + Minor: 1, + KernelDeviceNode: "/dev/mmcblk1p1", + KernelDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc1/mmc1:0001/block/mmcblk1/mmcblk1p1", + DiskIndex: 1, + StartInBytes: oneMeg, + SizeInBytes: 127 * oneMeg, + }, + }, +} + // ExpectedLUKSEncryptedRaspiMockDiskMapping is like // ExpectedRaspiMockDiskMapping, but it uses the "-enc" suffix for the // filesystem labels and has crypto_LUKS as the filesystem types @@ -484,7 +614,7 @@ const ExpectedRaspiDiskVolumeDeviceTraitsJSON = ` "size": 32010928128, "sector-size": 512, "schema": "dos", - "structure-encryption": {}, + "structure-encryption": {}, "structure": [ { "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", @@ -548,7 +678,7 @@ const ExpectedRaspiDiskVolumeNoSaveDeviceTraitsJSON = ` "size": 32010928128, "sector-size": 512, "schema": "dos", - "structure-encryption": {}, + "structure-encryption": {}, "structure": [ { "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", @@ -600,14 +730,14 @@ const ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraitsJSON = ` "size": 32010928128, "sector-size": 512, "schema": "dos", - "structure-encryption": { - "ubuntu-data": { - "method": "LUKS" - }, - "ubuntu-save": { - "method": "LUKS" - } - }, + "structure-encryption": { + "ubuntu-data": { + "method": "LUKS" + }, + "ubuntu-save": { + "method": "LUKS" + } + }, "structure": [ { "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", @@ -662,6 +792,166 @@ const ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraitsJSON = ` } ` +const ExpectedRaspiDiskVolumeMultiVolumeDeviceTraitsJSON = ` +{ + "pi": { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", + "kernel-path": "/dev/mmcblk0", + "disk-id": "7c301cbd", + "size": 32010928128, + "sector-size": 512, + "schema": "dos", + "structure-encryption": {}, + "structure": [ + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", + "kernel-path": "/dev/mmcblk0p1", + "partition-uuid": "7c301cbd-01", + "partition-label": "", + "partition-type": "0C", + "filesystem-label": "ubuntu-seed", + "filesystem-uuid": "0E09-0822", + "filesystem-type": "vfat", + "offset": 1048576, + "size": 1258291200 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p2", + "kernel-path": "/dev/mmcblk0p2", + "partition-uuid": "7c301cbd-02", + "partition-label": "", + "partition-type": "0C", + "filesystem-label": "ubuntu-boot", + "filesystem-uuid": "23F9-881F", + "filesystem-type": "vfat", + "offset": 1259339776, + "size": 786432000 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p3", + "kernel-path": "/dev/mmcblk0p3", + "partition-uuid": "7c301cbd-03", + "partition-label": "", + "partition-type": "83", + "filesystem-label": "ubuntu-save", + "filesystem-uuid": "1cdd5826-e9de-4d27-83f7-20249e710590", + "filesystem-type": "ext4", + "offset": 2045771776, + "size": 16777216 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p4", + "kernel-path": "/dev/mmcblk0p4", + "partition-uuid": "7c301cbd-04", + "partition-label": "", + "partition-type": "83", + "filesystem-label": "ubuntu-data", + "filesystem-uuid": "d7f39661-1da0-48de-8967-ce41343d4345", + "filesystem-type": "ext4", + "offset": 2062548992, + "size": 29948379136 + } + ] + }, + "backup": { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc1/mmc1:0001/block/mmcblk1", + "kernel-path": "/dev/mmcblk1", + "disk-id": "7c301cbe", + "size": 134217728, + "sector-size": 512, + "schema": "dos", + "structure": [ + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc1/mmc1:0001/block/mmcblk1/mmcblk1p1", + "kernel-path": "/dev/mmcblk1p1", + "partition-uuid": "7c301cbe-01", + "partition-label": "", + "partition-type": "83", + "filesystem-label": "system-backup", + "filesystem-uuid": "1cdd5826-e9de-4d27-83f7-20249e710591", + "filesystem-type": "ext4", + "offset": 1048576, + "size": 133169152 + } + ] + } +} +` + +const ExpectedRaspiDiskVolumeMultiVolumeDeviceNoSaveTraitsJSON = ` +{ + "pi": { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", + "kernel-path": "/dev/mmcblk0", + "disk-id": "7c301cbd", + "size": 32010928128, + "sector-size": 512, + "schema": "dos", + "structure-encryption": {}, + "structure": [ + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", + "kernel-path": "/dev/mmcblk0p1", + "partition-uuid": "7c301cbd-01", + "partition-label": "", + "partition-type": "0C", + "filesystem-label": "ubuntu-seed", + "filesystem-uuid": "0E09-0822", + "filesystem-type": "vfat", + "offset": 1048576, + "size": 1258291200 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p2", + "kernel-path": "/dev/mmcblk0p2", + "partition-uuid": "7c301cbd-02", + "partition-label": "", + "partition-type": "0C", + "filesystem-label": "ubuntu-boot", + "filesystem-uuid": "23F9-881F", + "filesystem-type": "vfat", + "offset": 1259339776, + "size": 786432000 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p3", + "kernel-path": "/dev/mmcblk0p3", + "partition-uuid": "7c301cbd-03", + "partition-label": "", + "partition-type": "83", + "filesystem-label": "ubuntu-data", + "filesystem-uuid": "d7f39661-1da0-48de-8967-ce41343d4345", + "filesystem-type": "ext4", + "offset": 2045771776, + "size": 29965156352 + } + ] + }, + "backup": { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc1/mmc1:0001/block/mmcblk1", + "kernel-path": "/dev/mmcblk1", + "disk-id": "7c301cbe", + "size": 134217728, + "sector-size": 512, + "schema": "dos", + "structure": [ + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc1/mmc1:0001/block/mmcblk1/mmcblk1p1", + "kernel-path": "/dev/mmcblk1p1", + "partition-uuid": "7c301cbe-01", + "partition-label": "", + "partition-type": "83", + "filesystem-label": "system-backup", + "filesystem-uuid": "1cdd5826-e9de-4d27-83f7-20249e710591", + "filesystem-type": "ext4", + "offset": 1048576, + "size": 133169152 + } + ] + } +} +` + // // Mock devices // @@ -1389,77 +1679,77 @@ var VMSystemVolumeDeviceTraits = gadget.DiskVolumeDeviceTraits{ // like VMMultiVolumeUC20DiskTraitsJSON but without the foo volume const VMSingleVolumeUC20DiskTraitsJSON = ` { - "pc": { - "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda", - "kernel-path": "/dev/vda", - "disk-id": "f0eef013-a777-4a27-aaf0-dbb5cf68c2b6", - "size": 5368709120, - "sector-size": 512, - "schema": "gpt", - "structure": [ - { - "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda1", - "kernel-path": "/dev/vda1", - "partition-uuid": "420e5a20-b888-42e2-b7df-ced5cbf14517", - "partition-label": "BIOS\\x20Boot", - "partition-type": "21686148-6449-6E6F-744E-656564454649", - "filesystem-uuid": "", - "filesystem-label": "", - "filesystem-type": "", - "offset": 1048576, - "size": 1048576 - }, - { - "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda2", - "kernel-path": "/dev/vda2", - "partition-uuid": "4b436628-71ba-43f9-aa12-76b84fe32728", - "partition-label": "ubuntu-seed", - "partition-type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - "filesystem-uuid": "04D6-5AE2", - "filesystem-label": "ubuntu-seed", - "filesystem-type": "vfat", - "offset": 2097152, - "size": 1258291200 - }, - { - "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda3", - "kernel-path": "/dev/vda3", - "partition-uuid": "ade3ba65-7831-fd40-bbe2-e01c9774ed5b", - "partition-label": "ubuntu-boot", - "partition-type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", - "filesystem-uuid": "5b3e775a-407d-4af7-aa16-b92a8b7507e6", - "filesystem-label": "ubuntu-boot", - "filesystem-type": "ext4", - "offset": 1260388352, - "size": 786432000 - }, - { - "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda4", - "kernel-path": "/dev/vda4", - "partition-uuid": "f1d01870-194b-8a45-84c0-0d1c90e17d9d", - "partition-label": "ubuntu-save", - "partition-type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", - "filesystem-uuid": "6766b605-9cd5-47ae-bc48-807c778b9987", - "filesystem-label": "ubuntu-save", - "filesystem-type": "ext4", - "offset": 2046820352, - "size": 16777216 - }, - { - "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda5", - "kernel-path": "/dev/vda5", - "partition-uuid": "4994f0e5-1ead-1a4d-b696-2d8cb1fa980d", - "partition-label": "ubuntu-data", - "partition-type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", - "filesystem-uuid": "4e29a1e9-526d-48fc-a5c2-4f97e7e011e2", - "filesystem-label": "ubuntu-data", - "filesystem-type": "ext4", - "offset": 2063597568, - "size": 3305094656 - } - ] - } - } + "pc": { + "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda", + "kernel-path": "/dev/vda", + "disk-id": "f0eef013-a777-4a27-aaf0-dbb5cf68c2b6", + "size": 5368709120, + "sector-size": 512, + "schema": "gpt", + "structure": [ + { + "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda1", + "kernel-path": "/dev/vda1", + "partition-uuid": "420e5a20-b888-42e2-b7df-ced5cbf14517", + "partition-label": "BIOS\\x20Boot", + "partition-type": "21686148-6449-6E6F-744E-656564454649", + "filesystem-uuid": "", + "filesystem-label": "", + "filesystem-type": "", + "offset": 1048576, + "size": 1048576 + }, + { + "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda2", + "kernel-path": "/dev/vda2", + "partition-uuid": "4b436628-71ba-43f9-aa12-76b84fe32728", + "partition-label": "ubuntu-seed", + "partition-type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "filesystem-uuid": "04D6-5AE2", + "filesystem-label": "ubuntu-seed", + "filesystem-type": "vfat", + "offset": 2097152, + "size": 1258291200 + }, + { + "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda3", + "kernel-path": "/dev/vda3", + "partition-uuid": "ade3ba65-7831-fd40-bbe2-e01c9774ed5b", + "partition-label": "ubuntu-boot", + "partition-type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "filesystem-uuid": "5b3e775a-407d-4af7-aa16-b92a8b7507e6", + "filesystem-label": "ubuntu-boot", + "filesystem-type": "ext4", + "offset": 1260388352, + "size": 786432000 + }, + { + "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda4", + "kernel-path": "/dev/vda4", + "partition-uuid": "f1d01870-194b-8a45-84c0-0d1c90e17d9d", + "partition-label": "ubuntu-save", + "partition-type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "filesystem-uuid": "6766b605-9cd5-47ae-bc48-807c778b9987", + "filesystem-label": "ubuntu-save", + "filesystem-type": "ext4", + "offset": 2046820352, + "size": 16777216 + }, + { + "device-path": "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda5", + "kernel-path": "/dev/vda5", + "partition-uuid": "4994f0e5-1ead-1a4d-b696-2d8cb1fa980d", + "partition-label": "ubuntu-data", + "partition-type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "filesystem-uuid": "4e29a1e9-526d-48fc-a5c2-4f97e7e011e2", + "filesystem-label": "ubuntu-data", + "filesystem-type": "ext4", + "offset": 2063597568, + "size": 3305094656 + } + ] + } + } ` const VMMultiVolumeUC20DiskTraitsJSON = ` diff --git a/gadget/install/install.go b/gadget/install/install.go index 4603162ce25..30f50d72063 100644 --- a/gadget/install/install.go +++ b/gadget/install/install.go @@ -161,7 +161,7 @@ func maybeEncryptPartition(dgpair *gadget.OnDiskAndGadgetStructurePair, encrypti } // TODO probably we won't need to pass partDisp when we include storage in laidOut -func createFilesystem(part *gadget.OnDiskStructure, fsParams *mkfsParams, partDisp string, perfTimings timings.Measurer) error { +func createFilesystem(fsParams *mkfsParams, partDisp string, perfTimings timings.Measurer) error { var err error timings.Run(perfTimings, fmt.Sprintf("make-filesystem[%s]", partDisp), fmt.Sprintf("Create filesystem for %s", fsParams.Device), @@ -194,7 +194,6 @@ func installOnePartition(dgpair *gadget.OnDiskAndGadgetStructurePair, observer gadget.ContentObserver, perfTimings timings.Measurer, ) (fsDevice string, encryptionKey keys.EncryptionKey, err error) { // 1. Encrypt - diskPart := dgpair.DiskStructure vs := dgpair.GadgetStructure role := vs.Role fsParams, encryptionKey, err := maybeEncryptPartition(dgpair, encryptionType, sectorSize, perfTimings) @@ -204,7 +203,7 @@ func installOnePartition(dgpair *gadget.OnDiskAndGadgetStructurePair, fsDevice = fsParams.Device // 2. Create filesystem - if err := createFilesystem(diskPart, fsParams, role, perfTimings); err != nil { + if err := createFilesystem(fsParams, role, perfTimings); err != nil { return "", nil, err } @@ -227,37 +226,65 @@ func installOnePartition(dgpair *gadget.OnDiskAndGadgetStructurePair, // resolveBootDevice auto-detects the boot device // bootDevice forces the device. Device forcing is used for (spread) testing only. -func resolveBootDevice(bootDevice string, bootVol *gadget.Volume) (string, error) { +func resolveBootDevice(bootDevice string, bootVol gadget.DeviceVolume) (string, error) { if bootDevice != "" { return bootDevice, nil } + + if bootVol.Device != "" { + // disk is assigned for this volume + bootDisk, err := disks.DiskFromDeviceName(bootVol.Device) + if err != nil { + return "", err + } + logger.Noticef("volume %s has been assigned disk %s", bootVol.Volume.Name, bootDisk.KernelDeviceNode()) + return bootDisk.KernelDeviceNode(), nil + } + + // default behavior for unassigned volumes foundDisk, err := disks.DiskFromMountPoint("/run/mnt/ubuntu-seed", nil) if err != nil { logger.Noticef("Warning: cannot find disk from mounted seed: %s", err) } else { return foundDisk.KernelDeviceNode(), nil } - bootDevice, err = diskWithSystemSeed(bootVol) + bootDevice, err = diskWithSystemSeed(bootVol.Volume) if err != nil { return "", fmt.Errorf("cannot find device to create partitions on: %v", err) } - return bootDevice, nil } +func stripDeviceVolumes(volumes map[string]gadget.DeviceVolume) map[string]*gadget.Volume { + stripped := make(map[string]*gadget.Volume) + for name, vol := range volumes { + stripped[name] = vol.Volume + } + return stripped +} + +func findBootVolume(volumes map[string]gadget.DeviceVolume) (string, error) { + stripped := stripDeviceVolumes(volumes) + vol, err := gadget.FindBootVolume(stripped) + if err != nil { + return "", err + } + return vol.Name, err +} + // createPartitions creates partitions on the disk and returns the // volume name where partitions have been created, the on disk // structures after that, the laidout volumes, and the disk sector // size. -func createPartitions(model gadget.Model, info *gadget.Info, gadgetRoot, kernelRoot, bootDevice string, options Options, - perfTimings timings.Measurer) ( +func createPartitions(volumes map[string]gadget.DeviceVolume, gadgetRoot, bootDevice string, perfTimings timings.Measurer) ( bootVolGadgetName string, created []*gadget.OnDiskAndGadgetStructurePair, bootVolSectorSize quantity.Size, err error) { // Find boot volume - bootVol, err := gadget.FindBootVolume(info.Volumes) + bootVolumeName, err := findBootVolume(volumes) if err != nil { return "", nil, 0, err } + bootVol := volumes[bootVolumeName] bootDevice, err = resolveBootDevice(bootDevice, bootVol) if err != nil { return "", nil, 0, err @@ -270,12 +297,12 @@ func createPartitions(model gadget.Model, info *gadget.Info, gadgetRoot, kernelR // check if the current partition table is compatible with the gadget, // ignoring partitions added by the installer (will be removed later) - if _, err := gadget.EnsureVolumeCompatibility(bootVol, diskVolume, nil); err != nil { + if _, err := gadget.EnsureVolumeCompatibility(bootVol.Volume, diskVolume, nil); err != nil { return "", nil, 0, fmt.Errorf("gadget and system-boot device %v partition table not compatible: %v", bootDevice, err) } // remove partitions added during a previous install attempt - if err := removeCreatedPartitions(gadgetRoot, bootVol, diskVolume); err != nil { + if err := removeCreatedPartitions(gadgetRoot, bootVol.Volume, diskVolume); err != nil { return "", nil, 0, fmt.Errorf("cannot remove partitions from previous install: %v", err) } // at this point we removed any existing partition, nuke any @@ -292,13 +319,13 @@ func createPartitions(model gadget.Model, info *gadget.Info, gadgetRoot, kernelR opts := &CreateOptions{ GadgetRootDir: gadgetRoot, } - created, err = createMissingPartitions(diskVolume, bootVol, opts) + created, err = createMissingPartitions(diskVolume, bootVol.Volume, opts) }) if err != nil { return "", nil, 0, fmt.Errorf("cannot create the partitions: %v", err) } - bootVolGadgetName = bootVol.Name + bootVolGadgetName = bootVolumeName bootVolSectorSize = diskVolume.SectorSize return bootVolGadgetName, created, bootVolSectorSize, nil } @@ -324,6 +351,31 @@ func onDiskStructsSortedIdx(vss map[int]*gadget.OnDiskStructure) []int { return yamlIdxSl } +func determineDeviceVolumes(gi *gadget.Info) (map[string]gadget.DeviceVolume, error) { + if len(gi.VolumeAssignments) != 0 { + devVols, err := gadget.FindVolumesMatchingDeviceAssignment(gi) + if err != nil { + return nil, err + } + + // in case of volume assignments let us list those for + // information + logger.Noticef("volume assignments:") + for name, vol := range devVols { + logger.Noticef(" %s => %s", name, vol.Device) + } + return devVols, nil + } + + volumes := make(map[string]gadget.DeviceVolume) + for name, vol := range gi.Volumes { + volumes[name] = gadget.DeviceVolume{ + Volume: vol, + } + } + return volumes, nil +} + // Run creates partitions, encrypts them when expected, creates // filesystems, and finally writes content on them. func Run(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelSnapInfo, bootDevice string, options Options, observer gadget.ContentObserver, perfTimings timings.Measurer) (*InstalledSystemSideData, error) { @@ -343,10 +395,15 @@ func Run(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelSnapInfo, return nil, err } + volumes, err := determineDeviceVolumes(info) + if err != nil { + return nil, err + } + // Step 1: create partitions kernelRoot := kernelSnapInfo.MountPoint - bootVolGadgetName, created, bootVolSectorSize, err := - createPartitions(model, info, gadgetRoot, kernelRoot, bootDevice, options, perfTimings) + bootVolumeName, created, bootVolSectorSize, err := + createPartitions(volumes, gadgetRoot, bootDevice, perfTimings) if err != nil { return nil, err } @@ -408,16 +465,31 @@ func Run(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelSnapInfo, // after we have created all partitions, build up the mapping of volumes // to disk device traits and save it to disk for later usage - optsPerVol := map[string]*gadget.DiskVolumeValidationOptions{ - // this assumes that the encrypted partitions above are always only on the - // system-boot volume, this assumption may change - bootVolGadgetName: { - ExpectedStructureEncryption: partsEncrypted, - }, + optsPerVol := make(map[string]*gadget.DiskVolumeValidationOptions) + traitVolumes := make(map[string]*gadget.Volume) + for name, vol := range volumes { + traitVolumes[name] = vol.Volume + if name == bootVolumeName { + // this assumes that the encrypted partitions above are always only on the + // system-boot volume, this assumption may change + optsPerVol[name] = &gadget.DiskVolumeValidationOptions{ + Device: vol.Device, + ExpectedStructureEncryption: partsEncrypted, + } + } else { + optsPerVol[name] = &gadget.DiskVolumeValidationOptions{ + Device: vol.Device, + } + } + } + + // save the traits to ubuntu-data host and optionally to ubuntu-save if it exists + if err := saveStorageTraits(model, traitVolumes, optsPerVol, hasSavePartition); err != nil { + return nil, err } // save the traits to ubuntu-data host and optionally to ubuntu-save if it exists - if err := saveStorageTraits(model, info.Volumes, optsPerVol, hasSavePartition); err != nil { + if err := saveStorageTraits(model, stripDeviceVolumes(volumes), optsPerVol, hasSavePartition); err != nil { return nil, err } @@ -701,10 +773,18 @@ func FactoryReset(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelS if err != nil { return nil, err } - bootVol, err := gadget.FindBootVolume(info.Volumes) + + volumes, err := determineDeviceVolumes(info) + if err != nil { + return nil, err + } + + bootVolumeName, err := findBootVolume(volumes) if err != nil { return nil, err } + + bootVol := volumes[bootVolumeName] bootDevice, err = resolveBootDevice(bootDevice, bootVol) if err != nil { return nil, err @@ -728,7 +808,7 @@ func FactoryReset(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelS // XXX what about ICE? return nil, fmt.Errorf("unsupported encryption type %v", options.EncryptionType) } - for _, volStruct := range bootVol.Structure { + for _, volStruct := range bootVol.Volume.Structure { if !roleNeedsEncryption(volStruct.Role) { continue } @@ -737,7 +817,7 @@ func FactoryReset(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelS } // factory reset is done on a system that was once installed, so this // should be always successful unless the partition table has changed - yamlIdxToOnDistStruct, err := gadget.EnsureVolumeCompatibility(bootVol, diskLayout, volCompatOps) + yamlIdxToOnDistStruct, err := gadget.EnsureVolumeCompatibility(bootVol.Volume, diskLayout, volCompatOps) if err != nil { return nil, fmt.Errorf("gadget and system-boot device %v partition table not compatible: %v", bootDevice, err) } @@ -752,7 +832,7 @@ func FactoryReset(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelS rolesToReset := []string{gadget.SystemBoot, gadget.SystemData} for _, yamlIdx := range onDiskStructsSortedIdx(yamlIdxToOnDistStruct) { onDiskStruct := yamlIdxToOnDistStruct[yamlIdx] - vs := bootVol.StructFromYamlIndex(yamlIdx) + vs := bootVol.Volume.StructFromYamlIndex(yamlIdx) if vs == nil { continue } @@ -796,18 +876,28 @@ func FactoryReset(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelS // after we have created all partitions, build up the mapping of volumes // to disk device traits and save it to disk for later usage - optsPerVol := map[string]*gadget.DiskVolumeValidationOptions{ - // this assumes that the encrypted partitions above are always only on the - // system-boot volume, this assumption may change - bootVol.Name: { - ExpectedStructureEncryption: volCompatOps.ExpectedStructureEncryption, - }, + optsPerVol := make(map[string]*gadget.DiskVolumeValidationOptions) + traitVolumes := make(map[string]*gadget.Volume) + for name, vol := range volumes { + traitVolumes[name] = vol.Volume + if name == bootVolumeName { + // this assumes that the encrypted partitions above are always only on the + // system-boot volume, this assumption may change + optsPerVol[name] = &gadget.DiskVolumeValidationOptions{ + Device: vol.Device, + ExpectedStructureEncryption: volCompatOps.ExpectedStructureEncryption, + } + } else { + optsPerVol[name] = &gadget.DiskVolumeValidationOptions{ + Device: vol.Device, + } + } } + // save the traits to ubuntu-data host and optionally to ubuntu-save if it exists - if err := saveStorageTraits(model, info.Volumes, optsPerVol, hasSavePartition); err != nil { + if err := saveStorageTraits(model, traitVolumes, optsPerVol, hasSavePartition); err != nil { return nil, err } - return &InstalledSystemSideData{ KeyForRole: keyForRole, DeviceForRole: deviceForRole, diff --git a/gadget/install/install_test.go b/gadget/install/install_test.go index 5e2bd6ea3c0..b74d9742239 100644 --- a/gadget/install/install_test.go +++ b/gadget/install/install_test.go @@ -79,41 +79,194 @@ func (s *installSuite) TestInstallRunError(c *C) { func (s *installSuite) TestInstallRunSimpleHappy(c *C) { s.testInstall(c, installOpts{ - encryption: false, + gadgetYaml: gadgettest.RaspiSimplifiedYaml, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskInstallModeMapping, + }, + disks: defaultDiskSetup, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeDeviceTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + }, }) } func (s *installSuite) TestInstallRunSimpleHappyFromMountPoint(c *C) { s.testInstall(c, installOpts{ - encryption: false, - fromSeed: true, + gadgetYaml: gadgettest.RaspiSimplifiedYaml, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskInstallModeMapping, + }, + disks: defaultDiskSetup, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeDeviceTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + }, + fromSeed: true, }) } func (s *installSuite) TestInstallRunEncryptedLUKS(c *C) { s.testInstall(c, installOpts{ + gadgetYaml: gadgettest.RaspiSimplifiedYaml, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskInstallModeMapping, + }, + disks: defaultDiskSetup, + traitsJSON: gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraits, + }, encryption: true, }) } func (s *installSuite) TestInstallRunExistingPartitions(c *C) { s.testInstall(c, installOpts{ - encryption: false, + gadgetYaml: gadgettest.RaspiSimplifiedYaml, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskMapping, + }, + disks: defaultDiskSetup, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeDeviceTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + }, existingParts: true, }) } func (s *installSuite) TestInstallRunEncryptionExistingPartitions(c *C) { s.testInstall(c, installOpts{ + gadgetYaml: gadgettest.RaspiSimplifiedYaml, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedLUKSEncryptedRaspiMockDiskMapping, + }, + disks: defaultDiskSetup, + traitsJSON: gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraits, + }, encryption: true, existingParts: true, }) } +func (s *installSuite) TestInstallRunVolumeAssignmentHappy(c *C) { + s.testInstall(c, installOpts{ + gadgetYaml: gadgettest.RaspiSimplifiedMultiVolumeAssignmentYaml, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskInstallModeMapping, + "mmcblk1": gadgettest.ExpectedRaspiMockBackupDiskMapping, + }, + disks: map[string]*installSetupDisk{ + "mmcblk0": { + path: "42:0", + parts: map[string]string{ + "mmcblk0p1": "ubuntu-seed", + }, + }, + "mmcblk1": { + path: "43:0", + parts: map[string]string{ + "mmcblk1p1": "system-backup", + }, + }, + }, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeMultiVolumeDeviceTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + "backup": gadgettest.ExpectedRaspiDiskVolumeDeviceBackupTraits, + }, + volumeAssignments: true, + }) +} + +func (s *installSuite) TestInstallRunVolumeAssignmentFromSeedHappy(c *C) { + s.testInstall(c, installOpts{ + gadgetYaml: gadgettest.RaspiSimplifiedMultiVolumeAssignmentYaml, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskInstallModeMapping, + "mmcblk1": gadgettest.ExpectedRaspiMockBackupDiskMapping, + }, + disks: map[string]*installSetupDisk{ + "mmcblk0": { + path: "42:0", + parts: map[string]string{ + "mmcblk0p1": "ubuntu-seed", + }, + }, + "mmcblk1": { + path: "43:0", + parts: map[string]string{ + "mmcblk1p1": "system-backup", + }, + }, + }, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeMultiVolumeDeviceTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + "backup": gadgettest.ExpectedRaspiDiskVolumeDeviceBackupTraits, + }, + volumeAssignments: true, + fromSeed: true, + }) +} + +func (s *installSuite) TestInstallRunVolumeAssignmentExistingPartsHappy(c *C) { + s.testInstall(c, installOpts{ + gadgetYaml: gadgettest.RaspiSimplifiedMultiVolumeAssignmentYaml, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskMapping, + "mmcblk1": gadgettest.ExpectedRaspiMockBackupDiskMapping, + }, + disks: map[string]*installSetupDisk{ + "mmcblk0": { + path: "42:0", + parts: map[string]string{ + "mmcblk0p1": "ubuntu-seed", + }, + }, + "mmcblk1": { + path: "43:0", + parts: map[string]string{ + "mmcblk1p1": "system-backup", + }, + }, + }, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeMultiVolumeDeviceTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + "backup": gadgettest.ExpectedRaspiDiskVolumeDeviceBackupTraits, + }, + volumeAssignments: true, + existingParts: true, + }) +} + +var defaultDiskSetup = map[string]*installSetupDisk{ + "mmcblk0": { + parts: map[string]string{ + "mmcblk0p1": "ubuntu-seed", + }, + }, +} + +type installSetupDisk struct { + path string + parts map[string]string +} + type installOpts struct { - encryption bool - existingParts bool - fromSeed bool + gadgetYaml string + diskMappings map[string]*disks.MockDiskMapping + disks map[string]*installSetupDisk + traitsJSON string + traits map[string]gadget.DiskVolumeDeviceTraits + encryption bool + existingParts bool + fromSeed bool + volumeAssignments bool } func (s *installSuite) testInstall(c *C, opts installOpts) { @@ -129,28 +282,29 @@ func (s *installSuite) testInstall(c *C, opts installOpts) { HasModes: true, } - s.setupMockUdevSymlinks(c, "mmcblk0p1") + c.Assert(len(opts.diskMappings) > 0, Equals, true, Commentf("mock disk must be provided")) + c.Assert(len(opts.disks) > 0, Equals, true, Commentf("mock disk must be provided")) - // mock single partition mapping to a disk with only ubuntu-seed partition - initialDisk := gadgettest.ExpectedRaspiMockDiskInstallModeMapping - if opts.existingParts { - // unless we are asked to mock with a full existing disk - if opts.encryption { - initialDisk = gadgettest.ExpectedLUKSEncryptedRaspiMockDiskMapping - } else { - initialDisk = gadgettest.ExpectedRaspiMockDiskMapping - } - } - m := map[string]*disks.MockDiskMapping{ - filepath.Join(s.dir, "/dev/mmcblk0p1"): initialDisk, + for name, dopts := range opts.disks { + s.setupMockUdevSymlinks(c, mockUdevDeviceSetup{ + name: name, + path: dopts.path, + parts: dopts.parts, + }) } - restore := disks.MockPartitionDeviceNodeToDiskMapping(m) + nodeToDiskMappings := make(map[string]*disks.MockDiskMapping, len(opts.diskMappings)) + for name, mappings := range opts.diskMappings { + nodeToDiskMappings[filepath.Join(s.dir, fmt.Sprintf("/dev/%sp1", name))] = mappings + } + restore := disks.MockPartitionDeviceNodeToDiskMapping(nodeToDiskMappings) defer restore() - restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ - "/dev/mmcblk0": initialDisk, - }) + deviceToDiskMappings := make(map[string]*disks.MockDiskMapping, len(opts.diskMappings)) + for name, mappings := range opts.diskMappings { + deviceToDiskMappings[fmt.Sprintf("/dev/%s", name)] = mappings + } + restore = disks.MockDeviceNameToDiskMapping(deviceToDiskMappings) defer restore() mockSfdisk := testutil.MockCommand(c, "sfdisk", "") @@ -168,12 +322,21 @@ elif [ "$*" = "info --query property --name /dev/block/42:0" ]; then echo "DEVTYPE=disk" echo "ID_PART_TABLE_UUID=some-gpt-uuid" echo "ID_PART_TABLE_TYPE=GPT" +elif [ "$*" = "info --query property --name /dev/mmcblk1p1" ]; then + echo "ID_PART_ENTRY_DISK=43:0" +elif [ "$*" = "info --query property --name /dev/block/43:0" ]; then + echo "DEVNAME=/dev/mmcblk1" + echo "DEVPATH=/devices/virtual/mmcblk1" + echo "DEVTYPE=disk" + echo "ID_PART_TABLE_UUID=another-gpt-uuid" + echo "ID_PART_TABLE_TYPE=GPT" fi `) defer mockUdevadm.Restore() if opts.fromSeed { restoreMountInfo := osutil.MockMountInfo(`130 30 42:1 / /run/mnt/ubuntu-seed rw,relatime shared:54 - vfat /dev/mmcblk0p1 rw +130 30 43:1 / /run/mnt/system-backup rw,relatime shared:54 - ext4 /dev/mmcblk1p1 rw `) defer restoreMountInfo() } else { @@ -197,21 +360,28 @@ fi // device mapping so that later on in the function when we query for // device traits, etc. we see the "full" disk - newDisk := gadgettest.ExpectedRaspiMockDiskMapping + n := make(map[string]*disks.MockDiskMapping, len(opts.diskMappings)) if opts.encryption { - newDisk = gadgettest.ExpectedLUKSEncryptedRaspiMockDiskMapping - } - - m := map[string]*disks.MockDiskMapping{ - filepath.Join(s.dir, "/dev/mmcblk0p1"): newDisk, + n[filepath.Join(s.dir, "/dev/mmcblk0p1")] = gadgettest.ExpectedLUKSEncryptedRaspiMockDiskMapping + } else if opts.volumeAssignments { + n[filepath.Join(s.dir, "/dev/mmcblk0p1")] = gadgettest.ExpectedRaspiMockDiskMapping + n[filepath.Join(s.dir, "/dev/mmcblk1p1")] = gadgettest.ExpectedRaspiMockBackupDiskMapping + } else { + n[filepath.Join(s.dir, "/dev/mmcblk0p1")] = gadgettest.ExpectedRaspiMockDiskMapping } - - restore := disks.MockPartitionDeviceNodeToDiskMapping(m) + restore := disks.MockPartitionDeviceNodeToDiskMapping(n) addCleanup(restore) - restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ - "/dev/mmcblk0": newDisk, - }) + m := make(map[string]*disks.MockDiskMapping, len(opts.diskMappings)) + if opts.encryption { + m["/dev/mmcblk0"] = gadgettest.ExpectedLUKSEncryptedRaspiMockDiskMapping + } else if opts.volumeAssignments { + m["/dev/mmcblk0"] = gadgettest.ExpectedRaspiMockDiskMapping + m["/dev/mmcblk1"] = gadgettest.ExpectedRaspiMockBackupDiskMapping + } else { + m["/dev/mmcblk0"] = gadgettest.ExpectedRaspiMockDiskMapping + } + restore = disks.MockDeviceNameToDiskMapping(m) addCleanup(restore) return nil @@ -331,7 +501,7 @@ fi }) defer restore() - gadgetRoot, err := gadgettest.WriteGadgetYaml(c.MkDir(), gadgettest.RaspiSimplifiedYaml) + gadgetRoot, err := gadgettest.WriteGadgetYaml(c.MkDir(), opts.gadgetYaml) c.Assert(err, IsNil) var saveEncryptionKey, dataEncryptionKey keys.EncryptionKey @@ -412,7 +582,9 @@ fi udevmadmCalls := [][]string{} - if opts.fromSeed { + // When volumes are assigned it does not query udevadm, but instead just + // verifies disks exists where their assignments are + if opts.fromSeed && !opts.volumeAssignments { udevmadmCalls = append(udevmadmCalls, []string{"udevadm", "info", "--query", "property", "--name", "/dev/mmcblk0p1"}) udevmadmCalls = append(udevmadmCalls, []string{"udevadm", "info", "--query", "property", "--name", "/dev/block/42:0"}) } @@ -451,13 +623,7 @@ fi // check the disk-mapping.json that was written as well mappingOnData, err := gadget.LoadDiskVolumesDeviceTraits(dirs.SnapDeviceDirUnder(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data"))) c.Assert(err, IsNil) - expMapping := gadgettest.ExpectedRaspiDiskVolumeDeviceTraits - if opts.encryption { - expMapping = gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraits - } - c.Assert(mappingOnData, DeepEquals, map[string]gadget.DiskVolumeDeviceTraits{ - "pi": expMapping, - }) + c.Assert(mappingOnData, DeepEquals, opts.traits) // we get the same thing on ubuntu-save dataFile := filepath.Join(dirs.SnapDeviceDirUnder(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data")), "disk-mapping.json") @@ -467,11 +633,7 @@ fi // also for extra paranoia, compare the object we load with manually loading // the static JSON to make sure they compare the same, this ensures that // the JSON that is written always stays compatible - jsonBytes := []byte(gadgettest.ExpectedRaspiDiskVolumeDeviceTraitsJSON) - if opts.encryption { - jsonBytes = []byte(gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraitsJSON) - } - + jsonBytes := []byte(opts.traitsJSON) err = os.WriteFile(dataFile, jsonBytes, 0644) c.Assert(err, IsNil) @@ -525,19 +687,39 @@ const mockUC20GadgetYaml = `volumes: size: 750M ` -func (s *installSuite) setupMockUdevSymlinks(c *C, devName string) { - err := os.MkdirAll(filepath.Join(s.dir, "/dev/disk/by-partlabel"), 0755) - c.Assert(err, IsNil) +type mockUdevDeviceSetup struct { + name string + parts map[string]string + path string +} - err = os.WriteFile(filepath.Join(s.dir, "/dev/"+devName), nil, 0644) - c.Assert(err, IsNil) - err = os.Symlink("../../"+devName, filepath.Join(s.dir, "/dev/disk/by-partlabel/ubuntu-seed")) - c.Assert(err, IsNil) +func (s *installSuite) setupMockUdevSymlinks(c *C, opts mockUdevDeviceSetup) { + c.Assert(os.MkdirAll(filepath.Join(s.dir, "/dev/disk/by-partlabel"), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(s.dir, "/dev/disk/by-path"), 0755), IsNil) + + // mock the device node + c.Assert(os.WriteFile(filepath.Join(s.dir, "/dev/"+opts.name), nil, 0644), IsNil) + + // mock the by-path node + if opts.path != "" { + c.Assert(os.Symlink("../../"+opts.name, filepath.Join(s.dir, fmt.Sprintf("/dev/disk/by-path/pci-%s", opts.path))), IsNil) + } + + // mock partitions + for name, label := range opts.parts { + c.Assert(os.WriteFile(filepath.Join(s.dir, "/dev/"+name), nil, 0644), IsNil) + c.Assert(os.Symlink("../../"+name, filepath.Join(s.dir, fmt.Sprintf("/dev/disk/by-partlabel/%s", label))), IsNil) + } } func (s *installSuite) TestDeviceFromRoleHappy(c *C) { - s.setupMockUdevSymlinks(c, "fakedevice0p1") + s.setupMockUdevSymlinks(c, mockUdevDeviceSetup{ + name: "fakedevice0", + parts: map[string]string{ + "fakedevice0p1": "ubuntu-seed", + }, + }) m := map[string]*disks.MockDiskMapping{ filepath.Join(s.dir, "/dev/fakedevice0p1"): { @@ -568,7 +750,12 @@ func (s *installSuite) TestDeviceFromRoleErrorNoMatchingSysfs(c *C) { } func (s *installSuite) TestDeviceFromRoleErrorNoRole(c *C) { - s.setupMockUdevSymlinks(c, "fakedevice0p1") + s.setupMockUdevSymlinks(c, mockUdevDeviceSetup{ + name: "fakedevice0", + parts: map[string]string{ + "fakedevice0p1": "ubuntu-seed", + }, + }) lv, err := gadgettest.LayoutFromYaml(c.MkDir(), mockGadgetYaml, nil) c.Assert(err, IsNil) @@ -577,14 +764,16 @@ func (s *installSuite) TestDeviceFromRoleErrorNoRole(c *C) { } type factoryResetOpts struct { - encryption bool - err string - disk *disks.MockDiskMapping - noSave bool - gadgetYaml string - traitsJSON string - traits gadget.DiskVolumeDeviceTraits - fromSeed bool + encryption bool + err string + diskMappings map[string]*disks.MockDiskMapping + disks map[string]*installSetupDisk + noSave bool + gadgetYaml string + traitsJSON string + traits map[string]gadget.DiskVolumeDeviceTraits + fromSeed bool + volumeAssignments bool } func (s *installSuite) testFactoryReset(c *C, opts factoryResetOpts) { @@ -596,18 +785,29 @@ func (s *installSuite) testFactoryReset(c *C, opts factoryResetOpts) { c.Fatalf("unsupported test scenario, cannot use encryption without ubuntu-save") } - s.setupMockUdevSymlinks(c, "mmcblk0p1") + c.Assert(len(opts.diskMappings) > 0, Equals, true, Commentf("mock disk must be provided")) + c.Assert(len(opts.disks) > 0, Equals, true, Commentf("mock disk must be provided")) - // mock single partition mapping to a disk with only ubuntu-seed partition - c.Assert(opts.disk, NotNil, Commentf("mock disk must be provided")) - restore := disks.MockPartitionDeviceNodeToDiskMapping(map[string]*disks.MockDiskMapping{ - filepath.Join(s.dir, "/dev/mmcblk0p1"): opts.disk, - }) + for name, dopts := range opts.disks { + s.setupMockUdevSymlinks(c, mockUdevDeviceSetup{ + name: name, + path: dopts.path, + parts: dopts.parts, + }) + } + + nodeToDiskMappings := make(map[string]*disks.MockDiskMapping, len(opts.diskMappings)) + for name, mappings := range opts.diskMappings { + nodeToDiskMappings[filepath.Join(s.dir, fmt.Sprintf("/dev/%sp1", name))] = mappings + } + restore := disks.MockPartitionDeviceNodeToDiskMapping(nodeToDiskMappings) defer restore() - restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ - "/dev/mmcblk0": opts.disk, - }) + deviceToDiskMappings := make(map[string]*disks.MockDiskMapping, len(opts.diskMappings)) + for name, mappings := range opts.diskMappings { + deviceToDiskMappings[fmt.Sprintf("/dev/%s", name)] = mappings + } + restore = disks.MockDeviceNameToDiskMapping(deviceToDiskMappings) defer restore() mockSfdisk := testutil.MockCommand(c, "sfdisk", "") @@ -625,12 +825,21 @@ elif [ "$*" = "info --query property --name /dev/block/42:0" ]; then echo "DEVTYPE=disk" echo "ID_PART_TABLE_UUID=some-gpt-uuid" echo "ID_PART_TABLE_TYPE=GPT" +elif [ "$*" = "info --query property --name /dev/mmcblk1p1" ]; then + echo "ID_PART_ENTRY_DISK=43:0" +elif [ "$*" = "info --query property --name /dev/block/43:0" ]; then + echo "DEVNAME=/dev/mmcblk1" + echo "DEVPATH=/devices/virtual/mmcblk1" + echo "DEVTYPE=disk" + echo "ID_PART_TABLE_UUID=another-gpt-uuid" + echo "ID_PART_TABLE_TYPE=GPT" fi `) defer mockUdevadm.Restore() if opts.fromSeed { restoreMountInfo := osutil.MockMountInfo(`130 30 42:1 / /run/mnt/ubuntu-seed rw,relatime shared:54 - vfat /dev/mmcblk0p1 rw +130 30 43:1 / /run/mnt/system-backup rw,relatime shared:54 - ext4 /dev/mmcblk1p1 rw `) defer restoreMountInfo() } else { @@ -812,7 +1021,9 @@ fi udevmadmCalls := [][]string{} - if opts.fromSeed { + // When using volume-assignments for the device, `resolveBootDevice` will not call + // disks.DiskFromMountPoint and these calls wont happen + if opts.fromSeed && !opts.volumeAssignments { udevmadmCalls = append(udevmadmCalls, []string{"udevadm", "info", "--query", "property", "--name", "/dev/mmcblk0p1"}) udevmadmCalls = append(udevmadmCalls, []string{"udevadm", "info", "--query", "property", "--name", "/dev/block/42:0"}) } @@ -828,9 +1039,7 @@ fi // check the disk-mapping.json that was written as well mappingOnData, err := gadget.LoadDiskVolumesDeviceTraits(dirs.SnapDeviceDirUnder(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data"))) c.Assert(err, IsNil) - c.Assert(mappingOnData, DeepEquals, map[string]gadget.DiskVolumeDeviceTraits{ - "pi": opts.traits, - }) + c.Assert(mappingOnData, DeepEquals, opts.traits) // we get the same thing on ubuntu-save dataFile := filepath.Join(dirs.SnapDeviceDirUnder(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data")), "disk-mapping.json") @@ -854,26 +1063,39 @@ fi func (s *installSuite) TestFactoryResetHappyFromSeed(c *C) { s.testFactoryReset(c, factoryResetOpts{ - disk: gadgettest.ExpectedRaspiMockDiskMapping, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskMapping, + }, + disks: defaultDiskSetup, gadgetYaml: gadgettest.RaspiSimplifiedYaml, traitsJSON: gadgettest.ExpectedRaspiDiskVolumeDeviceTraitsJSON, - traits: gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, - fromSeed: true, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + }, + fromSeed: true, }) } func (s *installSuite) TestFactoryResetHappyWithExisting(c *C) { s.testFactoryReset(c, factoryResetOpts{ - disk: gadgettest.ExpectedRaspiMockDiskMapping, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskMapping, + }, + disks: defaultDiskSetup, gadgetYaml: gadgettest.RaspiSimplifiedYaml, traitsJSON: gadgettest.ExpectedRaspiDiskVolumeDeviceTraitsJSON, - traits: gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + }, }) } func (s *installSuite) TestFactoryResetHappyWithoutDataAndBoot(c *C) { s.testFactoryReset(c, factoryResetOpts{ - disk: gadgettest.ExpectedRaspiMockDiskInstallModeMapping, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskInstallModeMapping, + }, + disks: defaultDiskSetup, gadgetYaml: gadgettest.RaspiSimplifiedYaml, err: "gadget and system-boot device /dev/mmcblk0 partition table not compatible: cannot find .*ubuntu-boot.*", }) @@ -881,21 +1103,94 @@ func (s *installSuite) TestFactoryResetHappyWithoutDataAndBoot(c *C) { func (s *installSuite) TestFactoryResetHappyWithoutSave(c *C) { s.testFactoryReset(c, factoryResetOpts{ - disk: gadgettest.ExpectedRaspiMockDiskMappingNoSave, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskMappingNoSave, + }, + disks: defaultDiskSetup, gadgetYaml: gadgettest.RaspiSimplifiedNoSaveYaml, noSave: true, traitsJSON: gadgettest.ExpectedRaspiDiskVolumeNoSaveDeviceTraitsJSON, - traits: gadgettest.ExpectedRaspiDiskVolumeDeviceNoSaveTraits, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceNoSaveTraits, + }, }) } func (s *installSuite) TestFactoryResetHappyEncrypted(c *C) { s.testFactoryReset(c, factoryResetOpts{ encryption: true, - disk: gadgettest.ExpectedLUKSEncryptedRaspiMockDiskMapping, + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedLUKSEncryptedRaspiMockDiskMapping, + }, + disks: defaultDiskSetup, gadgetYaml: gadgettest.RaspiSimplifiedYaml, traitsJSON: gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraitsJSON, - traits: gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraits, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraits, + }, + }) +} + +func (s *installSuite) TestFactoryResetHappyWithDeviceAssignmentFromSeed(c *C) { + s.testFactoryReset(c, factoryResetOpts{ + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskMappingNoSave, + "mmcblk1": gadgettest.ExpectedRaspiMockBackupDiskMapping, + }, + disks: map[string]*installSetupDisk{ + "mmcblk0": { + path: "42:0", + parts: map[string]string{ + "mmcblk0p1": "ubuntu-seed", + }, + }, + "mmcblk1": { + path: "43:0", + parts: map[string]string{ + "mmcblk1p1": "system-backup", + }, + }, + }, + gadgetYaml: gadgettest.RaspiSimplifiedMultiVolumeAssignmentNoSaveYaml, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeMultiVolumeDeviceNoSaveTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceNoSaveTraits, + "backup": gadgettest.ExpectedRaspiDiskVolumeDeviceBackupTraits, + }, + fromSeed: true, + noSave: true, + volumeAssignments: true, + }) +} + +func (s *installSuite) TestFactoryResetHappyWithDeviceAssignmentFromExisting(c *C) { + s.testFactoryReset(c, factoryResetOpts{ + diskMappings: map[string]*disks.MockDiskMapping{ + "mmcblk0": gadgettest.ExpectedRaspiMockDiskMappingNoSave, + "mmcblk1": gadgettest.ExpectedRaspiMockBackupDiskMapping, + }, + disks: map[string]*installSetupDisk{ + "mmcblk0": { + path: "42:0", + parts: map[string]string{ + "mmcblk0p1": "ubuntu-seed", + }, + }, + "mmcblk1": { + path: "43:0", + parts: map[string]string{ + "mmcblk1p1": "system-backup", + }, + }, + }, + gadgetYaml: gadgettest.RaspiSimplifiedMultiVolumeAssignmentNoSaveYaml, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeMultiVolumeDeviceNoSaveTraitsJSON, + traits: map[string]gadget.DiskVolumeDeviceTraits{ + "pi": gadgettest.ExpectedRaspiDiskVolumeDeviceNoSaveTraits, + "backup": gadgettest.ExpectedRaspiDiskVolumeDeviceBackupTraits, + }, + noSave: true, + volumeAssignments: true, }) } diff --git a/gadget/update.go b/gadget/update.go index 0f70e265f6c..54e7209a74f 100644 --- a/gadget/update.go +++ b/gadget/update.go @@ -638,6 +638,8 @@ const ( // options provided to EnsureVolumeCompatibility via // EnsureVolumeCompatibilityOptions. type DiskVolumeValidationOptions struct { + // Device allows to specify a specific disk path for the volume + Device string // AllowImplicitSystemData has the same meaning as the eponymously named // field in VolumeCompatibilityOptions. AllowImplicitSystemData bool @@ -818,12 +820,12 @@ var errSkipUpdateProceedRefresh = errors.New("cannot identify disk for gadget as // 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) { +func buildNewVolumeToDeviceMapping(mod Model, oldVolumes, newVolumes map[string]DeviceVolume) (map[string]DiskVolumeDeviceTraits, error) { var likelySystemBootVolume string isPreUC20 := (mod.Grade() == asserts.ModelGradeUnset) - if len(old.Info.Volumes) == 1 { + if len(oldVolumes) == 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 @@ -831,7 +833,7 @@ func buildNewVolumeToDeviceMapping(mod Model, old GadgetData, vols map[string]*V // at all // then we only have one volume to be concerned with - for volName := range old.Info.Volumes { + for volName := range oldVolumes { likelySystemBootVolume = volName } } else { @@ -839,8 +841,8 @@ func buildNewVolumeToDeviceMapping(mod Model, old GadgetData, vols map[string]*V // 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 { + for volName, vol := range oldVolumes { + for _, structure := range vol.Volume.Structure { if structure.Role == SystemBoot { // this is the volume likelySystemBootVolume = volName @@ -866,15 +868,23 @@ func buildNewVolumeToDeviceMapping(mod Model, old GadgetData, vols map[string]*V return nil, fmt.Errorf("cannot find any volume with system-boot, gadget is broken") } - vol := vols[likelySystemBootVolume] + vol := newVolumes[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]) + for i := range vol.Volume.Structure { + var structureDevice string + var err error + if vol.Device != "" { + // If a specific device is assigned for the volume, then we must + // use that, so we verify that the device is valid. + structureDevice, err = ResolveDeviceForStructure(vol.Device) + } else { + // 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.Volume.Structure[i]) + } if err == ErrDeviceNotFound { continue } @@ -937,7 +947,7 @@ func buildNewVolumeToDeviceMapping(mod Model, old GadgetData, vols map[string]*V } } - traits, err := DiskTraitsFromDeviceAndValidate(vol, dev, validateOpts) + traits, err := DiskTraitsFromDeviceAndValidate(vol.Volume, 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) @@ -976,8 +986,8 @@ type StructureLocation struct { // buildVolumeStructureToLocation builds a map of gadget volumes to // locations and to matched disk structures. func buildVolumeStructureToLocation(mod Model, - old GadgetData, - vols map[string]*Volume, + oldVolumes map[string]DeviceVolume, + newVolumes map[string]DeviceVolume, volToDeviceMapping map[string]DiskVolumeDeviceTraits, missingInitialMapping bool, ) (map[string]map[int]StructureLocation, map[string]map[int]*OnDiskStructure, error) { @@ -994,8 +1004,8 @@ func buildVolumeStructureToLocation(mod Model, return err } - volumeStructureToLocation := make(map[string]map[int]StructureLocation, len(old.Info.Volumes)) - gadgetVolToPartMap := make(map[string]map[int]*OnDiskStructure, len(old.Info.Volumes)) + volumeStructureToLocation := make(map[string]map[int]StructureLocation, len(oldVolumes)) + gadgetVolToPartMap := make(map[string]map[int]*OnDiskStructure, len(oldVolumes)) // now for each volume, iterate over the structures, putting the // necessary info into the map for that volume as we iterate @@ -1007,13 +1017,13 @@ func buildVolumeStructureToLocation(mod Model, for volName, diskDeviceTraits := range volToDeviceMapping { volumeStructureToLocation[volName] = make(map[int]StructureLocation) gadgetVolToPartMap[volName] = make(map[int]*OnDiskStructure) - oldVol, ok := old.Info.Volumes[volName] + oldVol, ok := oldVolumes[volName] if !ok { return nil, nil, fmt.Errorf("internal error: volume %s not present in gadget.yaml but present in traits mapping", volName) } - newVol := vols[volName] - if newVol == nil { + newVol, ok := newVolumes[volName] + if !ok { return nil, nil, fmt.Errorf("internal error: missing volume %s", volName) } @@ -1025,7 +1035,7 @@ func buildVolumeStructureToLocation(mod Model, ExpectedStructureEncryption: diskDeviceTraits.StructureEncryption, } - disk, gadgetToDiskStruct, err := searchVolumeWithTraitsAndMatchParts(newVol, diskDeviceTraits, validateOpts) + disk, gadgetToDiskStruct, err := searchVolumeWithTraitsAndMatchParts(newVol.Volume, diskDeviceTraits, validateOpts) if err != nil { dieErr := fmt.Errorf("could not map volume %s from gadget.yaml to any physical disk: %v", volName, err) return nil, nil, maybeFatalError(dieErr) @@ -1033,7 +1043,7 @@ func buildVolumeStructureToLocation(mod Model, gadgetVolToPartMap[volName] = gadgetToDiskStruct // the index here is 0-based and is equal to VolumeStructure.YamlIndex - for volYamlIndex, volStruct := range oldVol.Structure { + for volYamlIndex, volStruct := range oldVol.Volume.Structure { structStartOffset := gadgetToDiskStruct[volYamlIndex].StartOffset loc := StructureLocation{} @@ -1117,7 +1127,7 @@ func buildVolumeStructureToLocation(mod Model, return volumeStructureToLocation, gadgetVolToPartMap, nil } -func MockVolumeStructureToLocationMap(f func(_ GadgetData, _ Model, _ map[string]*Volume) ( +func MockVolumeStructureToLocationMap(f func(_ Model, _, _ map[string]DeviceVolume) ( map[string]map[int]StructureLocation, map[string]map[int]*OnDiskStructure, error)) (restore func()) { old := volumeStructureToLocationMap volumeStructureToLocationMap = f @@ -1138,7 +1148,7 @@ var volumeStructureToLocationMap = volumeStructureToLocationMapImpl // is the volume name and the second key is the yaml index of the // structure in the gadget definition. The value is the disk structure // that matches the gadget description. -func volumeStructureToLocationMapImpl(old GadgetData, mod Model, vols map[string]*Volume) ( +func volumeStructureToLocationMapImpl(mod Model, oldVolumes, newVolumes map[string]DeviceVolume) ( map[string]map[int]StructureLocation, map[string]map[int]*OnDiskStructure, error) { // first try to load the disk-mapping.json volume trait info @@ -1173,7 +1183,7 @@ func volumeStructureToLocationMapImpl(old GadgetData, mod Model, vols map[string // cases below, we treat this heuristic mapping data the same missingInitialMapping = true var err error - volToDeviceMapping, err = buildNewVolumeToDeviceMapping(mod, old, vols) + volToDeviceMapping, err = buildNewVolumeToDeviceMapping(mod, oldVolumes, newVolumes) if err != nil { return nil, nil, err } @@ -1186,7 +1196,7 @@ func volumeStructureToLocationMapImpl(old GadgetData, mod Model, vols map[string // if there are multiple volumes leave a message that we are only // performing updates for the volume with the system-boot role - if len(old.Info.Volumes) != 1 { + if len(oldVolumes) != 1 { logger.Noticef("WARNING: gadget has multiple volumes but updates are only being performed for volume %s", volName) } } @@ -1197,13 +1207,50 @@ func volumeStructureToLocationMapImpl(old GadgetData, mod Model, vols map[string // location to update given the VolumeStructure return buildVolumeStructureToLocation( mod, - old, - vols, + oldVolumes, + newVolumes, volToDeviceMapping, missingInitialMapping, ) } +func validateVolumesMatch(old, new map[string]DeviceVolume) error { + oldVolumes := make([]string, 0, len(old)) + newVolumes := make([]string, 0, len(new)) + + for oldVol := range old { + oldVolumes = append(oldVolumes, oldVol) + } + for newVol := range new { + newVolumes = append(newVolumes, newVol) + } + common := strutil.Intersection(newVolumes, oldVolumes) + // check dissimilar cases between common, new and old + switch { + case len(common) != len(newVolumes) && len(common) != len(oldVolumes): + // there are both volumes removed from old and volumes added to new + return fmt.Errorf("cannot update gadget assets: volumes were both added and removed") + case len(common) != len(newVolumes): + // then there are volumes in old that are not in new, i.e. a volume + // was removed + return fmt.Errorf("cannot update gadget assets: volumes were removed") + case len(common) != len(oldVolumes): + // then there are volumes in new that are not in old, i.e. a volume + // was added + return fmt.Errorf("cannot update gadget assets: volumes were added") + } + // check things like assigned device-path switching + // at this point here we can assume the lists are identical + for name, cvol := range old { + // the new one must match + nvol := new[name] + if cvol.Device != nvol.Device { + return fmt.Errorf("cannot update gadget assets: device assignment is not identical for %q", name) + } + } + return nil +} + // 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 @@ -1211,7 +1258,7 @@ func volumeStructureToLocationMapImpl(old GadgetData, mod Model, vols map[string // // 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 +// structures in an opt-in manner, only structures 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 @@ -1239,30 +1286,35 @@ func volumeStructureToLocationMapImpl(old GadgetData, mod Model, vols map[string // d. After step (c) is completed the kernel refresh will now also work (no more // violation of rule 1) func Update(model Model, old, new GadgetData, rollbackDirPath string, updatePolicy UpdatePolicyFunc, observer ContentUpdateObserver) error { - // if the volumes from the old and the new gadgets do not match, then fail - - // we don't support adding or removing volumes from the gadget.yaml - newVolumes := make([]string, 0, len(new.Info.Volumes)) - oldVolumes := make([]string, 0, len(old.Info.Volumes)) - for newVol := range new.Info.Volumes { - newVolumes = append(newVolumes, newVol) + volumesForGadget := func(gd GadgetData) (map[string]DeviceVolume, error) { + if len(gd.Info.VolumeAssignments) != 0 { + return FindVolumesMatchingDeviceAssignment(gd.Info) + } else { + vols := make(map[string]DeviceVolume) + for name, vol := range gd.Info.Volumes { + vols[name] = DeviceVolume{ + Volume: vol, + } + } + return vols, nil + } } - for oldVol := range old.Info.Volumes { - oldVolumes = append(oldVolumes, oldVol) + + // The gadget can only match if they have identical volumes assigned for the + // (currently) matching device + oldVolumes, err := volumesForGadget(old) + if err != nil { + return fmt.Errorf("cannot update gadget assets: %v", err) } - common := strutil.Intersection(newVolumes, oldVolumes) - // check dissimilar cases between common, new and old - switch { - case len(common) != len(newVolumes) && len(common) != len(oldVolumes): - // there are both volumes removed from old and volumes added to new - return fmt.Errorf("cannot update gadget assets: volumes were both added and removed") - case len(common) != len(newVolumes): - // then there are volumes in old that are not in new, i.e. a volume - // was removed - return fmt.Errorf("cannot update gadget assets: volumes were removed") - case len(common) != len(oldVolumes): - // then there are volumes in new that are not in old, i.e. a volume - // was added - return fmt.Errorf("cannot update gadget assets: volumes were added") + newVolumes, err := volumesForGadget(new) + if err != nil { + return fmt.Errorf("cannot update gadget assets: %v", err) + } + + // if the volumes from the old and the new gadgets do not match, then fail - + // we don't support adding or removing volumes from the gadget.yaml + if err := validateVolumesMatch(oldVolumes, newVolumes); err != nil { + return err } if updatePolicy == nil { @@ -1300,7 +1352,7 @@ func Update(model Model, old, new GadgetData, rollbackDirPath string, updatePoli atLeastOneKernelAssetConsumed := false // build the map of volume structures to locations and of disk strucutures - structureLocations, volToPartsMap, err := volumeStructureToLocationMap(old, model, new.Info.Volumes) + structureLocations, volToPartsMap, err := volumeStructureToLocationMap(model, oldVolumes, newVolumes) if err != nil { if err == errSkipUpdateProceedRefresh { // we couldn't successfully build a map for the structure locations, @@ -1321,17 +1373,17 @@ func Update(model Model, old, new GadgetData, rollbackDirPath string, updatePoli allUpdates := []updatePair{} laidOutVols := map[string]*LaidOutVolume{} - for volName, oldVol := range old.Info.Volumes { - newVol := new.Info.Volumes[volName] + for volName, oldVol := range oldVolumes { + newVol := newVolumes[volName] // layout old partially, without going deep into the layout of structure // content - pOld, err := layoutVolumePartially(oldVol, volToPartsMap[volName]) + pOld, err := layoutVolumePartially(oldVol.Volume, volToPartsMap[volName]) if err != nil { return fmt.Errorf("cannot lay out the old volume %s: %v", volName, err) } - pNew, err := LayoutVolume(newVol, volToPartsMap[volName], opts) + pNew, err := LayoutVolume(newVol.Volume, volToPartsMap[volName], opts) if err != nil { return fmt.Errorf("cannot lay out the new volume %s: %v", volName, err) } @@ -1361,15 +1413,15 @@ func Update(model Model, old, new GadgetData, rollbackDirPath string, updatePoli // can update old layout to new layout for _, update := range updates { - fromIdx, err := oldVol.yamlIdxToStructureIdx(update.from.VolumeStructure.YamlIndex) + fromIdx, err := oldVol.Volume.yamlIdxToStructureIdx(update.from.VolumeStructure.YamlIndex) if err != nil { return err } - toIdx, err := oldVol.yamlIdxToStructureIdx(update.from.VolumeStructure.YamlIndex) + toIdx, err := oldVol.Volume.yamlIdxToStructureIdx(update.from.VolumeStructure.YamlIndex) if err != nil { return err } - if err := canUpdateStructure(oldVol, fromIdx, newVol, toIdx); err != nil { + if err := canUpdateStructure(oldVol.Volume, fromIdx, newVol.Volume, toIdx); err != nil { return fmt.Errorf("cannot update volume structure %v for volume %s: %v", update.to, volName, err) } } @@ -1391,7 +1443,7 @@ func Update(model Model, old, new GadgetData, rollbackDirPath string, updatePoli return ErrNoUpdate } - if len(new.Info.Volumes) != 1 { + if len(newVolumes) != 1 { logger.Debugf("gadget asset update routine for multiple volumes") // check if the structure location map has only one volume in it - this @@ -1487,12 +1539,7 @@ func arePartitionTypesCompatible(from, to *VolumeStructure) bool { return true } } - - if isLegacyMBRTransition(from, to) { - return true - } - - return false + return isLegacyMBRTransition(from, to) } // canUpdateStructure checks gadget compatibility on updates, looking only at diff --git a/gadget/update_test.go b/gadget/update_test.go index 9898d4c2061..5d1fc101670 100644 --- a/gadget/update_test.go +++ b/gadget/update_test.go @@ -55,7 +55,7 @@ func (s *updateTestSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) s.AddCleanup(func() { dirs.SetRootDir("") }) - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return nil, nil, fmt.Errorf("unmocked volume structure to loc map") }) restoreDoer := sync.Once{} @@ -635,7 +635,7 @@ func (u *updateTestSuite) updateDataSet(c *C) (oldData gadget.GadgetData, newDat // reasonably default volume structure to location map - individual tests // can override this - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { // XXX return something that we'll check return map[string]map[int]gadget.StructureLocation{ "foo": { @@ -699,7 +699,7 @@ func (u *updateTestSuite) TestUpdateApplyHappy(c *C) { newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -713,7 +713,7 @@ func (u *updateTestSuite) TestUpdateApplyHappy(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -1078,7 +1078,7 @@ func (u *updateTestSuite) TestUpdateApplyUC20MissingInitialMapFullLogicOnlySyste newData.Info.Volumes["pc"].Structure[i+2].Update.Edition = 1 } - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -1102,8 +1102,8 @@ func (u *updateTestSuite) TestUpdateApplyUC20MissingInitialMapFullLogicOnlySyste }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -1363,7 +1363,7 @@ func (u *updateTestSuite) TestUpdateApplyUC20MissingInitialMapFullLogicOnlySyste // content, to check that updates are ignored in this case. newData.Info.Volumes["pc"].Structure[4].Update.Edition = 1 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -1633,7 +1633,7 @@ func (u *updateTestSuite) TestUpdateApplyUC20MissingInitialMapFullLogicOnlySyste newData.Info.Volumes["foo"].Structure[2].Content = []gadget.VolumeContent{{UnresolvedSource: fName}} newData.Info.Volumes["foo"].Structure[2].Update.Edition = 1 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -1657,8 +1657,8 @@ func (u *updateTestSuite) TestUpdateApplyUC20MissingInitialMapFullLogicOnlySyste }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -2213,7 +2213,7 @@ func (u *updateTestSuite) TestUpdateApplyUC20WithInitialMapIncompatibleStructure copy(oldData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) } - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -2247,8 +2247,8 @@ func (u *updateTestSuite) TestUpdateApplyUC20WithInitialMapIncompatibleStructure }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) defer r() @@ -2460,7 +2460,7 @@ volumes: // some filesystem newData.Info.Volumes["foo"].Structure[2].Update.Edition = 1 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -2496,8 +2496,8 @@ volumes: }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -2937,7 +2937,7 @@ func (u *updateTestSuite) TestUpdateApplyOnlyWhenNeeded(c *C) { oldData.Info.Volumes["foo"].Structure[2].Update.Edition = 3 newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -2952,7 +2952,7 @@ func (u *updateTestSuite) TestUpdateApplyOnlyWhenNeeded(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -3019,10 +3019,10 @@ func (u *updateTestSuite) TestUpdateApplyErrorLayout(c *C) { }, }, } - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{"foo": {}}, map[string]map[int]*gadget.OnDiskStructure{ - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -3085,7 +3085,7 @@ func (u *updateTestSuite) TestUpdateApplyErrorIllegalVolumeUpdate(c *C) { }, }, } - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{"foo": {}}, map[string]map[int]*gadget.OnDiskStructure{ "foo": { @@ -3151,10 +3151,10 @@ func (u *updateTestSuite) TestUpdateApplyErrorIllegalStructureUpdate(c *C) { }, }, } - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{"foo": {}}, map[string]map[int]*gadget.OnDiskStructure{ - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -3261,7 +3261,7 @@ func (u *updateTestSuite) TestUpdateApplyUpdatesAreOptInWithDefaultPolicy(c *C) }) defer restore() - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{"foo": {}}, map[string]map[int]*gadget.OnDiskStructure{ "foo": { @@ -3300,7 +3300,7 @@ func (u *updateTestSuite) policyDataSet(c *C) (oldData gadget.GadgetData, newDat Offset: asOffsetPtr(0), } - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { // XXX map return map[string]map[int]gadget.StructureLocation{ "foo": { @@ -3360,7 +3360,7 @@ func (u *updateTestSuite) TestUpdateApplyUpdatesArePolicyControlled(c *C) { newData.Info.Volumes["foo"].Structure[3].Update.Edition = 4 newData.Info.Volumes["foo"].Structure[4].Update.Edition = 5 - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -3446,7 +3446,7 @@ func (u *updateTestSuite) TestUpdateApplyUpdatesDefaultPolicy(c *C) { // new one has edition set explicitly newData.Info.Volumes["foo"].Structure[1].Update.Edition = 5 - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -3504,7 +3504,7 @@ func (u *updateTestSuite) TestUpdateApplyUpdatesRemodelPolicy(c *C) { oldData.Info.Volumes["foo"].Structure[3].Update.Edition = 4 oldData.Info.Volumes["foo"].Structure[4].Update.Edition = 5 - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -3562,7 +3562,7 @@ func (u *updateTestSuite) TestUpdateApplyBackupFails(c *C) { newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1 newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3 - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -3627,7 +3627,7 @@ func (u *updateTestSuite) TestUpdateApplyUpdateFailsThenRollback(c *C) { newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2 newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3 - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -3720,7 +3720,7 @@ func (u *updateTestSuite) TestUpdateApplyUpdateErrorRollbackFail(c *C) { newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2 newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -3735,7 +3735,7 @@ func (u *updateTestSuite) TestUpdateApplyUpdateErrorRollbackFail(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -3813,7 +3813,7 @@ func (u *updateTestSuite) TestUpdateApplyBadUpdater(c *C) { newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2 newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3 - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -3938,7 +3938,7 @@ func (u *updateTestSuite) TestUpdateApplyNoChangedContentInAll(c *C) { oldData.Info.Volumes["foo"].Structure[1].Update.Edition = 1 newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -3953,7 +3953,7 @@ func (u *updateTestSuite) TestUpdateApplyNoChangedContentInAll(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -3997,7 +3997,7 @@ func (u *updateTestSuite) TestUpdateApplyNoChangedContentInSome(c *C) { oldData.Info.Volumes["foo"].Structure[1].Update.Edition = 1 newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -4012,7 +4012,7 @@ func (u *updateTestSuite) TestUpdateApplyNoChangedContentInSome(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -4054,7 +4054,7 @@ func (u *updateTestSuite) TestUpdateApplyObserverBeforeWriteErrs(c *C) { oldData, newData, rollbackDir := u.updateDataSet(c) newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -4069,7 +4069,7 @@ func (u *updateTestSuite) TestUpdateApplyObserverBeforeWriteErrs(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -4104,7 +4104,7 @@ func (u *updateTestSuite) TestUpdateApplyObserverCanceledErrs(c *C) { oldData, newData, rollbackDir := u.updateDataSet(c) newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -4119,7 +4119,7 @@ func (u *updateTestSuite) TestUpdateApplyObserverCanceledErrs(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "foo": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["foo"]), + "foo": gadget.OnDiskStructsFromGadget(oldVolumes["foo"].Volume), }, nil }) @@ -4285,7 +4285,7 @@ func (u *updateTestSuite) TestUpdateApplyUpdatesWithKernelPolicy(c *C) { }, } - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "foo": { 0: { @@ -4408,7 +4408,7 @@ assets: }) defer restore() - r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, _, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{"foo": {}}, map[string]map[int]*gadget.OnDiskStructure{ "foo": { @@ -5045,11 +5045,9 @@ volumes: lvol, err := gadgettest.LayoutFromYaml(c.MkDir(), gadgetYaml, nil) c.Assert(err, IsNil) - old := gadget.GadgetData{ - Info: &gadget.Info{ - Volumes: map[string]*gadget.Volume{ - "volume-id": lvol.Volume, - }, + old := map[string]gadget.DeviceVolume{ + "volume-id": { + Volume: lvol.Volume, }, } @@ -5059,9 +5057,11 @@ volumes: "volume-id": lvol, } - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } _, err = gadget.BuildNewVolumeToDeviceMapping(uc16Model, old, vols) c.Assert(err, Equals, gadget.ErrSkipUpdateProceedRefresh) @@ -5071,14 +5071,11 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingImplicitSystemDataUC1 allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.UC16YAMLImplicitSystemData, uc16Model) c.Assert(err, IsNil) - old := gadget.GadgetData{ - Info: &gadget.Info{ - Volumes: make(map[string]*gadget.Volume), - }, - } - + old := make(map[string]gadget.DeviceVolume) for volName, laidOutVol := range allLaidOutVolumes { - old.Info.Volumes[volName] = laidOutVol.Volume + old[volName] = gadget.DeviceVolume{ + Volume: laidOutVol.Volume, + } } // setup symlink for the system-boot partition @@ -5102,9 +5099,11 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingImplicitSystemDataUC1 }) defer restore() - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } m, err := gadget.BuildNewVolumeToDeviceMapping(uc16Model, old, vols) c.Assert(err, IsNil) @@ -5140,11 +5139,9 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingImplicitSystemBootSin laidOutVolume, err := gadgettest.LayoutFromYaml(c.MkDir(), implicitSystemBootVolumeYAML, uc16Model) c.Assert(err, IsNil) - old := gadget.GadgetData{ - Info: &gadget.Info{ - Volumes: map[string]*gadget.Volume{ - "pc": laidOutVolume.Volume, - }, + old := map[string]gadget.DeviceVolume{ + "pc": { + Volume: laidOutVolume.Volume, }, } @@ -5173,9 +5170,11 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingImplicitSystemBootSin "pc": laidOutVolume, } - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } m, err := gadget.BuildNewVolumeToDeviceMapping(uc16Model, old, vols) c.Assert(err, IsNil) @@ -5232,15 +5231,17 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingImplicitSystemBootMul allLaidOutVolumes[volName] = lvol } - old := gadget.GadgetData{Info: info} + old := make(map[string]gadget.DeviceVolume) // don't need to mock anything, we don't get far enough // we fail with the error that skips the asset update but proceeds with the // rest of the refresh - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } _, err = gadget.BuildNewVolumeToDeviceMapping(uc16Model, old, vols) c.Assert(err, Equals, gadget.ErrSkipUpdateProceedRefresh) @@ -5250,22 +5251,21 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingPreUC20NonFatalError( allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.UC16YAMLImplicitSystemData, uc16Model) c.Assert(err, IsNil) - old := gadget.GadgetData{ - Info: &gadget.Info{ - Volumes: make(map[string]*gadget.Volume), - }, - } - + old := make(map[string]gadget.DeviceVolume) for volName, laidOutVol := range allLaidOutVolumes { - old.Info.Volumes[volName] = laidOutVol.Volume + old[volName] = gadget.DeviceVolume{ + Volume: laidOutVol.Volume, + } } // don't mock any symlinks so that it fails to find any disk matching the // system-boot volume - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } _, err = gadget.BuildNewVolumeToDeviceMapping(uc16Model, old, vols) c.Assert(err, Equals, gadget.ErrSkipUpdateProceedRefresh) @@ -5282,14 +5282,11 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingPreUC20CannotMap(c *C allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.UC16YAMLImplicitSystemData, uc16Model) c.Assert(err, IsNil) - old := gadget.GadgetData{ - Info: &gadget.Info{ - Volumes: make(map[string]*gadget.Volume), - }, - } - + old := make(map[string]gadget.DeviceVolume) for volName, laidOutVol := range allLaidOutVolumes { - old.Info.Volumes[volName] = laidOutVol.Volume + old[volName] = gadget.DeviceVolume{ + Volume: laidOutVol.Volume, + } } // setup symlink for the system-boot partition @@ -5313,9 +5310,11 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingPreUC20CannotMap(c *C }) defer restore() - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } // The call will fail as it won't find a match between /dev/vda2 and @@ -5333,14 +5332,11 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingUC20MultiVolume(c *C) allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.MultiVolumeUC20GadgetYaml, uc20Model) c.Assert(err, IsNil) - old := gadget.GadgetData{ - Info: &gadget.Info{ - Volumes: make(map[string]*gadget.Volume), - }, - } - + old := make(map[string]gadget.DeviceVolume) for volName, laidOutVol := range allLaidOutVolumes { - old.Info.Volumes[volName] = laidOutVol.Volume + old[volName] = gadget.DeviceVolume{ + Volume: laidOutVol.Volume, + } } // setup symlink for the ubuntu-seed partition @@ -5364,9 +5360,11 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingUC20MultiVolume(c *C) }) defer restore() - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } m, err := gadget.BuildNewVolumeToDeviceMapping(uc20Model, old, vols) c.Assert(err, IsNil) @@ -5380,14 +5378,11 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingUC20Encryption(c *C) allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.RaspiSimplifiedYaml, uc20Model) c.Assert(err, IsNil) - old := gadget.GadgetData{ - Info: &gadget.Info{ - Volumes: make(map[string]*gadget.Volume), - }, - } - + old := make(map[string]gadget.DeviceVolume) for volName, laidOutVol := range allLaidOutVolumes { - old.Info.Volumes[volName] = laidOutVol.Volume + old[volName] = gadget.DeviceVolume{ + Volume: laidOutVol.Volume, + } } // setup symlink for the ubuntu-seed partition @@ -5419,9 +5414,11 @@ func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingUC20Encryption(c *C) err = os.WriteFile(markerFile, nil, 0644) c.Assert(err, IsNil) - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } m, err := gadget.BuildNewVolumeToDeviceMapping(uc20Model, old, vols) c.Assert(err, IsNil) @@ -5718,9 +5715,11 @@ func (s *updateTestSuite) testBuildVolumeStructureToLocation(c *C, ) missingInitialMappingNo := false - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } structureMap, _, err := gadget.BuildVolumeStructureToLocation(model, old, vols, traits, missingInitialMappingNo) c.Assert(err, IsNil) @@ -5733,18 +5732,15 @@ func (s *updateTestSuite) setupForVolumeStructureToLocation(c *C, traits map[string]gadget.DiskVolumeDeviceTraits, volMappings map[string]*disks.MockDiskMapping, expMapping map[string]map[int]gadget.StructureLocation, -) (gadget.GadgetData, map[string]*gadget.LaidOutVolume) { +) (map[string]gadget.DeviceVolume, map[string]*gadget.LaidOutVolume) { allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", yaml, model) c.Assert(err, IsNil) - old := gadget.GadgetData{ - Info: &gadget.Info{ - Volumes: make(map[string]*gadget.Volume), - }, - } - + old := make(map[string]gadget.DeviceVolume) for volName, laidOutVol := range allLaidOutVolumes { - old.Info.Volumes[volName] = laidOutVol.Volume + old[volName] = gadget.DeviceVolume{ + Volume: laidOutVol.Volume, + } } devicePathMapping := map[string]*disks.MockDiskMapping{} @@ -5855,11 +5851,13 @@ func (s *updateTestSuite) testVolumeStructureToLocationMap(c *C, expMapping, ) - vols := map[string]*gadget.Volume{} + vols := make(map[string]gadget.DeviceVolume) for name, lov := range allLaidOutVolumes { - vols[name] = lov.Volume + vols[name] = gadget.DeviceVolume{ + Volume: lov.Volume, + } } - structureMap, _, err := gadget.VolumeStructureToLocationMap(old, model, vols) + structureMap, _, err := gadget.VolumeStructureToLocationMap(model, old, vols) c.Assert(err, IsNil) c.Assert(structureMap, DeepEquals, expMapping) } diff --git a/osutil/disks/mockdisk.go b/osutil/disks/mockdisk.go index ba67e33f098..5583c51d4b1 100644 --- a/osutil/disks/mockdisk.go +++ b/osutil/disks/mockdisk.go @@ -21,7 +21,11 @@ package disks import ( "fmt" + "path" + "path/filepath" + "strings" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" ) @@ -300,6 +304,39 @@ func MockPartitionDeviceNodeToDiskMapping(mockedDisks map[string]*MockDiskMappin } } +func resolveName(deviceName string) (string, error) { + resolve := func(p string) (string, error) { + if !osutil.FileExists(p) { + return "", nil + } + if osutil.IsSymlink(p) { + resolved, err := filepath.EvalSymlinks(p) + if err != nil { + return "", err + } + return resolved, nil + } + return p, nil + } + + if res, err := resolve(deviceName); err != nil { + return "", err + } else if res == "" { + // did not exist, try again but with corrected path + if res, err := resolve(path.Join(dirs.GlobalRootDir, deviceName)); err != nil { + return "", err + } else if res == "" { + // did not exist at all, meaning we assume it's the name of + // the device, not a path + return deviceName, nil + } else { + return strings.TrimPrefix(res, dirs.GlobalRootDir), nil + } + } else { + return res, nil + } +} + // MockDeviceNameToDiskMapping will mock DiskFromDeviceName such that the // provided map of device names to mock disks is used instead of the actual // implementation using udev. @@ -314,9 +351,14 @@ func MockDeviceNameToDiskMapping(mockedDisks map[string]*MockDiskMapping) (resto old := diskFromDeviceName diskFromDeviceName = func(deviceName string) (Disk, error) { - disk, ok := mockedDisks[deviceName] + // allow symlinks to point to mocked disks + resolved, err := resolveName(deviceName) + if err != nil { + return nil, err + } + disk, ok := mockedDisks[resolved] if !ok { - return nil, fmt.Errorf("device name %q not mocked", deviceName) + return nil, fmt.Errorf("device name %q not mocked", resolved) } return disk, nil } diff --git a/overlord/devicestate/devicestate_gadget_test.go b/overlord/devicestate/devicestate_gadget_test.go index 20d780476d4..4ee619c51d7 100644 --- a/overlord/devicestate/devicestate_gadget_test.go +++ b/overlord/devicestate/devicestate_gadget_test.go @@ -736,7 +736,7 @@ volumes: {"content.img", "updated content"}, }) - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -745,7 +745,7 @@ volumes: }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) defer r() diff --git a/overlord/managers_test.go b/overlord/managers_test.go index 2396408e27b..13d41c96b42 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -6593,10 +6593,10 @@ volumes: "revision": "1", }) - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{"volume-id": {0: {}}}, map[string]map[int]*gadget.OnDiskStructure{ - "volume-id": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["volume-id"]), + "volume-id": gadget.OnDiskStructsFromGadget(oldVolumes["volume-id"].Volume), }, nil }) defer r() @@ -6724,7 +6724,7 @@ volumes: }) s.serveSnap(snapPath, "2") - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "volume-id": { 0: { @@ -6734,7 +6734,7 @@ volumes: }, }, map[string]map[int]*gadget.OnDiskStructure{ - "volume-id": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["volume-id"]), + "volume-id": gadget.OnDiskStructsFromGadget(oldVolumes["volume-id"].Volume), }, nil }) @@ -7978,10 +7978,10 @@ func (s *mgrsSuiteCore) TestRemodelUC20DifferentKernelChannel(c *C) { now := time.Now() expectedLabel := now.Format("20060102") - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{"pc": {}}, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) defer r() @@ -8118,7 +8118,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20DifferentGadgetChannel(c *C) { now := time.Now() expectedLabel := now.Format("20060102") - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -8133,7 +8133,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20DifferentGadgetChannel(c *C) { }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) defer r() @@ -8411,7 +8411,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20BackToPreviousGadget(c *C) { now := time.Now() expectedLabel := now.Format("20060102") - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -8425,7 +8425,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20BackToPreviousGadget(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) defer r() @@ -8599,7 +8599,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20ExistingGadgetSnapDifferentChannel(c *C) now := time.Now() expectedLabel := now.Format("20060102") - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -8613,7 +8613,7 @@ func (s *mgrsSuiteCore) TestRemodelUC20ExistingGadgetSnapDifferentChannel(c *C) }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) defer r() @@ -9079,7 +9079,7 @@ func (s *mgrsSuiteCore) TestRemodelRollbackValidationSets(c *C) { bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Role: bootloader.RoleRecovery}) c.Assert(err, IsNil) - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -9093,7 +9093,7 @@ func (s *mgrsSuiteCore) TestRemodelRollbackValidationSets(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) defer r() @@ -9539,7 +9539,7 @@ func (s *mgrsSuiteCore) TestRemodelReplaceValidationSets(c *C) { bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Role: bootloader.RoleRecovery}) c.Assert(err, IsNil) - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -9553,7 +9553,7 @@ func (s *mgrsSuiteCore) TestRemodelReplaceValidationSets(c *C) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) defer r() @@ -9847,7 +9847,7 @@ func (s *mgrsSuiteCore) testRemodelUC20ToUC22(c *C, mockSnapdRefresh bool) { bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Role: bootloader.RoleRecovery}) c.Assert(err, IsNil) - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -9861,7 +9861,7 @@ func (s *mgrsSuiteCore) testRemodelUC20ToUC22(c *C, mockSnapdRefresh bool) { }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) defer r() @@ -11199,10 +11199,10 @@ func (s *mgrsSuiteCore) testGadgetKernelCommandLine(c *C, gadgetPath string, gad err = assertstate.Add(st, model) c.Assert(err, IsNil) - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{"pc": {}}, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) @@ -11787,7 +11787,7 @@ func (s *mgrsSuiteCore) testUpdateKernelBaseSingleRebootWithGadgetSetup(c *C, sn }) s.serveSnap(p, "2") - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "pc": { 0: { @@ -11801,7 +11801,7 @@ func (s *mgrsSuiteCore) testUpdateKernelBaseSingleRebootWithGadgetSetup(c *C, sn }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "pc": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["pc"]), + "pc": gadget.OnDiskStructsFromGadget(oldVolumes["pc"].Volume), }, nil }) defer r() @@ -13411,7 +13411,7 @@ func (ms *gadgetUpdatesSuite) TestGadgetWithKernelRefUpgradeFromOldErrorKernel(c structureName := "ubuntu-seed" structureMountDir := filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName) - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{ "volume-id": { 0: { @@ -13422,7 +13422,7 @@ func (ms *gadgetUpdatesSuite) TestGadgetWithKernelRefUpgradeFromOldErrorKernel(c }, }, }, map[string]map[int]*gadget.OnDiskStructure{ - "volume-id": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["volume-id"]), + "volume-id": gadget.OnDiskStructsFromGadget(oldVolumes["volume-id"].Volume), }, nil }) defer r() @@ -13630,9 +13630,9 @@ volumes: {"meta/gadget.yaml", gadgetYaml}, }) - r := gadget.MockVolumeStructureToLocationMap(func(gd gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.Volume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.Model, oldVolumes, _ map[string]gadget.DeviceVolume) (map[string]map[int]gadget.StructureLocation, map[string]map[int]*gadget.OnDiskStructure, error) { return map[string]map[int]gadget.StructureLocation{"volume-id": {0: {}}}, map[string]map[int]*gadget.OnDiskStructure{ - "volume-id": gadget.OnDiskStructsFromGadget(gd.Info.Volumes["volume-id"]), + "volume-id": gadget.OnDiskStructsFromGadget(oldVolumes["volume-id"].Volume), }, nil }) defer r() diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh index bf0c0ab14f6..51113a501cd 100755 --- a/tests/lib/nested.sh +++ b/tests/lib/nested.sh @@ -454,7 +454,7 @@ nested_cleanup_env() { rm -rf "$(nested_get_extra_snaps_path)" } -nested_get_image_name() { +nested_get_image_name_base() { local TYPE="$1" local SOURCE="${NESTED_CORE_CHANNEL}" local NAME="${NESTED_IMAGE_ID:-generic}" @@ -476,7 +476,13 @@ nested_get_image_name() { if [ "$(nested_get_extra_snaps | wc -l)" != "0" ]; then SOURCE="custom" fi - echo "ubuntu-${TYPE}-${VERSION}-${SOURCE}-${NAME}.img" + echo "ubuntu-${TYPE}-${VERSION}-${SOURCE}-${NAME}" +} + +nested_get_image_name() { + local BASE_NAME + BASE_NAME="$(nested_get_image_name_base "$1")" + echo "${BASE_NAME}.img" } nested_is_generic_image() { @@ -914,13 +920,23 @@ nested_create_core_vm() { # ubuntu-image dropped the --output parameter, so we have to rename # the image ourselves, the images are named after volumes listed in # gadget.yaml + local IMAGE_BASE_NAME + IMAGE_BASE_NAME="$(nested_get_image_name_base core)" find "$NESTED_IMAGES_DIR/" -maxdepth 1 -name '*.img' | while read -r imgname; do - if [ -e "$NESTED_IMAGES_DIR/$IMAGE_NAME" ]; then - echo "Image $IMAGE_NAME file already present" - exit 1 - fi - mv "$imgname" "$NESTED_IMAGES_DIR/$IMAGE_NAME" + volname=$(basename "$imgname" .img) + mv "$imgname" "$NESTED_IMAGES_DIR/$IMAGE_BASE_NAME-$volname.img" done + + # get the name of the boot-volume, and then create a symlink + # between the regular image name and the main volume, additional + # volumes must be manually added to the VM creation by the tests + local BOOTVOLUME + BOOTVOLUME=pc + if [ -e pc-gadget/meta/gadget.yaml ]; then + BOOTVOLUME="$(yq eval '.volumes[] | .structure.[] | select(.name == "ubuntu-boot") | parent(2) | key' pc-gadget/meta/gadget.yaml)" + fi + ln -s "$NESTED_IMAGES_DIR/$IMAGE_BASE_NAME-$BOOTVOLUME.img" "$NESTED_IMAGES_DIR/$IMAGE_NAME" + unset SNAPPY_FORCE_SAS_URL unset UBUNTU_IMAGE_SNAP_CMD fi diff --git a/tests/nested/manual/install-volume-assignment/task.yaml b/tests/nested/manual/install-volume-assignment/task.yaml new file mode 100644 index 00000000000..b9dbf953de9 --- /dev/null +++ b/tests/nested/manual/install-volume-assignment/task.yaml @@ -0,0 +1,98 @@ +summary: Install a gadget that uses volume-assignments + +details: | + Tests volume-assignments syntax, and that we get the volumes installed to the expected + disks. The gadget will assign two different volumes to two different disks. The disks + are hardcoded at their PCI location, which hopefully does not change easily. We must + use a path in /dev/disk/.. as those are the only supported device assignments currently + for volumes. The test also tests gadget update. + +systems: [ubuntu-2*] + +environment: + NESTED_CUSTOM_MODEL: $TESTSLIB/assertions/valid-for-testing-pc-{VERSION}.model + NESTED_ENABLE_SECURE_BOOT: false + NESTED_BUILD_SNAPD_FROM_CURRENT: true + NESTED_IMAGE_ID: volassign + +prepare: | + snap install yq + + VERSION=$(tests.nested show version) + snap download --basename=pc --channel="$VERSION/edge" pc + unsquashfs -d pc-gadget pc.snap + + # append volume-assignments + cat <> pc-gadget/meta/gadget.yaml + backup: + schema: mbr + structure: + - filesystem: ext4 + name: system-backup + size: 127M + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + volume-assignments: + - name: do-not-match-device + assignment: + pc: + device: /dev/disk/by-id/nvme0003 + - name: test-device + assignment: + pc: + device: /dev/disk/by-path/pci-0000:00:05.0 + backup: + device: /dev/disk/by-path/pci-0000:00:06.0 + EOF + + # enable debug + cat <> pc-gadget/cmdline.extra + snapd.debug=1 + EOF + + # Make sure that we get a different snap than the store one + touch pc-gadget/empty + snap pack --filename=pc_x1.snap pc-gadget/ "$(tests.nested get extra-snaps-path)" + + tests.nested build-image core + +execute: | + # shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + # create new disk for the gadget volume-assignments that we attach + # to the VM + BACKUP_VOLUME="$NESTED_IMAGES_DIR/$(nested_get_image_name_base core)-backup.img" + + # setup extra disk options for tests.nested + NESTED_PARAM_EXTRA="-drive file=$BACKUP_VOLUME,if=none,snapshot=off,format=raw,id=disk2 \ + -device virtio-blk-pci,drive=disk2,serial=target" + + tests.nested create-vm core --extra-param "$NESTED_PARAM_EXTRA" + + # Build a new gadget that has content that needs to be updated + sed -i 's/This program cannot be run in DOS mode/This program cannot be run in XXX mode/' \ + pc-gadget/grubx64.efi + yq -i '(.volumes.pc.structure[] | select(.name == "ubuntu-boot") | .update.edition) |= . + 1' \ + pc-gadget/meta/gadget.yaml + + snap pack --filename=pc.snap pc-gadget + remote.push pc.snap + + remote.exec "sudo snap wait system seed.loaded" + + # verify device path exists + # even though at this point if it didn't exist the system would have + # failed to install + remote.exec "ls /dev/disk/by-path | grep 'pci-0000:00:05.0'" + remote.exec "ls /dev/disk/by-path | grep 'pci-0000:00:06.0'" + + boot_id=$(tests.nested boot-id) + + # Install new gadget + remote.exec "sudo snap install --dangerous pc.snap" || [ "$?" -eq 255 ] + # It should reboot now + remote.wait-for reboot "$boot_id" + + # Check that asset has been updated + remote.exec sudo grep -i -a '"This program cannot be run in XXX mode"' \ + /run/mnt/ubuntu-boot/EFI/boot/grubx64.efi