Skip to content

Commit

Permalink
gadget,tests: add support for volume-assignment syntax in gadget.yaml…
Browse files Browse the repository at this point in the history
…, this will allow the gadget yaml to specify specific disk mappings when either installing or updating the system
  • Loading branch information
Meulengracht committed Oct 1, 2024
1 parent a2262e8 commit 31408df
Show file tree
Hide file tree
Showing 14 changed files with 1,458 additions and 383 deletions.
89 changes: 61 additions & 28 deletions gadget/device_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package gadget
import (
"fmt"
"path/filepath"
"strings"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
Expand All @@ -31,38 +32,18 @@ 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 resolveBaseDevice(baseDevicePath string) (string, error) {
if !osutil.IsSymlink(baseDevicePath) {
// no need to resolve anything
return baseDevicePath, nil
}
return evalSymlinks(baseDevicePath)
}

func resolveMaybeDiskPaths(baseDevicePath string, diskPaths []string) (string, error) {
var found string
var match string
for _, candidate := range candidates {
for _, candidate := range diskPaths {
if !osutil.FileExists(candidate) {
continue
}
Expand All @@ -81,6 +62,11 @@ func FindDeviceForStructure(vs *VolumeStructure) (string, error) {
return "", fmt.Errorf("conflicting device match, %q points to %q, previous match %q points to %q",
candidate, target, match, found)
}
if !strings.HasPrefix(target, baseDevicePath) {
// does not match the base-device path, meaning the candidate
// will be skipped
continue
}
found = target
match = candidate
}
Expand All @@ -91,3 +77,50 @@ 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)
}

func FindFixedDeviceForStructure(baseDevicePath string, vs *VolumeStructure) (string, error) {
if baseDevicePath == "" {
return "", fmt.Errorf("internal error: base device path must be supplied")
}
resolved, err := resolveBaseDevice(baseDevicePath)
if err != nil {
return "", err
}

candidates := discoverDeviceDiskCandidatesForStructure(vs)
return resolveMaybeDiskPaths(resolved, candidates)
}
48 changes: 48 additions & 0 deletions gadget/device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,51 @@ func (d *deviceSuite) TestDeviceFindBadEvalSymlinks(c *C) {
c.Check(err, ErrorMatches, `cannot read device link: failed`)
c.Check(found, Equals, "")
}

func (d *deviceSuite) TestDeviceFindFixedEmptyBase(c *C) {
found, err := gadget.FindFixedDeviceForStructure("", &gadget.VolumeStructure{Name: "relative", EnclosingVolume: &gadget.Volume{}})
c.Check(err, ErrorMatches, `internal error: base device path must be supplied`)
c.Check(found, Equals, "")
}

func (d *deviceSuite) TestDeviceFindFixedMatchesBaseDeviceAsSymlink(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.FindFixedDeviceForStructure(
filepath.Join(d.dir, "/dev/disk/by-label/foo"),
&gadget.VolumeStructure{Name: "relative", EnclosingVolume: &gadget.Volume{}})
c.Check(err, IsNil)
c.Check(found, Equals, filepath.Join(d.dir, "/dev/fakedevice"))
}

func (d *deviceSuite) TestDeviceFindFixedMatchesBaseDevice(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.FindFixedDeviceForStructure(fakedevice,
&gadget.VolumeStructure{Name: "relative", EnclosingVolume: &gadget.Volume{}})
c.Check(err, IsNil)
c.Check(found, Equals, filepath.Join(d.dir, "/dev/fakedevice"))
}

func (d *deviceSuite) TestDeviceFindFixedNoMatchesBaseDevice(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.FindFixedDeviceForStructure(fakedevice1,
&gadget.VolumeStructure{Name: "relative", EnclosingVolume: &gadget.Volume{}})
c.Check(err, ErrorMatches, `device not found`)
c.Check(found, Equals, "")
}
11 changes: 10 additions & 1 deletion gadget/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
98 changes: 97 additions & 1 deletion gadget/gadget.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"fmt"
"math"
"os"
"path"
"path/filepath"
"regexp"
"sort"
Expand All @@ -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"
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -992,6 +1078,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]
Expand Down Expand Up @@ -1039,6 +1128,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
}

Expand Down
Loading

0 comments on commit 31408df

Please sign in to comment.