diff --git a/build-aux/snap/snapcraft.yaml b/build-aux/snap/snapcraft.yaml index 8984227644d..a72e7d3b4ee 100644 --- a/build-aux/snap/snapcraft.yaml +++ b/build-aux/snap/snapcraft.yaml @@ -96,6 +96,7 @@ parts: runtime: plugin: nil stage-packages: + - libblkid1 - libbrotli1 - libc6 - libcap2 @@ -231,6 +232,7 @@ parts: - autoconf-archive - automake - xfslibs-dev + - libblkid-dev - libudev-dev - libcap-dev - libseccomp-dev diff --git a/cmd/snap-bootstrap/blkid/blkid.go b/cmd/snap-bootstrap/blkid/blkid.go new file mode 100644 index 00000000000..ee128762849 --- /dev/null +++ b/cmd/snap-bootstrap/blkid/blkid.go @@ -0,0 +1,186 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package blkid + +//#cgo CFLAGS: -D_FILE_OFFSET_BITS=64 +//#cgo pkg-config: blkid +//#cgo LDFLAGS: +// +//#include +//#include +import "C" + +import ( + "fmt" + "unsafe" +) + +const ( + BLKID_PARTS_ENTRY_DETAILS int = C.BLKID_PARTS_ENTRY_DETAILS +) + +// AbstractBlkidProbe is wrapper for blkid_probe +// See "Low-level" section of libblkid documentation +type AbstractBlkidProbe interface { + // LookupValue is a wrapper for blkid_probe_lookup_value + LookupValue(entryName string) (string, error) + // Close is a wrapper for blkid_free_probe + Close() + // EnablePartitions is a wrapper for blkid_probe_enable_partitions + EnablePartitions(value bool) + // EnableSuperblocks is a wrapper for blkid_probe_enable_superblocks + EnableSuperblocks(value bool) + // SetPartitionsFlags is a wrapper for blkid_probe_set_partitions_flags + SetPartitionsFlags(flags int) + // DoSafeprobe is a wrapper for blkid_do_safeprobe + DoSafeprobe() error + // GetPartitions is a wrapper for blkid_probe_get_partitions + GetPartitions() (AbstractBlkidPartlist, error) +} + +// AbstractBlkidPartlist is a wrapper for blkid_partlist +type AbstractBlkidPartlist interface { + // GetPartitions is a wrapper for blkid_partlist_get_partition + // and blkid_partlist_numof_partitions. + GetPartitions() []AbstractBlkidPartition +} + +// AbstractBlkidPartition is a wrapper for blkid_partition +type AbstractBlkidPartition interface { + // GetName is a wrapper for blkid_partition_get_name + GetName() string + // GetUUID is a wrapper for blkid_partition_get_uuid + GetUUID() string +} + +type blkidProbe struct { + probeHandle C.blkid_probe +} + +func newProbeFromFilenameImpl(node string) (AbstractBlkidProbe, error) { + cnode := C.CString(node) + defer C.free(unsafe.Pointer(cnode)) + probe, err := C.blkid_new_probe_from_filename(cnode) + if probe == nil { + if err == nil { + return nil, fmt.Errorf("blkid_new_probe_from_filename failed but no error was returned") + } + return nil, err + } + return &blkidProbe{probe}, nil +} + +var NewProbeFromFilename = newProbeFromFilenameImpl + +func (p *blkidProbe) checkProbe() { + if p.probeHandle == nil { + panic("used blkid probe after Close") + } +} + +func (p *blkidProbe) LookupValue(entryName string) (string, error) { + p.checkProbe() + var value *C.char + var value_len C.size_t + cname := C.CString(entryName) + defer C.free(unsafe.Pointer(cname)) + res := C.blkid_probe_lookup_value(p.probeHandle, cname, &value, &value_len) + if res < 0 { + return "", fmt.Errorf("probe value was not found: %s", entryName) + } + if value_len > 0 { + return C.GoStringN(value, C.int(value_len-1)), nil + } else { + return "", fmt.Errorf("probe value has unexpected size") + } +} + +func (p *blkidProbe) Close() { + p.checkProbe() + C.blkid_free_probe(p.probeHandle) + p.probeHandle = C.blkid_probe(nil) +} + +func (p *blkidProbe) EnablePartitions(value bool) { + p.checkProbe() + v := 0 + if value { + v = 1 + } + C.blkid_probe_enable_partitions(p.probeHandle, C.int(v)) +} + +func (p *blkidProbe) EnableSuperblocks(value bool) { + p.checkProbe() + v := 0 + if value { + v = 1 + } + C.blkid_probe_enable_superblocks(p.probeHandle, C.int(v)) +} + +func (p *blkidProbe) SetPartitionsFlags(flags int) { + p.checkProbe() + C.blkid_probe_set_partitions_flags(p.probeHandle, C.int(flags)) +} + +func (p *blkidProbe) DoSafeprobe() error { + p.checkProbe() + res, err := C.blkid_do_safeprobe(p.probeHandle) + if res < 0 { + return err + } + return nil +} + +type blkidPartlist struct { + partlistHandle C.blkid_partlist +} + +func (p *blkidProbe) GetPartitions() (AbstractBlkidPartlist, error) { + p.checkProbe() + partitions, err := C.blkid_probe_get_partitions(p.probeHandle) + if partitions == nil { + return nil, err + } + return &blkidPartlist{partitions}, nil +} + +type blkidPartition struct { + partitionHandle C.blkid_partition +} + +func (p *blkidPartlist) GetPartitions() []AbstractBlkidPartition { + npartitions := C.blkid_partlist_numof_partitions(p.partlistHandle) + ret := make([]AbstractBlkidPartition, npartitions) + for i := 0; i < int(npartitions); i++ { + partition := C.blkid_partlist_get_partition(p.partlistHandle, C.int(i)) + ret[i] = &blkidPartition{partition} + } + return ret +} + +func (p *blkidPartition) GetName() string { + return C.GoString(C.blkid_partition_get_name(p.partitionHandle)) +} + +func (p *blkidPartition) GetUUID() string { + return C.GoString(C.blkid_partition_get_uuid(p.partitionHandle)) +} diff --git a/cmd/snap-bootstrap/blkid/blkid_test.go b/cmd/snap-bootstrap/blkid/blkid_test.go new file mode 100644 index 00000000000..3049cb53065 --- /dev/null +++ b/cmd/snap-bootstrap/blkid/blkid_test.go @@ -0,0 +1,90 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package blkid_test + +import ( + "os/exec" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd/snap-bootstrap/blkid" + + "github.com/snapcore/snapd/testutil" +) + +func Test(t *testing.T) { TestingT(t) } + +type blkidSuite struct { + testutil.BaseTest + + image string +} + +var _ = Suite(&blkidSuite{}) + +func (s *blkidSuite) SetUpTest(c *C) { + systemdRepart, err := exec.LookPath("systemd-repart") + if err != nil { + c.Skip("systemd-repart is not available") + } + + s.BaseTest.SetUpTest(c) + + tmp := c.MkDir() + image := filepath.Join(tmp, "image") + + cmd := exec.Command(systemdRepart, "--offline=yes", "--size=64M", "--empty=create", "--definitions=test-data/repart.d", image) + err = cmd.Run() + if err != nil { + c.Skip("systemd-repart is not working") + } + + s.image = image +} + +func (s *blkidSuite) TestScanPartitionTable(c *C) { + probe, err := blkid.NewProbeFromFilename(s.image) + c.Assert(err, IsNil) + defer probe.Close() + + probe.EnablePartitions(true) + + err = probe.DoSafeprobe() + c.Assert(err, IsNil) + + pttype, err := probe.LookupValue("PTTYPE") + c.Assert(err, IsNil) + c.Check(pttype, Equals, "gpt") + + partlist, err := probe.GetPartitions() + c.Assert(err, IsNil) + + partitions := 0 + for _, p := range partlist.GetPartitions() { + partitions++ + label := p.GetName() + c.Check(label, Equals, "thelabel") + uuid := p.GetUUID() + c.Check(uuid, Equals, "93fd7d8f-a662-4451-a03f-c065f2c2e1ab") + } + c.Check(partitions, Equals, 1) +} diff --git a/cmd/snap-bootstrap/blkid/mock_blkid.go b/cmd/snap-bootstrap/blkid/mock_blkid.go new file mode 100644 index 00000000000..e2cc7b97bae --- /dev/null +++ b/cmd/snap-bootstrap/blkid/mock_blkid.go @@ -0,0 +1,110 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package blkid + +import ( + "fmt" +) + +type Constructor func(string) (AbstractBlkidProbe, error) + +func MockBlkid(constr Constructor) func() { + old := NewProbeFromFilename + NewProbeFromFilename = constr + return func() { + NewProbeFromFilename = old + } +} + +func MockBlkidMap(probeMap map[string]*FakeBlkidProbe) func() { + return MockBlkid(func(name string) (AbstractBlkidProbe, error) { + value, ok := probeMap[name] + if !ok { + return nil, fmt.Errorf("not found") + } + return value, nil + }) +} + +func BuildFakeProbe(values map[string]string) *FakeBlkidProbe { + return &FakeBlkidProbe{values, &FakeBlkidPartlist{}} +} + +func (p *FakeBlkidProbe) AddPartition(name, uuid string) { + p.partlist.partitions = append(p.partlist.partitions, &FakeBlkidPartition{name, uuid}) +} + +type FakeBlkidPartition struct { + name string + uuid string +} + +type FakeBlkidPartlist struct { + partitions []*FakeBlkidPartition +} + +type FakeBlkidProbe struct { + values map[string]string + partlist *FakeBlkidPartlist +} + +func (p *FakeBlkidProbe) LookupValue(entryName string) (string, error) { + value, ok := p.values[entryName] + if !ok { + return "", fmt.Errorf("Probe value was not found: %s", entryName) + } + return value, nil +} + +func (p *FakeBlkidProbe) Close() { +} + +func (p *FakeBlkidProbe) EnablePartitions(value bool) { +} + +func (p *FakeBlkidProbe) EnableSuperblocks(value bool) { +} + +func (p *FakeBlkidProbe) SetPartitionsFlags(flags int) { +} + +func (p *FakeBlkidProbe) DoSafeprobe() error { + return nil +} + +func (p *FakeBlkidProbe) GetPartitions() (AbstractBlkidPartlist, error) { + return p.partlist, nil +} + +func (p *FakeBlkidPartlist) GetPartitions() []AbstractBlkidPartition { + ret := make([]AbstractBlkidPartition, len(p.partitions)) + for i, partition := range p.partitions { + ret[i] = partition + } + return ret +} + +func (p *FakeBlkidPartition) GetName() string { + return p.name +} + +func (p *FakeBlkidPartition) GetUUID() string { + return p.uuid +} diff --git a/cmd/snap-bootstrap/blkid/test-data/repart.d/10-ext4.conf b/cmd/snap-bootstrap/blkid/test-data/repart.d/10-ext4.conf new file mode 100644 index 00000000000..f11e2304b77 --- /dev/null +++ b/cmd/snap-bootstrap/blkid/test-data/repart.d/10-ext4.conf @@ -0,0 +1,5 @@ +[Partition] +Type=ea0dd8e5-b23e-4633-9137-adbd12633746 +UUID=93fd7d8f-a662-4451-a03f-c065f2c2e1ab +Label=thelabel +Format=ext4 diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts.go b/cmd/snap-bootstrap/cmd_initramfs_mounts.go index 029e27ce381..7ca14580ac6 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts.go @@ -1815,30 +1815,62 @@ func getNonUEFISystemDisk(fallbacklabel string) (string, error) { return candidate, nil } -// mountNonDataPartitionMatchingKernelDisk will select the partition to mount at -// dir, using the boot package function FindPartitionUUIDForBootedKernelDisk to -// determine what partition the booted kernel came from. If which disk the -// kernel came from cannot be determined, then it will fallback to mounting via -// the specified disk label. +// mountNonDataPartitionMatchingKernelDisk will select the partition +// to mount at dir using the boot package function +// FindPartitionUUIDForBootedKernelDisk to determine what partition +// the booted kernel came from. +// +// If "snap-bootstrap scan-disk" was run as part of udev it will +// restrict the search of the partition from the boot disk it found. +// +// If "snap-bootstrap scan-disk" is not in use (legacy case), +// it will look for any partition that matches the boot. +// +// If which disk the kernel came from cannot be determined, then it +// will fallback to mounting via the specified disk label. If +// "snap-bootstrap scan-disk" was used, it will restrict the search to +// the boot disk. func mountNonDataPartitionMatchingKernelDisk(dir, fallbacklabel string, opts *systemdMountOptions) error { - partuuid, err := bootFindPartitionUUIDForBootedKernelDisk() var partSrc string - if err == nil { - // TODO: the by-partuuid is only available on gpt disks, on mbr we need - // to use by-uuid or by-id - partSrc = filepath.Join("/dev/disk/by-partuuid", partuuid) - } else { - partSrc, err = getNonUEFISystemDisk(fallbacklabel) + + if osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/dev/disk/snapd/disk")) { + disk, err := disks.DiskFromDeviceName("/dev/disk/snapd/disk") if err != nil { return err } - } + partuuid, err := bootFindPartitionUUIDForBootedKernelDisk() + if err == nil { + partition, err := disk.FindMatchingPartitionWithPartUUID(partuuid) + if err != nil { + return err + } + partSrc = partition.KernelDeviceNode + } else { + partition, err := disk.FindMatchingPartitionWithFsLabel(fallbacklabel) + if err != nil { + return err + } + partSrc = partition.KernelDeviceNode + } + } else { + partuuid, err := bootFindPartitionUUIDForBootedKernelDisk() + if err == nil { + // TODO: the by-partuuid is only available on gpt disks, on mbr we need + // to use by-uuid or by-id + partSrc = filepath.Join("/dev/disk/by-partuuid", partuuid) + } else { + partSrc, err = getNonUEFISystemDisk(fallbacklabel) + if err != nil { + return err + } + } - // The partition uuid is read from the EFI variables. At this point - // the kernel may not have initialized the storage HW yet so poll - // here. - if err := waitForDevice(partSrc); err != nil { - return err + // The partition uuid is read from the EFI variables. At this point + // the kernel may not have initialized the storage HW yet so poll + // here. + if err := waitForDevice(partSrc); err != nil { + return err + } } return doSystemdMount(partSrc, dir, opts) } diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go index bd69f170033..2d70a47b252 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go @@ -34,6 +34,7 @@ import ( "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/bootloadertest" main "github.com/snapcore/snapd/cmd/snap-bootstrap" @@ -1489,3 +1490,189 @@ func (s *initramfsMountsSuite) TestGetDiskNotUEFISeedPartCapitalFsLabel(c *C) { c.Assert(err.Error(), Equals, `filesystem label "UBUNTU-BOOT" not found`) c.Assert(path, Equals, "") } + +func (s *initramfsClassicMountsSuite) TestInitramfsMountsObeyDevLink(c *C) { + s.mockProcCmdlineContent(c, "snapd_system_disk=/should/be/ignored snapd_recovery_mode=run") + + devLink := filepath.Join(dirs.GlobalRootDir, "/dev/disk/snapd/disk") + c.Assert(os.MkdirAll(filepath.Dir(devLink), 0755), IsNil) + fakeDevice := filepath.Join(dirs.GlobalRootDir, "/dev/sda") + c.Assert(os.WriteFile(fakeDevice, []byte{}, 0644), IsNil) + c.Assert(os.Symlink(fakeDevice, devLink), IsNil) + + restoreDiskMapping := disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/sda": defaultBootWithSaveDisk, + }) + defer restoreDiskMapping() + + restore := main.MockPartitionUUIDForBootedKernelDisk("ubuntu-boot-partuuid") + defer restore() + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + { + "/dev/sda3", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + nil, + }, + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + writeGadget(c, "ubuntu-seed", "system-seed", "") + + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsDataDir) + c.Assert(err, IsNil) + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) +} + +func (s *initramfsClassicMountsSuite) TestInitramfsMountsObeyDevLinkFallback(c *C) { + s.mockProcCmdlineContent(c, "snapd_system_disk=/should/be/ignored snapd_recovery_mode=run") + + devLink := filepath.Join(dirs.GlobalRootDir, "/dev/disk/snapd/disk") + c.Assert(os.MkdirAll(filepath.Dir(devLink), 0755), IsNil) + fakeDevice := filepath.Join(dirs.GlobalRootDir, "/dev/sda") + c.Assert(os.WriteFile(fakeDevice, []byte{}, 0644), IsNil) + c.Assert(os.Symlink(fakeDevice, devLink), IsNil) + + restoreDiskMapping := disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/sda": defaultBootWithSaveDisk, + }) + defer restoreDiskMapping() + + // NO UEFI + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + { + "/dev/sda3", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + nil, + }, + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + writeGadget(c, "ubuntu-seed", "system-seed", "") + + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsDataDir) + c.Assert(err, IsNil) + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) +} + +func (s *initramfsClassicMountsSuite) TestInitramfsMountsInstallObeyDevLink(c *C) { + s.mockProcCmdlineContent(c, "snapd_system_disk=/should/be/ignored snapd_recovery_mode=install snapd_recovery_system="+s.sysLabel) + + devLink := filepath.Join(dirs.GlobalRootDir, "/dev/disk/snapd/disk") + c.Assert(os.MkdirAll(filepath.Dir(devLink), 0755), IsNil) + fakeDevice := filepath.Join(dirs.GlobalRootDir, "/dev/sda") + c.Assert(os.WriteFile(fakeDevice, []byte{}, 0644), IsNil) + c.Assert(os.Symlink(fakeDevice, devLink), IsNil) + + restoreDiskMapping := disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/sda": defaultBootWithSaveDisk, + }) + defer restoreDiskMapping() + + restore := main.MockPartitionUUIDForBootedKernelDisk("ubuntu-seed-partuuid") + defer restore() + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + { + "/dev/sda2", + boot.InitramfsUbuntuSeedDir, + needsFsckAndNoSuidNoDevNoExecMountOpts, + nil, + }, + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + nil, + }, + }, nil) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) +} diff --git a/cmd/snap-bootstrap/cmd_scan_disk.go b/cmd/snap-bootstrap/cmd_scan_disk.go new file mode 100644 index 00000000000..7cab42a6c37 --- /dev/null +++ b/cmd/snap-bootstrap/cmd_scan_disk.go @@ -0,0 +1,301 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +/* + * This tool expects to be called from a udev rules file such as: + * + * ``` + * SUBSYSTEM!="block", GOTO="ubuntu_core_partitions_end" + * + * ENV{DEVTYPE}=="disk", IMPORT{program}="/usr/lib/snapd/snap-bootstrap scan-disk" + * ENV{DEVTYPE}=="partition", IMPORT{parent}="UBUNTU_DISK" + * ENV{UBUNTU_DISK}!="1", GOTO="ubuntu_core_partitions_end" + * + * ENV{DEVTYPE}=="disk", SYMLINK+="disk/snapd/disk" + * ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-seed", SYMLINK+="disk/snapd/ubuntu-seed" + * ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-boot", SYMLINK+="disk/snapd/ubuntu-boot" + * ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-data", ENV{ID_FS_TYPE}=="crypto_LUKS", SYMLINK+="disk/snapd/ubuntu-data-luks" + * ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-data", ENV{ID_FS_TYPE}!="crypto_LUKS", SYMLINK+="disk/snapd/ubuntu-data" + * ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-save", ENV{ID_FS_TYPE}=="crypto_LUKS", SYMLINK+="disk/snapd/ubuntu-save-luks" + * ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-save", ENV{ID_FS_TYPE}!="crypto_LUKS", SYMLINK+="disk/snapd/ubuntu-save" + * + * LABEL="ubuntu_core_partitions_end" + * + * ENV{DM_UUID}=="CRYPT-*", ENV{DM_NAME}=="ubuntu-data-*", SYMLINK+="disk/snapd/ubuntu-data" + * ENV{DM_UUID}=="CRYPT-*", ENV{DM_NAME}=="ubuntu-save-*", SYMLINK+="disk/snapd/ubuntu-save" + * ``` + * + * See + * core-initrd/latest/factory/usr/lib/udev/rules.d/90-ubuntu-core-partitions.rules + * for implementation. + * + * Note that symlink /dev/disk/snapd/disk can be expected by + * snap-bootstrap. In that case, snap-initramfs-mounts.service should + * have: + * + * ``` + * BindsTo=dev-disk-snapd--disk.device + * After=dev-disk-snapd-disk.device + * ``` + * + */ + +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/cmd/snap-bootstrap/blkid" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil/kcmdline" +) + +func init() { + const ( + short = "Verify that a disk is the booting disk" + long = "This tool is expected to be called from udev" + ) + + addCommandBuilder(func(parser *flags.Parser) { + if _, err := parser.AddCommand("scan-disk", short, long, &cmdScanDisk{}); err != nil { + panic(err) + } + }) +} + +type cmdScanDisk struct{} + +func (c *cmdScanDisk) Execute([]string) error { + return ScanDisk(os.Stdout) +} + +type Partition struct { + Name string + UUID string +} + +func isGpt(probe blkid.AbstractBlkidProbe) bool { + pttype, err := probe.LookupValue("PTTYPE") + if err != nil { + return false + } + return pttype == "gpt" +} + +func probePartitions(node string) ([]Partition, error) { + probe, err := blkid.NewProbeFromFilename(node) + if err != nil { + return nil, err + } + defer probe.Close() + + probe.EnablePartitions(true) + probe.SetPartitionsFlags(blkid.BLKID_PARTS_ENTRY_DETAILS) + probe.EnableSuperblocks(true) + + if err := probe.DoSafeprobe(); err != nil { + return nil, err + } + + if !isGpt(probe) { + return nil, nil + } + + partitions, err := probe.GetPartitions() + if err != nil { + return nil, err + } + + ret := make([]Partition, 0) + for _, partition := range partitions.GetPartitions() { + label := partition.GetName() + uuid := partition.GetUUID() + ret = append(ret, Partition{label, uuid}) + } + + return ret, nil +} + +func samePath(a, b string) (bool, error) { + aSt, err := os.Stat(a) + if err != nil { + return false, err + } + bSt, err := os.Stat(b) + if err != nil { + return false, err + } + return os.SameFile(aSt, bSt), nil +} + +func scanDiskNodeFallback(output io.Writer, node string) error { + var fallbackPartition string + + partitions, err := probePartitions(node) + if err != nil { + return fmt.Errorf("cannot get partitions: %s\n", err) + } + /* + * If LoaderDevicePartUUID was not set, it is probably because + * we did not boot with UEFI. In that case we try to detect + * disk with partition labels. + */ + + mode, _, err := boot.ModeAndRecoverySystemFromKernelCommandLine() + if err != nil { + return err + } + switch mode { + case "recover": + fallbackPartition = "ubuntu-seed" + case "install": + fallbackPartition = "ubuntu-seed" + case "factory-reset": + fallbackPartition = "ubuntu-seed" + case "run": + fallbackPartition = "ubuntu-boot" + case "cloudimg-rootfs": + fallbackPartition = "ubuntu-boot" + default: + return fmt.Errorf("internal error: mode not handled") + } + + /* + * If we are not in UEFI mode and snapd_system_disk is + * defined, we need to verify the disk also matches that. If + * not, we just return, ignoring this disk. + */ + values, err := kcmdline.KeyValues("snapd_system_disk") + if err != nil { + return fmt.Errorf("cannot read kernel command line: %s\n", err) + } + + if value, ok := values["snapd_system_disk"]; ok { + var currentPath string + var expectedPath string + if strings.HasPrefix(value, "/dev/") || !strings.HasPrefix(value, "/") { + name := strings.TrimPrefix(value, "/dev/") + expectedPath = fmt.Sprintf("/dev/%s", name) + currentPath = node + } else { + expectedPath = value + currentPath = osGetenv("DEVPATH") + } + + same, err := samePath(filepath.Join(dirs.GlobalRootDir, expectedPath), + filepath.Join(dirs.GlobalRootDir, currentPath)) + if err != nil { + return fmt.Errorf("cannot check snapd_system_disk kernel parameter: %s\n", err) + } + if !same { + /* + * This block device is not the device + * requested from the command line. But this + * is not an error. There are lots of block + * devices. + */ + return nil + } + } + + for _, part := range partitions { + if part.Name == fallbackPartition { + fmt.Fprintf(output, "UBUNTU_DISK=1\n") + return nil + } + } + + /* + * We have found the block device is not a boot device. But + * this is not an error. There are plenty of block devices + * that are not the boot device. + */ + return nil +} + +func scanDiskNode(output io.Writer, node string) error { + /* + * We need to find out if the given node contains the ESP that + * was booted. The boot loader will set + * LoaderDevicePartUUID. We will need to scan all the + * partitions for that UUID. + */ + + bootUUID, err := bootFindPartitionUUIDForBootedKernelDisk() + if err != nil { + return scanDiskNodeFallback(output, node) + } + + partitions, err := probePartitions(node) + if err != nil { + return fmt.Errorf("cannot get partitions: %s\n", err) + } + + /* + * Now we scan the partitions. We need to find the partition + * grub booted from. + */ + found := false + hasSeed := false + hasBoot := false + for _, part := range partitions { + if part.UUID == bootUUID { + /* + * We have just found the ESP boot partition! + */ + found = true + } + + if part.Name == "ubuntu-seed" { + hasSeed = true + } else if part.Name == "ubuntu-boot" { + hasBoot = true + } + } + + /* + * We now print the result if we confirmed we found the boot ESP. + */ + if found && (hasSeed || hasBoot) { + fmt.Fprintf(output, "UBUNTU_DISK=1\n") + } + + /* + * We have found the block device is not a boot device. But + * this is not an error. There are plenty of block devices + * that are not the boot device. + */ + return nil +} + +func ScanDisk(output io.Writer) error { + devname := osGetenv("DEVNAME") + if osGetenv("DEVTYPE") == "disk" { + return scanDiskNode(output, devname) + } else { + return fmt.Errorf("unknown type for block device %s\n", devname) + } +} diff --git a/cmd/snap-bootstrap/cmd_scan_disk_test.go b/cmd/snap-bootstrap/cmd_scan_disk_test.go new file mode 100644 index 00000000000..e2a13ef25d7 --- /dev/null +++ b/cmd/snap-bootstrap/cmd_scan_disk_test.go @@ -0,0 +1,292 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "bufio" + "bytes" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + main "github.com/snapcore/snapd/cmd/snap-bootstrap" + + "github.com/snapcore/snapd/cmd/snap-bootstrap/blkid" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil/kcmdline" + "github.com/snapcore/snapd/testutil" +) + +type scanDiskSuite struct { + testutil.BaseTest + + probeMap map[string]*blkid.FakeBlkidProbe + cmdlineFile string + env map[string]string +} + +var _ = Suite(&scanDiskSuite{}) + +func (s *scanDiskSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + dirs.SetRootDir(c.MkDir()) + + s.probeMap = make(map[string]*blkid.FakeBlkidProbe) + cleanupBlkid := blkid.MockBlkidMap(s.probeMap) + s.AddCleanup(cleanupBlkid) + + disk_values := make(map[string]string) + disk_values["PTTYPE"] = "gpt" + disk_probe := blkid.BuildFakeProbe(disk_values) + for _, partition := range []struct { + node string + label string + uuid string + }{ + {"/dev/foop1", "ubuntu-seed", "6ae5a792-912e-43c9-ac92-e36723bbda12"}, + {"/dev/foop2", "ubuntu-boot", "29261148-b8ba-4335-b934-417ed71e9e91"}, + {"/dev/foop3", "ubuntu-data-enc", "c01a272d-fc72-40de-92fb-242c2da82533"}, + {"/dev/foop4", "ubuntu-save-enc", "050ee326-ab58-4eb4-ba7d-13694b2d0c8a"}, + } { + values := make(map[string]string) + values["PART_ENTRY_UUID"] = partition.uuid + s.probeMap[partition.node] = blkid.BuildFakeProbe(values) + disk_probe.AddPartition(partition.label, partition.uuid) + } + s.probeMap["/dev/foo"] = disk_probe + + s.cmdlineFile = filepath.Join(c.MkDir(), "proc-cmdline") + err := os.WriteFile(s.cmdlineFile, []byte("snapd_recovery_mode=run"), 0644) + c.Assert(err, IsNil) + cmdlineCleanup := kcmdline.MockProcCmdline(s.cmdlineFile) + s.AddCleanup(cmdlineCleanup) + + s.env = make(map[string]string) + cleanupEnv := main.MockOsGetenv(func(envVar string) string { + return s.env[envVar] + }) + s.AddCleanup(cleanupEnv) +} + +func (s *scanDiskSuite) setCmdLine(c *C, value string) { + err := os.WriteFile(s.cmdlineFile, []byte(value), 0644) + c.Assert(err, IsNil) +} + +type outputScanner struct { + buffer *bytes.Buffer +} + +func newBuffer() *outputScanner { + return &outputScanner{&bytes.Buffer{}} +} + +func (o *outputScanner) File() *bytes.Buffer { + return o.buffer +} + +func (o *outputScanner) GetLines() map[string]struct{} { + scanner := bufio.NewScanner(bytes.NewReader(o.buffer.Bytes())) + lines := make(map[string]struct{}) + for scanner.Scan() { + lines[scanner.Text()] = struct{}{} + } + return lines +} + +func (s *scanDiskSuite) TestDetectBootDisk(c *C) { + main.MockPartitionUUIDForBootedKernelDisk("29261148-b8ba-4335-b934-417ed71e9e91") + + s.env["DEVNAME"] = "/dev/foo" + s.env["DEVTYPE"] = "disk" + + output := newBuffer() + err := main.ScanDisk(output.File()) + c.Assert(err, IsNil) + lines := output.GetLines() + + _, hasDisk := lines["UBUNTU_DISK=1"] + c.Assert(hasDisk, Equals, true) + c.Assert(len(lines), Equals, 1) +} + +func (s *scanDiskSuite) TestDetectBootDiskNotUEFIBoot(c *C) { + main.MockPartitionUUIDForBootedKernelDisk("ffffffff-ffff-ffff-ffff-ffffffffffff") + + s.env["DEVNAME"] = "/dev/foo" + s.env["DEVTYPE"] = "disk" + + output := newBuffer() + err := main.ScanDisk(output.File()) + c.Assert(err, IsNil) + lines := output.GetLines() + + c.Assert(len(lines), Equals, 0) +} + +func (s *scanDiskSuite) TestDetectBootDiskFallback(c *C) { + main.MockPartitionUUIDForBootedKernelDisk("") + + s.env["DEVNAME"] = "/dev/foo" + s.env["DEVTYPE"] = "disk" + + output := newBuffer() + err := main.ScanDisk(output.File()) + c.Assert(err, IsNil) + lines := output.GetLines() + + _, hasDisk := lines["UBUNTU_DISK=1"] + c.Assert(hasDisk, Equals, true) + c.Assert(len(lines), Equals, 1) +} + +func (s *scanDiskSuite) TestDetectBootDiskFallbackInstall(c *C) { + s.setCmdLine(c, "snapd_recovery_mode=install snapd_recovery_system=20191118") + s.env["DEVNAME"] = "/dev/foo" + s.env["DEVTYPE"] = "disk" + + disk_values := make(map[string]string) + disk_values["PTTYPE"] = "gpt" + disk_probe := blkid.BuildFakeProbe(disk_values) + disk_probe.AddPartition("ubuntu-seed", "6ae5a792-912e-43c9-ac92-e36723bbda12") + s.probeMap["/dev/foo"] = disk_probe + delete(s.probeMap, "/dev/foop2") + delete(s.probeMap, "/dev/foop3") + delete(s.probeMap, "/dev/foop4") + + output := newBuffer() + err := main.ScanDisk(output.File()) + c.Assert(err, IsNil) + lines := output.GetLines() + + _, hasDisk := lines["UBUNTU_DISK=1"] + c.Assert(hasDisk, Equals, true) + c.Assert(len(lines), Equals, 1) +} + +func (s *scanDiskSuite) TestDetectBootDiskFallbackMissingBoot(c *C) { + s.env["DEVNAME"] = "/dev/foo" + s.env["DEVTYPE"] = "disk" + + disk_values := make(map[string]string) + disk_values["PTTYPE"] = "gpt" + disk_probe := blkid.BuildFakeProbe(disk_values) + disk_probe.AddPartition("ubuntu-seed", "6ae5a792-912e-43c9-ac92-e36723bbda12") + disk_probe.AddPartition("ubuntu-data-enc", "c01a272d-fc72-40de-92fb-242c2da82533") + disk_probe.AddPartition("ubuntu-save-enc", "050ee326-ab58-4eb4-ba7d-13694b2d0c8a") + s.probeMap["/dev/foo"] = disk_probe + delete(s.probeMap, "/dev/foop2") + + output := newBuffer() + err := main.ScanDisk(output.File()) + c.Assert(err, IsNil) + lines := output.GetLines() + + c.Assert(len(lines), Equals, 0) +} + +func (s *scanDiskSuite) TestDetectBootDiskFallbackMissingSeedRecover(c *C) { + s.setCmdLine(c, "snapd_recovery_mode=recover") + + s.env["DEVNAME"] = "/dev/foo" + s.env["DEVTYPE"] = "disk" + + disk_values := make(map[string]string) + disk_values["PTTYPE"] = "gpt" + disk_probe := blkid.BuildFakeProbe(disk_values) + disk_probe.AddPartition("ubuntu-boot", "29261148-b8ba-4335-b934-417ed71e9e91") + disk_probe.AddPartition("ubuntu-data-enc", "c01a272d-fc72-40de-92fb-242c2da82533") + disk_probe.AddPartition("ubuntu-save-enc", "050ee326-ab58-4eb4-ba7d-13694b2d0c8a") + s.probeMap["/dev/foo"] = disk_probe + delete(s.probeMap, "/dev/foop1") + + output := newBuffer() + err := main.ScanDisk(output.File()) + c.Assert(err, IsNil) + lines := output.GetLines() + + c.Assert(len(lines), Equals, 0) +} + +func (s *scanDiskSuite) TestDetectBootDiskFallbackKernelParam(c *C) { + devFoo := filepath.Join(dirs.GlobalRootDir, "/dev/foo") + c.Assert(os.MkdirAll(filepath.Dir(devFoo), 0755), IsNil) + c.Assert(os.WriteFile(devFoo, []byte{}, 0644), IsNil) + + s.setCmdLine(c, "snapd_system_disk=/dev/foo snapd_recovery_mode=run") + + s.env["DEVPATH"] = "/sys/devices/foo" + s.env["DEVNAME"] = "/dev/foo" + s.env["DEVTYPE"] = "disk" + + output := newBuffer() + err := main.ScanDisk(output.File()) + c.Assert(err, IsNil) + lines := output.GetLines() + + _, hasDisk := lines["UBUNTU_DISK=1"] + c.Assert(hasDisk, Equals, true) + c.Assert(len(lines), Equals, 1) +} + +func (s *scanDiskSuite) TestDetectBootDiskFallbackKernelParamDevPath(c *C) { + devFoo := filepath.Join(dirs.GlobalRootDir, "/sys/devices/foo") + c.Assert(os.MkdirAll(filepath.Dir(devFoo), 0755), IsNil) + c.Assert(os.WriteFile(devFoo, []byte{}, 0644), IsNil) + + s.setCmdLine(c, "snapd_system_disk=/sys/devices/foo snapd_recovery_mode=run") + + s.env["DEVPATH"] = "/sys/devices/foo" + s.env["DEVNAME"] = "/dev/foo" + s.env["DEVTYPE"] = "disk" + + output := newBuffer() + err := main.ScanDisk(output.File()) + c.Assert(err, IsNil) + lines := output.GetLines() + + _, hasDisk := lines["UBUNTU_DISK=1"] + c.Assert(hasDisk, Equals, true) + c.Assert(len(lines), Equals, 1) +} + +func (s *scanDiskSuite) TestDetectBootDiskFallbackKernelParamNotMatching(c *C) { + devFoo := filepath.Join(dirs.GlobalRootDir, "/dev/foo") + c.Assert(os.MkdirAll(filepath.Dir(devFoo), 0755), IsNil) + c.Assert(os.WriteFile(devFoo, []byte{}, 0644), IsNil) + devBar := filepath.Join(dirs.GlobalRootDir, "/dev/bar") + c.Assert(os.MkdirAll(filepath.Dir(devBar), 0755), IsNil) + c.Assert(os.WriteFile(devBar, []byte{}, 0644), IsNil) + + // Ask for /dev/bar instead of /dev/foo + s.setCmdLine(c, "snapd_system_disk=/dev/bar snapd_recovery_mode=run") + + s.env["DEVPATH"] = "/sys/devices/foo" + s.env["DEVNAME"] = "/dev/foo" + s.env["DEVTYPE"] = "disk" + + output := newBuffer() + err := main.ScanDisk(output.File()) + c.Assert(err, IsNil) + lines := output.GetLines() + c.Check(len(lines), Equals, 0) +} diff --git a/core-initrd/latest/factory/usr/lib/systemd/system/snap-initramfs-mounts.service b/core-initrd/latest/factory/usr/lib/systemd/system/snap-initramfs-mounts.service index a1fe3ae8bb1..90e84f2b179 100644 --- a/core-initrd/latest/factory/usr/lib/systemd/system/snap-initramfs-mounts.service +++ b/core-initrd/latest/factory/usr/lib/systemd/system/snap-initramfs-mounts.service @@ -7,6 +7,9 @@ DefaultDependencies=no After=sysinit.target Before=initrd-root-device.target +BindsTo=dev-disk-snapd-disk.device +After=dev-disk-snapd-disk.device + Wants=dbus.socket After=dbus.socket diff --git a/core-initrd/latest/factory/usr/lib/udev/rules.d/90-ubuntu-core-partitions.rules b/core-initrd/latest/factory/usr/lib/udev/rules.d/90-ubuntu-core-partitions.rules new file mode 100644 index 00000000000..fdf52c78749 --- /dev/null +++ b/core-initrd/latest/factory/usr/lib/udev/rules.d/90-ubuntu-core-partitions.rules @@ -0,0 +1,18 @@ +SUBSYSTEM!="block", GOTO="ubuntu_core_partitions_end" + +ENV{DEVTYPE}=="disk", IMPORT{program}="/usr/lib/snapd/snap-bootstrap scan-disk" +ENV{DEVTYPE}=="partition", IMPORT{parent}="UBUNTU_DISK" +ENV{UBUNTU_DISK}!="1", GOTO="ubuntu_core_partitions_end" + +ENV{DEVTYPE}=="disk", SYMLINK+="disk/snapd/disk" +ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-seed", SYMLINK+="disk/snapd/ubuntu-seed" +ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-boot", SYMLINK+="disk/snapd/ubuntu-boot" +ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-data", ENV{ID_FS_TYPE}=="crypto_LUKS", SYMLINK+="disk/snapd/ubuntu-data-luks" +ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-data", ENV{ID_FS_TYPE}!="crypto_LUKS", SYMLINK+="disk/snapd/ubuntu-data" +ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-save", ENV{ID_FS_TYPE}=="crypto_LUKS", SYMLINK+="disk/snapd/ubuntu-save-luks" +ENV{DEVTYPE}=="partition", ENV{ID_PART_ENTRY_NAME}=="ubuntu-save", ENV{ID_FS_TYPE}!="crypto_LUKS", SYMLINK+="disk/snapd/ubuntu-save" + +LABEL="ubuntu_core_partitions_end" + +ENV{DM_UUID}=="CRYPT-*", ENV{DM_NAME}=="ubuntu-data-*", SYMLINK+="disk/snapd/ubuntu-data" +ENV{DM_UUID}=="CRYPT-*", ENV{DM_NAME}=="ubuntu-save-*", SYMLINK+="disk/snapd/ubuntu-save" diff --git a/osutil/disks/disks.go b/osutil/disks/disks.go index cc78f11257e..4f04f14870a 100644 --- a/osutil/disks/disks.go +++ b/osutil/disks/disks.go @@ -35,6 +35,11 @@ type Options struct { // Disk is a single physical disk device that contains partitions. type Disk interface { + // FindMatchingPartitionWithUUID finds a partition with a matching + // partition UUID on the disk. If no matching partition is found, + // a PartitionNotFoundError will be returned. + FindMatchingPartitionWithPartUUID(string) (Partition, error) + // FindMatchingPartitionWithFsLabel finds the partition with a matching // filesystem label on the disk. Note that for non-ascii labels like // "Some label", the label will be encoded using \x for potentially @@ -207,6 +212,8 @@ func (e PartitionNotFoundError) Error() string { t = "partition label" case "filesystem-label": t = "filesystem label" + case "partition-uuid": + t = "partition uuid" default: return fmt.Sprintf("searching with unknown search type %q and search query %q did not return a partition", e.SearchType, e.SearchQuery) } diff --git a/osutil/disks/disks_linux.go b/osutil/disks/disks_linux.go index 39978d46340..1b4bdf3b80f 100644 --- a/osutil/disks/disks_linux.go +++ b/osutil/disks/disks_linux.go @@ -848,6 +848,23 @@ func (d *disk) populatePartitions() error { return nil } +func (d *disk) FindMatchingPartitionWithPartUUID(uuid string) (Partition, error) { + if err := d.populatePartitions(); err != nil { + return Partition{}, err + } + + for _, p := range d.partitions { + if p.PartitionUUID == uuid { + return p, nil + } + } + + return Partition{}, PartitionNotFoundError{ + SearchType: "partition-uuid", + SearchQuery: uuid, + } +} + func (d *disk) FindMatchingPartitionWithPartLabel(label string) (Partition, error) { // always encode the label encodedLabel := BlkIDEncodeLabel(label) diff --git a/osutil/disks/disks_linux_test.go b/osutil/disks/disks_linux_test.go index d38ad981be7..31751e6ad40 100644 --- a/osutil/disks/disks_linux_test.go +++ b/osutil/disks/disks_linux_test.go @@ -2079,3 +2079,21 @@ func (s *diskSuite) TestDevlinks(c *C) { _, err = disks.Devlinks("/dev/some/error") c.Check(err, ErrorMatches, `cannot process udev properties: some error`) } + +func (s *diskSuite) TestFindMatchingPartitionWithPartUUID(c *C) { + restore := disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/vda": gadgettest.VMSystemVolumeDiskMappingSeedFsLabelCaps, + }) + defer restore() + + d, err := disks.DiskFromDeviceName("/dev/vda") + c.Assert(err, IsNil) + + p, err := d.FindMatchingPartitionWithPartUUID("ade3ba65-7831-fd40-bbe2-e01c9774ed5b") + c.Assert(err, IsNil) + c.Check(p.KernelDeviceNode, Equals, "/dev/vda2") + c.Check(p.PartitionUUID, Equals, "ade3ba65-7831-fd40-bbe2-e01c9774ed5b") + + _, err = d.FindMatchingPartitionWithPartUUID("fe1ec853-15b1-4c72-a207-6a9b185dcbbb") + c.Assert(err, ErrorMatches, "partition uuid \"fe1ec853-15b1-4c72-a207-6a9b185dcbbb\" not found") +} diff --git a/osutil/disks/mockdisk.go b/osutil/disks/mockdisk.go index 5583c51d4b1..7c5f550381b 100644 --- a/osutil/disks/mockdisk.go +++ b/osutil/disks/mockdisk.go @@ -62,6 +62,22 @@ type MockDiskMapping struct { DiskSizeInBytes uint64 } +func (d *MockDiskMapping) FindMatchingPartitionWithPartUUID(uuid string) (Partition, error) { + // TODO: this should just iterate over the static list when that is a thing + osutil.MustBeTestBinary("mock disks only to be used in tests") + + for _, p := range d.Structure { + if p.PartitionUUID == uuid { + return p, nil + } + } + + return Partition{}, PartitionNotFoundError{ + SearchType: "partition-uuid", + SearchQuery: uuid, + } +} + // FindMatchingPartitionUUIDWithFsLabel returns a matching PartitionUUID // for the specified filesystem label if it exists. Part of the Disk interface. func (d *MockDiskMapping) FindMatchingPartitionWithFsLabel(label string) (Partition, error) { diff --git a/osutil/disks/mockdisk_test.go b/osutil/disks/mockdisk_test.go index 72f8b0b656c..0b5257fedd3 100644 --- a/osutil/disks/mockdisk_test.go +++ b/osutil/disks/mockdisk_test.go @@ -395,6 +395,14 @@ func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMapping(c *C) { PartitionUUID: "part1", }) + part, err = foundDisk.FindMatchingPartitionWithPartUUID("part1") + c.Assert(err, IsNil) + c.Assert(part, DeepEquals, disks.Partition{ + PartitionLabel: "part-label1", + FilesystemLabel: "label1", + PartitionUUID: "part1", + }) + // and it has the right set of partitions parts, err := foundDisk.Partitions() c.Assert(err, IsNil) @@ -447,6 +455,14 @@ func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMapping(c *C) { PartitionUUID: "part2", }) + part, err = foundDisk2.FindMatchingPartitionWithPartUUID("part2") + c.Assert(err, IsNil) + c.Assert(part, DeepEquals, disks.Partition{ + PartitionLabel: "part-label2", + FilesystemLabel: "label2", + PartitionUUID: "part2", + }) + // and it has the right set of partitions parts, err = foundDisk2.Partitions() c.Assert(err, IsNil) diff --git a/packaging/debian-sid/control b/packaging/debian-sid/control index c0fd9a08e44..78374c4d79e 100644 --- a/packaging/debian-sid/control +++ b/packaging/debian-sid/control @@ -43,6 +43,7 @@ Build-Depends: autoconf, grub-common, indent, libapparmor-dev, + libblkid-dev, libcap-dev, libglib2.0-dev, liblzo2-dev, diff --git a/packaging/ubuntu-14.04/control b/packaging/ubuntu-14.04/control index 72fd10c18bb..1bc359a9591 100644 --- a/packaging/ubuntu-14.04/control +++ b/packaging/ubuntu-14.04/control @@ -23,6 +23,7 @@ Build-Depends: autoconf, indent, init-system-helpers, libapparmor-dev, + libblkid-dev, libcap-dev, libglib2.0-dev, libseccomp-dev (>= 2.1.1-1ubuntu1~trusty4), diff --git a/packaging/ubuntu-16.04/control b/packaging/ubuntu-16.04/control index c166025d58b..578a2d79ede 100644 --- a/packaging/ubuntu-16.04/control +++ b/packaging/ubuntu-16.04/control @@ -24,6 +24,7 @@ Build-Depends: autoconf, indent, init-system-helpers, libapparmor-dev, + libblkid-dev, libcap-dev, libfuse3-dev (>= 3.10.5-1) | libfuse-dev, libglib2.0-dev, diff --git a/tests/cross/go-build/task.yaml b/tests/cross/go-build/task.yaml index f7334b0f9c8..d73aa49528c 100644 --- a/tests/cross/go-build/task.yaml +++ b/tests/cross/go-build/task.yaml @@ -50,7 +50,7 @@ prepare: | EOF dpkg --add-architecture "$X_DEBARCH" apt --quiet -o Dpkg::Progress-Fancy=false update - apt --yes --quiet -o Dpkg::Progress-Fancy=false install "$X_GCC" libseccomp2:"$X_DEBARCH" libseccomp-dev:"$X_DEBARCH" + apt --yes --quiet -o Dpkg::Progress-Fancy=false install "$X_GCC" libseccomp2:"$X_DEBARCH" libseccomp-dev:"$X_DEBARCH" libblkid-dev:"$X_DEBARCH" restore: | rm -rf /tmp/cross-build diff --git a/tests/nested/manual/core22-basic/task.yaml b/tests/nested/manual/core22-basic/task.yaml index 297098fe87a..30f1eb84470 100644 --- a/tests/nested/manual/core22-basic/task.yaml +++ b/tests/nested/manual/core22-basic/task.yaml @@ -81,3 +81,14 @@ execute: | remote.exec "test -f /run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key" remote.exec "test -f /run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key" fi + + if os.query is-ubuntu-ge 24.04; then + remote.exec "udevadm info --query=name /dev/disk/snapd/disk" | MATCH "." + remote.exec "udevadm info --query=property --property=ID_PART_ENTRY_NAME --value /dev/disk/snapd/ubuntu-seed" | MATCH "^ubuntu-seed$" + remote.exec "udevadm info --query=property --property=ID_PART_ENTRY_NAME --value /dev/disk/snapd/ubuntu-boot" | MATCH "^ubuntu-boot$" + remote.exec "udevadm info --query=property --property=ID_PART_ENTRY_NAME --value /dev/disk/snapd/ubuntu-data-luks" | MATCH "^ubuntu-data$" + remote.exec "udevadm info --query=property --property=ID_PART_ENTRY_NAME --value /dev/disk/snapd/ubuntu-save-luks" | MATCH "^ubuntu-save$" + # TODO: when udev rules are available in core-base, we should also test those + #remote.exec "udevadm info --query=property --property=DM_NAME --value /dev/disk/snapd/ubuntu-data" | MATCH "^ubuntu-data" + #remote.exec "udevadm info --query=property --property=DM_NAME --value /dev/disk/snapd/ubuntu-save" | MATCH "^ubuntu-save" + fi diff --git a/tests/nested/manual/remodel-uc-to-next-version-fakestore/repack-kernel.sh b/tests/nested/manual/remodel-uc-to-next-version-fakestore/repack-kernel.sh index 16f24d890e3..d23190e1855 100644 --- a/tests/nested/manual/remodel-uc-to-next-version-fakestore/repack-kernel.sh +++ b/tests/nested/manual/remodel-uc-to-next-version-fakestore/repack-kernel.sh @@ -24,7 +24,7 @@ done add-apt-repository ppa:snappy-dev/image -y # TODO:FDEM:FIX: this will need changes for UC24. -apt-get install -y golang ubuntu-core-initramfs +apt-get install -y golang ubuntu-core-initramfs libblkid-dev snap download pc-kernel --channel="${version}/${branch}" --basename=pc-kernel --target-directory="${tmpd}" unsquashfs -d "${tmpd}/pc-kernel" "${tmpd}/pc-kernel.snap"