diff options
| author | Paweł Stołowski <stolowski@gmail.com> | 2022-01-10 13:50:05 +0100 |
|---|---|---|
| committer | Paweł Stołowski <stolowski@gmail.com> | 2022-01-10 13:50:05 +0100 |
| commit | 7812114ccfcb061f2d2e410b87e9a3e865da56f7 (patch) | |
| tree | 6f98bbef012df5e3a18617164797f91c9fcd975f | |
| parent | 6aee3a20e50b8a55eedea270907534e3c16f33ef (diff) | |
| parent | 1c39a66605eee95e759d5cfe08b889b2ed21a039 (diff) | |
Merge branch 'master' into notifications/close-notificationnotifications/close-notification
225 files changed, 7973 insertions, 636 deletions
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fc4bf47dfa..f828bd95bc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -44,9 +44,19 @@ jobs: uses: snapcore/action-build@v1 with: snapcraft-channel: 4.x/candidate - - name: Cache built artifact + - name: Cache and check built artifact run: | mkdir -p $(dirname "$CACHE_RESULT_STAMP") + unsquashfs snapd*.snap meta/snap.yaml usr/lib/snapd/info + if cat squashfs-root/meta/snap.yaml | grep -q "version:.*dirty.*"; then + echo "PR produces dirty snapd snap version" + cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt + exit 1 + elif cat squashfs-root/usr/lib/snapd/info | grep -q "VERSION=.*dirty.*"; then + echo "PR produces dirty internal snapd info version" + cat squashfs-root/usr/lib/snapd/info + exit 1 + fi cp -v *.snap "$(dirname $CACHE_RESULT_STAMP)/" - name: Uploading snapd snap artifact uses: actions/upload-artifact@v2 @@ -255,9 +265,8 @@ jobs: - debian-10-64 - debian-11-64 - debian-sid-64 - - fedora-33-64 - fedora-34-64 - - opensuse-15.2-64 + - fedora-35-64 - opensuse-15.3-64 - opensuse-tumbleweed-64 - ubuntu-14.04-64 diff --git a/asserts/sysdb/staging.go b/asserts/sysdb/staging.go index c26a48a466..78e9472d78 100644 --- a/asserts/sysdb/staging.go +++ b/asserts/sysdb/staging.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withtestkeys || withstagingkeys // +build withtestkeys withstagingkeys /* diff --git a/asserts/sysdb/testkeys.go b/asserts/sysdb/testkeys.go index 7b61564597..09a0f49793 100644 --- a/asserts/sysdb/testkeys.go +++ b/asserts/sysdb/testkeys.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withtestkeys // +build withtestkeys /* diff --git a/bootloader/assets/assetstesting.go b/bootloader/assets/assetstesting.go index 260092db3d..ea77045d43 100644 --- a/bootloader/assets/assetstesting.go +++ b/bootloader/assets/assetstesting.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withbootassetstesting // +build withbootassetstesting /* diff --git a/bootloader/assets/grub_cfg_asset.go b/bootloader/assets/grub_cfg_asset.go index 8b0ce2a34b..b8c4b14673 100644 --- a/bootloader/assets/grub_cfg_asset.go +++ b/bootloader/assets/grub_cfg_asset.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2021 Canonical Ltd + * Copyright (C) 2022 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 diff --git a/bootloader/assets/grub_recovery_cfg_asset.go b/bootloader/assets/grub_recovery_cfg_asset.go index 0b31ebb017..79448a1145 100644 --- a/bootloader/assets/grub_recovery_cfg_asset.go +++ b/bootloader/assets/grub_recovery_cfg_asset.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2021 Canonical Ltd + * Copyright (C) 2022 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 diff --git a/bootloader/withbootassettesting.go b/bootloader/withbootassettesting.go index 2f737d669d..3d20f87b71 100644 --- a/bootloader/withbootassettesting.go +++ b/bootloader/withbootassettesting.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withbootassetstesting // +build withbootassetstesting /* diff --git a/bootloader/withbootassettesting_test.go b/bootloader/withbootassettesting_test.go index 1e547019d1..587c9646ff 100644 --- a/bootloader/withbootassettesting_test.go +++ b/bootloader/withbootassettesting_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withbootassetstesting // +build withbootassetstesting /* diff --git a/build-aux/snap/snapcraft.yaml b/build-aux/snap/snapcraft.yaml index ac8ec3267b..7bfe654156 100644 --- a/build-aux/snap/snapcraft.yaml +++ b/build-aux/snap/snapcraft.yaml @@ -30,8 +30,15 @@ parts: plugin: nil source: . build-snaps: [go/1.13/stable] + # these packages are needed to call mkversion.sh in override-pull, all other + # dependencies are installed using apt-get build-dep + build-packages: + - git + - dpkg-dev override-pull: | snapcraftctl pull + # set version, this needs dpkg-parsechangelog (from dpkg-dev) and git + snapcraftctl set-version "$(./mkversion.sh --output-only)" # Ensure that ./debian/ packaging which we are about to use # matches the current `build-base` release. I.e. ubuntu-16.04 # for build-base:core, etc. @@ -41,9 +48,18 @@ parts: export DEBCONF_NONINTERACTIVE_SEEN=true sudo -E apt-get build-dep -y ./ ./get-deps.sh --skip-unused-check - # set version after installing dependencies so we have all the tools here - snapcraftctl set-version "$(./mkversion.sh --output-only)" override-build: | + # TODO: when something like "craftctl get-version" is ready, then we can + # use that, but until then, we have to re-run mkversion.sh to check if the + # version number was set as "dirty" from the override-pull step + if sh -x ./mkversion.sh --output-only | grep "dirty"; then + mkdir -p $SNAPCRAFT_PART_INSTALL/usr/lib/snapd + ( + echo "dirty git tree during build detected:" + git status + git diff + ) > $SNAPCRAFT_PART_INSTALL/usr/lib/snapd/dirty-git-tree-info.txt + fi # unset the LD_FLAGS and LD_LIBRARY_PATH vars that snapcraft sets for us # as those will point to the $SNAPCRAFT_STAGE which on re-builds will # contain things like libc and friends that confuse the debian package diff --git a/c-vendor/vendor.sh b/c-vendor/vendor.sh index 507461587c..f9804a35e8 100755 --- a/c-vendor/vendor.sh +++ b/c-vendor/vendor.sh @@ -7,9 +7,12 @@ set -e if [ ! -d ./squashfuse ]; then git clone https://github.com/vasi/squashfuse fi + # This is just tip/master as of Aug 30th 2021, there is no other # specific reason to use this. It works with both "libfuse-dev" and # "libfuse3-dev" which is important as 16.04 only have libfuse-dev # and 21.10 only has libfuse3-dev -(cd squashfuse && git checkout 74f4fe86ebd47a2fb7df5cb60d452354f977c72e) +if [ -d ./squashfuse/.git ]; then + (cd squashfuse && git checkout 74f4fe86ebd47a2fb7df5cb60d452354f977c72e) +fi diff --git a/cmd/.clangd b/cmd/.clangd index 617bbe62c5..6d8b27ae1f 100644 --- a/cmd/.clangd +++ b/cmd/.clangd @@ -1,2 +1,2 @@ CompileFlags: - Add: -I/usr/include/glib-2.0 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wno-missing-field-initializers -Wno-unused-parameter \ No newline at end of file + Add: [-I/usr/include/glib-2.0, -Wall, -Wextra, -Wmissing-prototypes, -Wstrict-prototypes, -Wno-missing-field-initializers, -Wno-unused-parameter] \ No newline at end of file diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts.go b/cmd/snap-bootstrap/cmd_initramfs_mounts.go index 7c50984b4d..fefd893191 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts.go @@ -86,6 +86,10 @@ var ( secbootLockSealedKeys func() error bootFindPartitionUUIDForBootedKernelDisk = boot.FindPartitionUUIDForBootedKernelDisk + + mountReadOnlyOptions = &systemdMountOptions{ + ReadOnly: true, + } ) func stampedAction(stamp string, action func() error) error { @@ -1309,7 +1313,7 @@ func generateMountsCommonInstallRecover(mst *initramfsMountsState) (model *asser dir := snapTypeToMountDir[essentialSnap.EssentialType] // TODO:UC20: we need to cross-check the kernel path with snapd_recovery_kernel used by grub - if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { + if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), mountReadOnlyOptions); err != nil { return nil, nil, err } } @@ -1557,7 +1561,7 @@ func generateMountsModeRun(mst *initramfsMountsState) error { if sn, ok := mounts[typ]; ok { dir := snapTypeToMountDir[typ] snapPath := filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), sn.Filename()) - if err := doSystemdMount(snapPath, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { + if err := doSystemdMount(snapPath, filepath.Join(boot.InitramfsRunMntDir, dir), mountReadOnlyOptions); err != nil { return err } } @@ -1570,8 +1574,7 @@ func generateMountsModeRun(mst *initramfsMountsState) error { if err != nil { return fmt.Errorf("cannot load metadata and verify snapd snap: %v", err) } - - return doSystemdMount(essSnaps[0].Path, filepath.Join(boot.InitramfsRunMntDir, "snapd"), nil) + return doSystemdMount(essSnaps[0].Path, filepath.Join(boot.InitramfsRunMntDir, "snapd"), mountReadOnlyOptions) } return nil diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go index 31ee416424..b87922daf4 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build nosecboot // +build nosecboot /* diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go index b095363641..9002e040a2 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go index 7d2733a770..5f5eed98e6 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go @@ -92,6 +92,9 @@ var ( needsNoSuidDiskMountOpts = &main.SystemdMountOptions{ NoSuid: true, } + snapMountOpts = &main.SystemdMountOptions{ + ReadOnly: true, + } seedPart = disks.Partition{ FilesystemLabel: "ubuntu-seed", @@ -501,6 +504,7 @@ func (s *initramfsMountsSuite) makeSeedSnapSystemdMount(typ snap.Type) systemdMo } mnt.what = filepath.Join(s.seedDir, "snaps", name+"_1.snap") mnt.where = filepath.Join(boot.InitramfsRunMntDir, dir) + mnt.opts = snapMountOpts return mnt } @@ -519,6 +523,7 @@ func (s *initramfsMountsSuite) makeRunSnapSystemdMount(typ snap.Type, sn snap.Pl mnt.what = filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), sn.Filename()) mnt.where = filepath.Join(boot.InitramfsRunMntDir, dir) + mnt.opts = snapMountOpts return mnt } @@ -1247,6 +1252,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.kernel.Filename()), @@ -1254,6 +1260,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.core20.Filename()), @@ -1261,6 +1268,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", "tmpfs", @@ -1415,6 +1423,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.kernel.Filename()), @@ -1422,6 +1431,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.core20.Filename()), @@ -1429,6 +1439,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", "tmpfs", @@ -1561,6 +1572,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.kernel.Filename()), @@ -1568,6 +1580,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.core20.Filename()), @@ -1575,6 +1588,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", "tmpfs", @@ -1746,6 +1760,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.kernel.Filename()), @@ -1753,6 +1768,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, }) } @@ -1870,6 +1886,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.kernel.Filename()), @@ -1877,6 +1894,7 @@ After=%[1]s "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, }) } diff --git a/cmd/snap-bootstrap/initramfs_systemd_mount.go b/cmd/snap-bootstrap/initramfs_systemd_mount.go index 84ae97e8fe..d0fef732cb 100644 --- a/cmd/snap-bootstrap/initramfs_systemd_mount.go +++ b/cmd/snap-bootstrap/initramfs_systemd_mount.go @@ -72,6 +72,8 @@ type systemdMountOptions struct { NoSuid bool // Bind indicates a bind mount Bind bool + // Read-only mount + ReadOnly bool } // doSystemdMount will mount "what" at "where" using systemd-mount(1) with @@ -135,6 +137,9 @@ func doSystemdMountImpl(what, where string, opts *systemdMountOptions) error { if opts.Bind { options = append(options, "bind") } + if opts.ReadOnly { + options = append(options, "ro") + } if len(options) > 0 { args = append(args, "--options="+strings.Join(options, ",")) } diff --git a/cmd/snap-bootstrap/initramfs_systemd_mount_test.go b/cmd/snap-bootstrap/initramfs_systemd_mount_test.go index 20a437a4ac..8349bffe54 100644 --- a/cmd/snap-bootstrap/initramfs_systemd_mount_test.go +++ b/cmd/snap-bootstrap/initramfs_systemd_mount_test.go @@ -22,6 +22,7 @@ package main_test import ( "fmt" "path/filepath" + "strings" "time" . "gopkg.in/check.v1" @@ -161,6 +162,16 @@ func (s *doSystemdMountSuite) TestDoSystemdMount(c *C) { isMountedReturns: []bool{true}, comment: "happy nosuid+bind", }, + { + what: "/run/mnt/data/some.snap", + where: "/run/mnt/base", + opts: &main.SystemdMountOptions{ + ReadOnly: true, + }, + timeNowTimes: []time.Time{testStart, testStart}, + isMountedReturns: []bool{true}, + comment: "happy ro", + }, } for _, t := range tt { @@ -221,6 +232,8 @@ func (s *doSystemdMountSuite) TestDoSystemdMount(c *C) { } else { c.Assert(err, IsNil) + c.Assert(len(cmd.Calls()), Equals, 1) + call := cmd.Calls()[0] args := []string{ "systemd-mount", t.what, t.where, "--no-pager", "--no-ask-password", } @@ -235,15 +248,30 @@ func (s *doSystemdMountSuite) TestDoSystemdMount(c *C) { if opts.NoWait { args = append(args, "--no-block") } - if opts.Bind && opts.NoSuid { - args = append(args, "--options=nosuid,bind") - } else if opts.NoSuid { - args = append(args, "--options=nosuid") - } else if opts.Bind { - args = append(args, "--options=bind") + c.Assert(call[:len(args)], DeepEquals, args) + foundNoSuid := false + foundBind := false + foundReadOnly := false + if len(call) != len(args) { + c.Assert(len(call), Equals, len(args)+1) + c.Assert(strings.HasPrefix(call[len(args)], "--options="), Equals, true) + for _, opt := range strings.Split(strings.TrimPrefix(call[len(args)], "--options="), ",") { + switch opt { + case "nosuid": + foundNoSuid = true + case "bind": + foundBind = true + case "ro": + foundReadOnly = true + default: + c.Logf("Option '%s' unexpected", opt) + c.Fail() + } + } } - - c.Assert(cmd.Calls(), DeepEquals, [][]string{args}) + c.Assert(foundNoSuid, Equals, opts.NoSuid) + c.Assert(foundBind, Equals, opts.Bind) + c.Assert(foundReadOnly, Equals, opts.ReadOnly) // check that the overrides are present if opts.Ephemeral is false, // or check the overrides are not present if opts.Ephemeral is true diff --git a/cmd/snap-device-helper/snap-device-helper-test.c b/cmd/snap-device-helper/snap-device-helper-test.c index bd8dd69877..fa40055dac 100644 --- a/cmd/snap-device-helper/snap-device-helper-test.c +++ b/cmd/snap-device-helper/snap-device-helper-test.c @@ -80,7 +80,7 @@ static void sdh_test_tear_down(sdh_test_fixture *fixture, gconstpointer user_dat } static struct mocks { - size_t cgorup_new_calls; + size_t cgroup_new_calls; void *new_ret; char *new_tag; int new_flags; @@ -104,7 +104,7 @@ static void mocks_reset(void) { /* mocked in test */ sc_device_cgroup *sc_device_cgroup_new(const char *security_tag, int flags) { g_debug("cgroup new called"); - mocks.cgorup_new_calls++; + mocks.cgroup_new_calls++; mocks.new_tag = g_strdup(security_tag); mocks.new_flags = flags; return (sc_device_cgroup *)mocks.new_ret; @@ -153,7 +153,7 @@ static void test_sdh_action(sdh_test_fixture *fixture, gconstpointer test_data) int ret = snap_device_helper_run(&inv_block); g_assert_cmpint(ret, ==, 0); - g_assert_cmpint(mocks.cgorup_new_calls, ==, 1); + g_assert_cmpint(mocks.cgroup_new_calls, ==, 1); if (g_strcmp0(td->action, "add") == 0 || g_strcmp0(td->action, "change") == 0) { g_assert_cmpint(mocks.cgroup_allow_calls, ==, 1); g_assert_cmpint(mocks.cgroup_deny_calls, ==, 0); @@ -184,7 +184,7 @@ static void test_sdh_action(sdh_test_fixture *fixture, gconstpointer test_data) symlink_in_sysroot(fixture, "/sys/devices/foo/tty/ttyS0/subsystem", "../../../../class/other"); ret = snap_device_helper_run(&inv_serial); g_assert_cmpint(ret, ==, 0); - g_assert_cmpint(mocks.cgorup_new_calls, ==, 1); + g_assert_cmpint(mocks.cgroup_new_calls, ==, 1); if (g_strcmp0(td->action, "add") == 0 || g_strcmp0(td->action, "change") == 0) { g_assert_cmpint(mocks.cgroup_allow_calls, ==, 1); g_assert_cmpint(mocks.cgroup_deny_calls, ==, 0); @@ -271,7 +271,7 @@ static void test_sdh_action_nvme(sdh_test_fixture *fixture, gconstpointer test_d }; int ret = snap_device_helper_run(&inv_block); g_assert_cmpint(ret, ==, 0); - g_assert_cmpint(mocks.cgorup_new_calls, ==, 1); + g_assert_cmpint(mocks.cgroup_new_calls, ==, 1); g_assert_cmpint(mocks.cgroup_allow_calls, ==, 1); g_assert_cmpint(mocks.cgroup_deny_calls, ==, 0); g_assert_cmpint(mocks.device_major, ==, tcs[i].expected_maj); @@ -282,6 +282,94 @@ static void test_sdh_action_nvme(sdh_test_fixture *fixture, gconstpointer test_d } } +static void test_sdh_action_remove_fallback_devtype(sdh_test_fixture *fixture, gconstpointer test_data) { + /* check that fallback guessing of device type if applied during remove action */ + mkdir_in_sysroot(fixture, "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/nvme0n1"); + mkdir_in_sysroot(fixture, "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/nvme0n1p1"); + mkdir_in_sysroot(fixture, "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/ng0n1"); + mkdir_in_sysroot(fixture, "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/hwmon0"); + mkdir_in_sysroot(fixture, "/sys/devices/foo/block/sda/sda4"); + mkdir_in_sysroot(fixture, "/sys//devices/pnp0/00:04/tty/ttyS0"); + + struct { + const char *dev; + const char *majmin; + int expected_maj; + int expected_min; + int expected_type; + } tcs[] = { + /* these device paths match the fallback pattern of block devices */ + { + .dev = "/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/nvme0n1", + .majmin = "259:0", + .expected_maj = 259, + .expected_min = 0, + .expected_type = S_IFBLK, + }, + { + .dev = "/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/nvme0n1p1", + .majmin = "259:1", + .expected_maj = 259, + .expected_min = 1, + .expected_type = S_IFBLK, + }, + { + .dev = "/devices/foo/block/sda/sda4", + .majmin = "8:0", + .expected_maj = 8, + .expected_min = 0, + .expected_type = S_IFBLK, + }, + /* these are treated as char devices */ + { + .dev = "/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0", + .majmin = "242:0", + .expected_maj = 242, + .expected_min = 0, + .expected_type = S_IFCHR, + }, + { + .dev = "/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/hwmon0", + .majmin = "241:0", + .expected_maj = 241, + .expected_min = 0, + .expected_type = S_IFCHR, + }, + { + .dev = "/devices/pnp0/00:04/tty/ttyS0", + .majmin = "4:64", + .expected_maj = 4, + .expected_min = 64, + .expected_type = S_IFCHR, + }, + }; + + int bogus = 0; + + for (size_t i = 0; i < sizeof(tcs) / sizeof(tcs[0]); i++) { + mocks_reset(); + /* make cgroup_device_new return a non-NULL */ + mocks.new_ret = &bogus; + + struct sdh_invocation inv_block = { + .action = "remove", + .tagname = "snap_foo_bar", + .devpath = tcs[i].dev, + .majmin = tcs[i].majmin, + }; + int ret = snap_device_helper_run(&inv_block); + g_assert_cmpint(ret, ==, 0); + g_assert_cmpint(mocks.cgroup_new_calls, ==, 1); + g_assert_cmpint(mocks.cgroup_allow_calls, ==, 0); + g_assert_cmpint(mocks.cgroup_deny_calls, ==, 1); + g_assert_cmpint(mocks.device_major, ==, tcs[i].expected_maj); + g_assert_cmpint(mocks.device_minor, ==, tcs[i].expected_min); + g_assert_cmpint(mocks.device_type, ==, tcs[i].expected_type); + g_assert_cmpint(mocks.new_flags, !=, 0); + g_assert_cmpint(mocks.new_flags, ==, SC_DEVICE_CGROUP_FROM_EXISTING); + } +} + static void run_sdh_die(const char *action, const char *tagname, const char *devpath, const char *majmin, const char *msg) { struct sdh_invocation inv = { @@ -355,12 +443,18 @@ static void test_sdh_err_badaction(sdh_test_fixture *fixture, gconstpointer test "ERROR: unknown action \"badaction\"\n"); } -static void test_sdh_err_nosymlink(sdh_test_fixture *fixture, gconstpointer test_data) { +static void test_sdh_err_nosymlink_block(sdh_test_fixture *fixture, gconstpointer test_data) { // missing symlink run_sdh_die("add", "snap_foo_bar", "/devices/foo/block/sda/sda4", "8:4", "cannot read symlink */sys//devices/foo/block/sda/sda4/subsystem*\n"); } +static void test_sdh_err_nosymlink_char(sdh_test_fixture *fixture, gconstpointer test_data) { + // missing symlink + run_sdh_die("add", "snap_foo_bar", "/devices/pnp0/00:04/tty/ttyS0", "4:64", + "cannot read symlink */sys//devices/pnp0/00:04/tty/ttyS0/subsystem*\n"); +} + static void test_sdh_err_funtag1(sdh_test_fixture *fixture, gconstpointer test_data) { run_sdh_die("add", "snap___bar", "/devices/foo/block/sda/sda4", "8:4", "security tag \"snap._.bar\" for snap \"_\" is not valid\n"); @@ -427,6 +521,7 @@ static void __attribute__((constructor)) init(void) { _test_add("/snap-device-helper/add", &add_data, test_sdh_action); _test_add("/snap-device-helper/change", &change_data, test_sdh_action); _test_add("/snap-device-helper/remove", &remove_data, test_sdh_action); + _test_add("/snap-device-helper/remove_fallback", NULL, test_sdh_action_remove_fallback_devtype); _test_add("/snap-device-helper/err/no-appname", NULL, test_sdh_err_noappname); _test_add("/snap-device-helper/err/bad-appname", NULL, test_sdh_err_badappname); @@ -436,7 +531,8 @@ static void __attribute__((constructor)) init(void) { _test_add("/snap-device-helper/err/wrong-devmajorminor_late1", NULL, test_sdh_err_wrongdevmajorminor_late1); _test_add("/snap-device-helper/err/wrong-devmajorminor_late2", NULL, test_sdh_err_wrongdevmajorminor_late2); _test_add("/snap-device-helper/err/bad-action", NULL, test_sdh_err_badaction); - _test_add("/snap-device-helper/err/no-symlink", NULL, test_sdh_err_nosymlink); + _test_add("/snap-device-helper/err/no-symlink-block", NULL, test_sdh_err_nosymlink_block); + _test_add("/snap-device-helper/err/no-symlink-char", NULL, test_sdh_err_nosymlink_char); _test_add("/snap-device-helper/err/funtag1", NULL, test_sdh_err_funtag1); _test_add("/snap-device-helper/err/funtag2", NULL, test_sdh_err_funtag2); _test_add("/snap-device-helper/err/funtag3", NULL, test_sdh_err_funtag3); diff --git a/cmd/snap-device-helper/snap-device-helper.c b/cmd/snap-device-helper/snap-device-helper.c index 26bd47aad7..563f3c006e 100644 --- a/cmd/snap-device-helper/snap-device-helper.c +++ b/cmd/snap-device-helper/snap-device-helper.c @@ -15,6 +15,7 @@ * */ #include <errno.h> +#include <fnmatch.h> #include <libgen.h> #include <limits.h> #include <stdbool.h> @@ -175,11 +176,26 @@ int snap_device_helper_run(const struct sdh_invocation *inv) { char fullsubsystem[PATH_MAX] = {0}; sc_must_snprintf(sysdevsubsystem, sizeof(sysdevsubsystem), "%s/sys/%s/subsystem", sysroot, devpath); if (readlink(sysdevsubsystem, fullsubsystem, sizeof(fullsubsystem)) < 0) { - die("cannot read symlink %s", sysdevsubsystem); - } - char *subsystem = basename(fullsubsystem); - if (sc_streq(subsystem, "block")) { - devtype = S_IFBLK; + if (errno == ENOENT && sc_streq(action, "remove")) { + // on removal the devices are going away, so it is possible that the + // symlink is already gone, in which case try guessing the type like + // the old shell-based snap-device-helper did: + // + // > char devices are .../nvme/nvme* but block devices are + // > .../nvme/nvme*/nvme*n* and .../nvme/nvme*/nvme*n*p* so if have a + // > device that has nvme/nvme*/nvme*n* in it, treat it as a block + // > device + if ((fnmatch("*/block/*", devpath, 0) == 0) || (fnmatch("*/nvme/nvme*/nvme*n*", devpath, 0) == 0)) { + devtype = S_IFBLK; + } + } else { + die("cannot read symlink %s", sysdevsubsystem); + } + } else { + char *subsystem = basename(fullsubsystem); + if (sc_streq(subsystem, "block")) { + devtype = S_IFBLK; + } } sc_device_cgroup *cgroup = sc_device_cgroup_new(security_tag, SC_DEVICE_CGROUP_FROM_EXISTING); if (!cgroup) { diff --git a/cmd/snap-failure/cmd_snapd.go b/cmd/snap-failure/cmd_snapd.go index 625860b1b3..75088cacc1 100644 --- a/cmd/snap-failure/cmd_snapd.go +++ b/cmd/snap-failure/cmd_snapd.go @@ -126,6 +126,11 @@ func (c *cmdSnapd) Execute(args []string) error { // system, either a remodel or a plain snapd installation, call // the snapd from the core snap snapdPath = filepath.Join(dirs.SnapMountDir, "core", "current", "/usr/lib/snapd/snapd") + if !osutil.FileExists(snapdPath) { + // it is possible that the core snap is not installed at + // all, in which case we should try the snapd snap + snapdPath = filepath.Join(dirs.SnapMountDir, "snapd", "current", "/usr/lib/snapd/snapd") + } prevRev = "0" case nil: // the snapd snap was installed before, use the previous revision diff --git a/cmd/snap-failure/cmd_snapd_test.go b/cmd/snap-failure/cmd_snapd_test.go index c7fbe62612..1c03647049 100644 --- a/cmd/snap-failure/cmd_snapd_test.go +++ b/cmd/snap-failure/cmd_snapd_test.go @@ -265,6 +265,41 @@ func (r *failureSuite) TestCallPrevSnapdFromCore(c *C) { }) } +func (r *failureSuite) TestCallPrevSnapdFromSnapdWhenNoCore(c *C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + + // only one entry in sequence + writeSeqFile(c, "snapd", snap.R(123), []*snap.SideInfo{ + {Revision: snap.R(123)}, + }) + + // sanity + c.Assert(filepath.Join(dirs.SnapMountDir, "core", "current", "/usr/lib/snapd/snapd"), testutil.FileAbsent) + // mock snapd in the core snap + snapdCmd := testutil.MockCommand(c, filepath.Join(dirs.SnapMountDir, "snapd", "current", "/usr/lib/snapd/snapd"), + `test "$SNAPD_REVERT_TO_REV" = "0"`) + defer snapdCmd.Restore() + + systemctlCmd := testutil.MockCommand(c, "systemctl", "") + defer systemctlCmd.Restore() + + os.Args = []string{"snap-failure", "snapd"} + err := failure.Run() + c.Check(err, IsNil) + c.Check(r.Stderr(), HasLen, 0) + + c.Check(snapdCmd.Calls(), DeepEquals, [][]string{ + {"snapd"}, + }) + c.Check(systemctlCmd.Calls(), DeepEquals, [][]string{ + {"systemctl", "stop", "snapd.socket"}, + {"systemctl", "is-failed", "snapd.socket", "snapd.service"}, + {"systemctl", "reset-failed", "snapd.socket", "snapd.service"}, + {"systemctl", "restart", "snapd.socket"}, + }) +} + func (r *failureSuite) TestCallPrevSnapdFail(c *C) { origArgs := os.Args defer func() { os.Args = origArgs }() diff --git a/cmd/snap-mgmt/snap-mgmt.sh.in b/cmd/snap-mgmt/snap-mgmt.sh.in index 47540ddbef..f9e4e6d7d5 100644 --- a/cmd/snap-mgmt/snap-mgmt.sh.in +++ b/cmd/snap-mgmt/snap-mgmt.sh.in @@ -45,7 +45,10 @@ purge() { # Undo any bind mounts to ${SNAP_MOUNT_DIR} or /var/snap done by parallel # installs or LP:#1668659 for mp in "$SNAP_MOUNT_DIR" /var/snap; do - if grep -q " $mp $mp" /proc/self/mountinfo; then + # btrfs bind mounts actually include subvolume in the filesystem-path + # https://www.mail-archive.com/linux-btrfs@vger.kernel.org/msg51810.html + if grep -q " $mp $mp " /proc/self/mountinfo || + grep -q -e "\(/.*\)$mp $mp .* btrfs .*\(subvol=\1\)\(,.*\)\?\$" /proc/self/mountinfo ; then umount -l "$mp" || true fi done @@ -110,6 +113,7 @@ purge() { fi # modules rm -f "/etc/modules-load.d/snap.${snap}.conf" + rm -f "/etc/modprobe.d/snap.${snap}.conf" # timer and socket units find /etc/systemd/system -name "snap.${snap}.*.timer" -o -name "snap.${snap}.*.socket" | while read -r f; do systemctl_stop "$(basename "$f")" diff --git a/cmd/snap-preseed/preseed_linux.go b/cmd/snap-preseed/preseed_linux.go index 6c6e819445..bfb201819f 100644 --- a/cmd/snap-preseed/preseed_linux.go +++ b/cmd/snap-preseed/preseed_linux.go @@ -158,16 +158,16 @@ type targetSnapdInfo struct { // The function must be called after syscall.Chroot(..). func chooseTargetSnapdVersion() (*targetSnapdInfo, error) { // read snapd version from the mounted core/snapd snap - infoPath := filepath.Join(snapdMountPath, dirs.CoreLibExecDir, "info") - verFromSnap, err := snapdtool.SnapdVersionFromInfoFile(infoPath) + snapdInfoDir := filepath.Join(snapdMountPath, dirs.CoreLibExecDir) + verFromSnap, _, err := snapdtool.SnapdVersionFromInfoFile(snapdInfoDir) if err != nil { return nil, err } // read snapd version from the main fs under chroot (snapd from the deb); // assumes running under chroot already. - infoPath = filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "info") - verFromDeb, err := snapdtool.SnapdVersionFromInfoFile(infoPath) + hostInfoDir := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir) + verFromDeb, _, err := snapdtool.SnapdVersionFromInfoFile(hostInfoDir) if err != nil { return nil, err } diff --git a/cmd/snap-preseed/preseed_other.go b/cmd/snap-preseed/preseed_other.go index 72c8c6079a..2de640b0c5 100644 --- a/cmd/snap-preseed/preseed_other.go +++ b/cmd/snap-preseed/preseed_other.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !linux // +build !linux /* diff --git a/cmd/snap-repair/staging.go b/cmd/snap-repair/staging.go index dca15c1afd..6152240055 100644 --- a/cmd/snap-repair/staging.go +++ b/cmd/snap-repair/staging.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withtestkeys || withstagingkeys // +build withtestkeys withstagingkeys /* diff --git a/cmd/snap-repair/testkeys.go b/cmd/snap-repair/testkeys.go index e6b9158a7f..b598c42529 100644 --- a/cmd/snap-repair/testkeys.go +++ b/cmd/snap-repair/testkeys.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withtestkeys // +build withtestkeys /* diff --git a/cmd/snap-seccomp/main_nonriscv64.go b/cmd/snap-seccomp/main_nonriscv64.go index 2825561e38..e3690d079d 100644 --- a/cmd/snap-seccomp/main_nonriscv64.go +++ b/cmd/snap-seccomp/main_nonriscv64.go @@ -1,5 +1,6 @@ // -*- Mode: Go; indent-tabs-mode: t -*- // +//go:build !riscv64 // +build !riscv64 /* diff --git a/cmd/snap-seccomp/main_ppc64le.go b/cmd/snap-seccomp/main_ppc64le.go index fdfc2543d0..d719b1402f 100644 --- a/cmd/snap-seccomp/main_ppc64le.go +++ b/cmd/snap-seccomp/main_ppc64le.go @@ -1,5 +1,6 @@ // -*- Mode: Go; indent-tabs-mode: t -*- // +//go:build ppc64le && go1.7 && !go1.8 // +build ppc64le,go1.7,!go1.8 /* diff --git a/cmd/snap-seccomp/main_riscv64.go b/cmd/snap-seccomp/main_riscv64.go index 58003f9759..ca78eddf6f 100644 --- a/cmd/snap-seccomp/main_riscv64.go +++ b/cmd/snap-seccomp/main_riscv64.go @@ -1,5 +1,6 @@ // -*- Mode: Go; indent-tabs-mode: t -*- // +//go:build riscv64 // +build riscv64 /* diff --git a/cmd/snap-seccomp/old_seccomp.go b/cmd/snap-seccomp/old_seccomp.go index 8b3b4bf9dd..a053dceb82 100644 --- a/cmd/snap-seccomp/old_seccomp.go +++ b/cmd/snap-seccomp/old_seccomp.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build oldseccomp // +build oldseccomp /* diff --git a/cmd/snap-update-ns/bootstrap_ppc64le.go b/cmd/snap-update-ns/bootstrap_ppc64le.go index fdfc2543d0..d719b1402f 100644 --- a/cmd/snap-update-ns/bootstrap_ppc64le.go +++ b/cmd/snap-update-ns/bootstrap_ppc64le.go @@ -1,5 +1,6 @@ // -*- Mode: Go; indent-tabs-mode: t -*- // +//go:build ppc64le && go1.7 && !go1.8 // +build ppc64le,go1.7,!go1.8 /* diff --git a/cmd/snap/cmd_list.go b/cmd/snap/cmd_list.go index d92f2fbf4a..2914a771c8 100644 --- a/cmd/snap/cmd_list.go +++ b/cmd/snap/cmd_list.go @@ -84,6 +84,14 @@ func fmtChannel(ch string) string { return ch[:idx+1] + "…" } +func fmtVersion(v string) string { + if v == "" { + // most likely a broken snap, leave a placeholder + return "-" + } + return v +} + func (x *cmdList) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs @@ -116,7 +124,7 @@ func (x *cmdList) Execute(args []string) error { // doing it this way because otherwise it's a sea of %s\t%s\t%s line := []string{ snap.Name, - snap.Version, + fmtVersion(snap.Version), snap.Revision.String(), fmtChannel(snap.TrackingChannel), shortPublisher(esc, snap.Publisher), diff --git a/cmd/snap/cmd_list_test.go b/cmd/snap/cmd_list_test.go index 0cdd9efb31..b711e3755f 100644 --- a/cmd/snap/cmd_list_test.go +++ b/cmd/snap/cmd_list_test.go @@ -202,6 +202,8 @@ func (s *SnapSuite) TestListWithNotes(c *check.C) { ,{"name": "dm1", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "devmode"} ,{"name": "dm2", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "strict"} ,{"name": "cf1", "status": "active", "version": "6", "revision":2, "confinement": "devmode", "jailmode": true} +,{"name": "br1", "status": "active", "version": "", "revision":2, "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "confinement": "strict", "broken": "snap is broken"} +,{"name": "dbr1", "status": "", "version": "", "revision":2, "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "confinement": "strict", "broken": "snap is broken"} ]}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) @@ -217,6 +219,8 @@ func (s *SnapSuite) TestListWithNotes(c *check.C) { c.Check(s.Stdout(), check.Matches, `(?ms).*^dm1 +.* +devmode$`) c.Check(s.Stdout(), check.Matches, `(?ms).*^dm2 +.* +devmode$`) c.Check(s.Stdout(), check.Matches, `(?ms).*^cf1 +.* +jailmode$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^br1 +- +2 +- +bar +broken$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^dbr1 +- +2 +- +bar +disabled,broken$`) c.Check(s.Stderr(), check.Equals, "") } diff --git a/cmd/snap/cmd_userd.go b/cmd/snap/cmd_userd.go index d1e114d4fa..4c9df875ea 100644 --- a/cmd/snap/cmd_userd.go +++ b/cmd/snap/cmd_userd.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !darwin // +build !darwin /* diff --git a/cmd/snap/cmd_userd_test.go b/cmd/snap/cmd_userd_test.go index 8b9880ab70..b22c847d69 100644 --- a/cmd/snap/cmd_userd_test.go +++ b/cmd/snap/cmd_userd_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !darwin // +build !darwin /* diff --git a/cmd/snap/cmd_version_other.go b/cmd/snap/cmd_version_other.go index 2de3d0b64b..2204cfdc25 100644 --- a/cmd/snap/cmd_version_other.go +++ b/cmd/snap/cmd_version_other.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !linux // +build !linux /* diff --git a/cmd/snap/cmd_warnings_test.go b/cmd/snap/cmd_warnings_test.go index 9a85c0c747..0245fcd77a 100644 --- a/cmd/snap/cmd_warnings_test.go +++ b/cmd/snap/cmd_warnings_test.go @@ -222,7 +222,7 @@ func (s *warningSuite) TestListWithWarnings(c *check.C) { c.Check(rest, check.HasLen, 0) c.Check(s.Stdout(), check.Equals, ` Name Version Rev Tracking Publisher Notes - unset - - disabled + - unset - - disabled `[1:]) c.Check(s.Stderr(), check.Equals, "WARNING: There are 2 new warnings. See 'snap warnings'.\n") diff --git a/cmd/snap/last.go b/cmd/snap/last.go index 0abb383b53..4cd30104dd 100644 --- a/cmd/snap/last.go +++ b/cmd/snap/last.go @@ -25,6 +25,7 @@ import ( "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/strutil" ) type changeIDMixin struct { @@ -72,7 +73,17 @@ func (l *changeIDMixin) GetChangeID() (string, error) { kind = kind[:l] } // our internal change types use "-snap" postfix but let user skip it and use short form. - if kind == "refresh" || kind == "install" || kind == "remove" || kind == "connect" || kind == "disconnect" || kind == "configure" || kind == "try" { + shortForms := []string{ + // see api_snaps.go:snapInstructionDispTable + "install", "refresh", "remove", "revert", "enable", "disable", "switch", + // see api_interfaces.go:changeInterfaces + "connect", "disconnect", + // see api_snap_conf.go:setSnapConf + "configure", + // see api_sideload_n_try.go:trySnap + "try", + } + if strutil.ListContains(shortForms, kind) { kind += "-snap" } changes, err := queryChanges(cli, &client.ChangesOptions{Selector: client.ChangesAll}) diff --git a/daemon/api.go b/daemon/api.go index 7a8f972890..5aebe1ea21 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -132,6 +132,7 @@ func storeFrom(d *Daemon) snapstate.StoreService { var ( snapstateInstall = snapstate.Install snapstateInstallPath = snapstate.InstallPath + snapstateInstallPathMany = snapstate.InstallPathMany snapstateRefreshCandidates = snapstate.RefreshCandidates snapstateTryPath = snapstate.TryPath snapstateUpdate = snapstate.Update @@ -142,7 +143,8 @@ var ( snapstateRevertToRevision = snapstate.RevertToRevision snapstateSwitch = snapstate.Switch - assertstateRefreshSnapAssertions = assertstate.RefreshSnapAssertions + assertstateRefreshSnapAssertions = assertstate.RefreshSnapAssertions + assertstateRestoreValidationSetsTracking = assertstate.RestoreValidationSetsTracking ) func ensureStateSoonImpl(st *state.State) { diff --git a/daemon/api_sideload_n_try.go b/daemon/api_sideload_n_try.go index a8a260bf13..bec514d91d 100644 --- a/daemon/api_sideload_n_try.go +++ b/daemon/api_sideload_n_try.go @@ -21,6 +21,7 @@ package daemon import ( "bytes" + "context" "errors" "fmt" "io" @@ -37,6 +38,7 @@ import ( "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" @@ -75,24 +77,53 @@ func (f *Form) RemoveAllExcept(paths []string) { } } -// SnapFileNameAndTempPath returns the original file path/name and the path to -// where the temp file is written. -func (f *Form) SnapFileNameAndTempPath() (srcFilename, path string, apiErr *apiError) { +type uploadedSnap struct { + // filename is the original name/path of the snap file. + filename string + // tmpPath is the location where the temp snap file is stored. + tmpPath string + // instanceName is optional and can only be set if only one snap was uploaded. + instanceName string +} + +// GetSnapFiles returns the original name and temp path for each snap file in +// the form. Optionally, it might include a requested instance name, but only +// if the was only one file in the form. +func (f *Form) GetSnapFiles() ([]*uploadedSnap, *apiError) { if len(f.FileRefs["snap"]) == 0 { - return "", "", BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) + return nil, BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) } - snapFile := f.FileRefs["snap"][0] - srcFilename, path = snapFile.Filename, snapFile.TmpPath + refs := f.FileRefs["snap"] + if len(refs) == 1 && len(f.Values["snap-path"]) > 0 { + uploaded := &uploadedSnap{ + filename: f.Values["snap-path"][0], + tmpPath: refs[0].TmpPath, + } - if len(f.Values["snap-path"]) > 0 { - srcFilename = f.Values["snap-path"][0] + if len(f.Values["name"]) > 0 { + uploaded.instanceName = f.Values["name"][0] + } + return []*uploadedSnap{uploaded}, nil } - return srcFilename, path, nil + snapFiles := make([]*uploadedSnap, len(refs)) + for i, ref := range refs { + snapFiles[i] = &uploadedSnap{ + filename: ref.Filename, + tmpPath: ref.TmpPath, + } + } + + return snapFiles, nil +} + +type sideloadFlags struct { + snapstate.Flags + dangerousOK bool } -func sideloadOrTrySnap(c *Command, body io.ReadCloser, boundary string) Response { +func sideloadOrTrySnap(c *Command, body io.ReadCloser, boundary string, user *auth.UserState) Response { route := c.d.router.Get(stateChangeCmd.Path) if route == nil { return InternalError("cannot find route for change") @@ -111,7 +142,6 @@ func sideloadOrTrySnap(c *Command, body io.ReadCloser, boundary string) Response form.RemoveAllExcept(pathsToNotRemove) }() - dangerousOK := isTrue(form, "dangerous") flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic")) if err != nil { return BadRequest(err.Error()) @@ -123,97 +153,153 @@ func sideloadOrTrySnap(c *Command, body io.ReadCloser, boundary string) Response } return trySnap(c.d.overlord.State(), form.Values["snap-path"][0], flags) } - flags.RemoveSnapPath = true + flags.RemoveSnapPath = true flags.Unaliased = isTrue(form, "unaliased") flags.IgnoreRunning = isTrue(form, "ignore-running") - systemRestartImmediate := isTrue(form, "system-restart-immediate") - origPath, tempPath, errRsp := form.SnapFileNameAndTempPath() + sideloadFlags := sideloadFlags{ + Flags: flags, + dangerousOK: isTrue(form, "dangerous"), + } + + snapFiles, errRsp := form.GetSnapFiles() + if errRsp != nil { + return errRsp + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + var chg *state.Change + if len(snapFiles) > 1 { + chg, errRsp = sideloadManySnaps(st, snapFiles, sideloadFlags, user) + } else { + chg, errRsp = sideloadSnap(st, snapFiles[0], sideloadFlags) + } if errRsp != nil { return errRsp } + chg.Set("system-restart-immediate", isTrue(form, "system-restart-immediate")) + + ensureStateSoon(st) + + // the handoff is only done when the unlock succeeds (instead of panicking) + // but this is good enough + pathsToNotRemove = make([]string, len(snapFiles)) + for i, snapFile := range snapFiles { + pathsToNotRemove[i] = snapFile.tmpPath + } + + return AsyncResponse(nil, chg.ID()) +} + +func sideloadManySnaps(st *state.State, snapFiles []*uploadedSnap, flags sideloadFlags, user *auth.UserState) (*state.Change, *apiError) { + sideInfos := make([]*snap.SideInfo, len(snapFiles)) + names := make([]string, len(snapFiles)) + tempPaths := make([]string, len(snapFiles)) + origPaths := make([]string, len(snapFiles)) + + for i, snapFile := range snapFiles { + si, apiError := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags) + if apiError != nil { + return nil, apiError + } + + sideInfos[i] = si + names[i] = si.RealName + tempPaths[i] = snapFile.tmpPath + origPaths[i] = snapFile.filename + } + + var userID int + if user != nil { + userID = user.ID + } + + tss, err := snapstateInstallPathMany(context.TODO(), st, sideInfos, tempPaths, userID, &flags.Flags) + if err != nil { + return nil, errToResponse(err, tempPaths, InternalError, "cannot install snap files: %v") + } + + msg := fmt.Sprintf(i18n.G("Install snaps %s from files %s"), strutil.Quoted(names), strutil.Quoted(origPaths)) + chg := newChange(st, "install-snap", msg, tss, names) + chg.Set("api-data", map[string][]string{"snap-names": names}) + + return chg, nil +} + +func sideloadSnap(st *state.State, snapFile *uploadedSnap, flags sideloadFlags) (*state.Change, *apiError) { var instanceName string - if len(form.Values["name"]) > 0 { + if snapFile.instanceName != "" { // caller has specified desired instance name - instanceName = form.Values["name"][0] + instanceName = snapFile.instanceName if err := snap.ValidateInstanceName(instanceName); err != nil { - return BadRequest(err.Error()) + return nil, BadRequest(err.Error()) } } - st := c.d.overlord.State() - st.Lock() - defer st.Unlock() + sideInfo, apiErr := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags) + if apiErr != nil { + return nil, apiErr + } + + if instanceName != "" { + requestedSnapName := snap.InstanceSnap(instanceName) + if requestedSnapName != sideInfo.RealName { + return nil, BadRequest(fmt.Sprintf("instance name %q does not match snap name %q", instanceName, sideInfo.RealName)) + } + } else { + instanceName = sideInfo.RealName + } + + tset, _, err := snapstateInstallPath(st, sideInfo, snapFile.tmpPath, instanceName, "", flags.Flags) + if err != nil { + return nil, errToResponse(err, []string{sideInfo.RealName}, InternalError, "cannot install snap file: %v") + } + + msg := fmt.Sprintf(i18n.G("Install %q snap from file %q"), instanceName, snapFile.filename) + chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName}) + chg.Set("api-data", map[string]string{"snap-name": instanceName}) + + return chg, nil +} - var snapName string +func readSideInfo(st *state.State, tempPath string, origPath string, flags sideloadFlags) (*snap.SideInfo, *apiError) { var sideInfo *snap.SideInfo - if !dangerousOK { + if !flags.dangerousOK { si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st)) switch { case err == nil: - snapName = si.RealName sideInfo = si case asserts.IsNotFound(err): // with devmode we try to find assertions but it's ok // if they are not there (implies --dangerous) - if !isTrue(form, "devmode") { + if !flags.DevMode { msg := "cannot find signatures with metadata for snap" if origPath != "" { msg = fmt.Sprintf("%s %q", msg, origPath) } - return BadRequest(msg) + return nil, BadRequest(msg) } // TODO: set a warning if devmode default: - return BadRequest(err.Error()) + return nil, BadRequest(err.Error()) } } - if snapName == "" { + if sideInfo == nil { // potentially dangerous but dangerous or devmode params were set info, err := unsafeReadSnapInfo(tempPath) if err != nil { - return BadRequest("cannot read snap file: %v", err) + return nil, BadRequest("cannot read snap file: %v", err) } - snapName = info.SnapName() - sideInfo = &snap.SideInfo{RealName: snapName} + sideInfo = &snap.SideInfo{RealName: info.SnapName()} } - - if instanceName != "" { - requestedSnapName := snap.InstanceSnap(instanceName) - if requestedSnapName != snapName { - return BadRequest(fmt.Sprintf("instance name %q does not match snap name %q", instanceName, snapName)) - } - } else { - instanceName = snapName - } - - msg := fmt.Sprintf(i18n.G("Install %q snap from file"), instanceName) - if origPath != "" { - msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), instanceName, origPath) - } - - tset, _, err := snapstateInstallPath(st, sideInfo, tempPath, instanceName, "", flags) - if err != nil { - return errToResponse(err, []string{snapName}, InternalError, "cannot install snap file: %v") - } - - chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName}) - if systemRestartImmediate { - chg.Set("system-restart-immediate", true) - } - chg.Set("api-data", map[string]string{"snap-name": instanceName}) - - ensureStateSoon(st) - - // only when the unlock succeeds (as opposed to panicing) is the handoff done - // but this is good enough - pathsToNotRemove = append(pathsToNotRemove, tempPath) - - return AsyncResponse(nil, chg.ID()) + return sideInfo, nil } // maxReadBuflen is the maximum buffer size for reading the non-file parts in the snap upload form diff --git a/daemon/api_sideload_n_try_test.go b/daemon/api_sideload_n_try_test.go index bcdb02c6f4..618dba66bd 100644 --- a/daemon/api_sideload_n_try_test.go +++ b/daemon/api_sideload_n_try_test.go @@ -22,13 +22,16 @@ package daemon_test import ( "bytes" "context" + "crypto" "crypto/rand" + "errors" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "regexp" + "strconv" "time" "gopkg.in/check.v1" @@ -43,6 +46,7 @@ import ( "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/sandbox" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/testutil" ) @@ -583,7 +587,7 @@ func (s *sideloadSuite) TestSideloadUsePreciselyAllMemory(c *check.C) { c.Check(apiErr.Message, check.Equals, `cannot find "snap" file field in provided multipart/form-data payload`) } -func (s *sideloadSuite) TestCleanUpTempFilesIfRequestFailed(c *check.C) { +func (s *sideloadSuite) TestSideloadCleanUpTempFilesIfRequestFailed(c *check.C) { s.daemonWithOverlordMockAndStore() // write file parts @@ -618,7 +622,7 @@ func (s *sideloadSuite) TestCleanUpTempFilesIfRequestFailed(c *check.C) { c.Check(matches, check.HasLen, 0) } -func (s *sideloadSuite) TestCleanUpUnusedTempSnapFiles(c *check.C) { +func (s *sideloadSuite) TestSideloadCleanUpUnusedTempSnapFiles(c *check.C) { body := "----hello--\r\n" + "Content-Disposition: form-data; name=\"devmode\"\r\n" + "\r\n" + @@ -628,10 +632,9 @@ func (s *sideloadSuite) TestCleanUpUnusedTempSnapFiles(c *check.C) { "\r\n" + "xyzzy\r\n" + "----hello--\r\n" + - "Content-Disposition: form-data; name=\"snap\"; filename=\"two\"\r\n" + + // only files with the name 'snap' are used + "Content-Disposition: form-data; name=\"not-snap\"; filename=\"two\"\r\n" + "\r\n" + - // sideloadCheck checks that the snap file passed to the change has contents "xyzzy" so - // having a different body here tests that the second file isn't passed to change "bla\r\n" + "----hello--\r\n" @@ -645,6 +648,283 @@ func (s *sideloadSuite) TestCleanUpUnusedTempSnapFiles(c *check.C) { c.Check(matches, check.HasLen, 1) } +func (s *sideloadSuite) TestSideloadManySnaps(c *check.C) { + d := s.daemonWithFakeSnapManager(c) + expectedFlags := &snapstate.Flags{RemoveSnapPath: true, DevMode: true} + + restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { + c.Check(flags, check.DeepEquals, expectedFlags) + c.Check(userID, check.Not(check.Equals), 0) + + var tss []*state.TaskSet + for i, path := range paths { + si := infos[i] + c.Check(path, testutil.FileEquals, si.RealName) + + ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName))) + tss = append(tss, ts) + } + + return tss, nil + }) + defer restore() + + snaps := []string{"one", "two"} + var i int + readRest := daemon.MockUnsafeReadSnapInfo(func(string) (*snap.Info, error) { + info := &snap.Info{SuggestedName: snaps[i]} + i++ + return info, nil + }) + defer readRest() + + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + prefixed := make([]string, len(snaps)) + for i, snap := range snaps { + prefixed[i] = "file-" + snap + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + prefixed[i] + "\"\r\n" + + "\r\n" + + snap + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + s.asUserAuth(c, req) + rsp := s.asyncReq(c, req, s.authUser) + + st := d.Overlord().State() + st.Lock() + defer st.Unlock() + + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install snaps %s from files %s`, strutil.Quoted(snaps), strutil.Quoted(prefixed))) + + var data map[string][]string + c.Assert(chg.Get("api-data", &data), check.IsNil) + c.Check(data["snap-names"], check.DeepEquals, snaps) +} + +func (s *sideloadSuite) TestSideloadManyFailInstallPathMany(c *check.C) { + s.daemon(c) + restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { + return nil, errors.New("expected") + }) + defer restore() + + readRest := daemon.MockUnsafeReadSnapInfo(func(string) (*snap.Info, error) { + return &snap.Info{SuggestedName: "name"}, nil + }) + defer readRest() + + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + for _, snap := range []string{"one", "two"} { + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"file-" + snap + "\"\r\n" + + "\r\n" + + "xyzzy \r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + apiErr := s.errorReq(c, req, nil) + + c.Check(apiErr.JSON().Status, check.Equals, 500) + c.Check(apiErr.Message, check.Equals, `cannot install snap files: expected`) +} + +func (s *sideloadSuite) TestSideloadManyFailUnsafeReadInfo(c *check.C) { + s.daemon(c) + restore := daemon.MockUnsafeReadSnapInfo(func(string) (*snap.Info, error) { + return nil, errors.New("expected") + }) + defer restore() + + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + for _, snap := range []string{"one", "two"} { + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"file-" + snap + "\"\r\n" + + "\r\n" + + "xyzzy \r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + apiErr := s.errorReq(c, req, nil) + + c.Check(apiErr.JSON().Status, check.Equals, 400) + c.Check(apiErr.Message, check.Equals, `cannot read snap file: expected`) +} + +func (s *sideloadSuite) TestSideloadManySnapsDevmode(c *check.C) { + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + s.errReadInfo(c, body) +} + +func (s *sideloadSuite) TestSideloadManySnapsDangerous(c *check.C) { + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"dangerous\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + s.errReadInfo(c, body) +} + +func (s *sideloadSuite) errReadInfo(c *check.C, body string) { + s.daemon(c) + + for _, snap := range []string{"one", "two"} { + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + snap + "\"\r\n" + + "\r\n" + + snap + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + rsp := s.errorReq(c, req, nil) + + c.Assert(rsp.Status, check.Equals, 400) + // gets as far as reading the file to get the SideInfo + c.Assert(rsp.Message, check.Matches, "cannot read snap file:.*") +} + +func (s *sideloadSuite) TestSideloadManySnapsAsserted(c *check.C) { + d := s.daemonWithOverlordMockAndStore() + st := d.Overlord().State() + snaps := []string{"one", "two"} + s.mockAssertions(c, st, snaps) + + body := "----hello--\r\n" + expectedFlags := snapstate.Flags{RemoveSnapPath: true} + s.testSideloadManySnaps(c, st, body, snaps, expectedFlags) +} + +func (s *sideloadSuite) TestSideloadManySnapsOneNotAsserted(c *check.C) { + d := s.daemonWithOverlordMockAndStore() + st := d.Overlord().State() + snaps := []string{"one", "two"} + s.mockAssertions(c, st, []string{"one"}) + + body := "----hello--\r\n" + + fileSnaps := make([]string, len(snaps)) + for i, snap := range snaps { + fileSnaps[i] = "file-" + snap + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + fileSnaps[i] + "\"\r\n" + + "\r\n" + + snap + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + rsp := s.errorReq(c, req, nil) + + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Message, check.Matches, "cannot find signatures with metadata for snap \"file-two\"") +} + +func (s *sideloadSuite) mockAssertions(c *check.C, st *state.State, snaps []string) { + for _, snap := range snaps { + hash := crypto.SHA3_384.New() + data := []byte(snap) + hash.Write(data) + digest := hash.Sum(nil) + + base64Digest, err := asserts.EncodeDigest(crypto.SHA3_384, digest) + c.Assert(err, check.IsNil) + dev1Acct := assertstest.NewAccount(s.StoreSigning, "devel1", nil, "") + snapDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": snap + "-id", + "snap-name": snap, + "publisher-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + snapRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ + "snap-sha3-384": base64Digest, + "snap-size": strconv.Itoa(len(data)), + "snap-id": snap + "-id", + "snap-revision": "41", + "developer-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + + st.Lock() + assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""), dev1Acct, snapDecl, snapRev) + st.Unlock() + } +} + +func (s *sideloadSuite) testSideloadManySnaps(c *check.C, st *state.State, body string, snaps []string, expectedFlags snapstate.Flags) { + restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { + c.Check(*flags, check.DeepEquals, expectedFlags) + + var tss []*state.TaskSet + for i, si := range infos { + c.Check(si, check.DeepEquals, &snap.SideInfo{ + RealName: snaps[i], + SnapID: snaps[i] + "-id", + Revision: snap.R(41), + }) + + ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName))) + tss = append(tss, ts) + } + + return tss, nil + }) + defer restore() + + fileSnaps := make([]string, len(snaps)) + for i, snap := range snaps { + fileSnaps[i] = "file-" + snap + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + fileSnaps[i] + "\"\r\n" + + "\r\n" + + snap + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + rsp := s.asyncReq(c, req, nil) + + c.Check(rsp.Status, check.Equals, 202) + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install snaps %s from files %s`, strutil.Quoted(snaps), strutil.Quoted(fileSnaps))) +} + type trySuite struct { apiBaseSuite } diff --git a/daemon/api_snaps.go b/daemon/api_snaps.go index 0089d14ae8..f84ce706d6 100644 --- a/daemon/api_snaps.go +++ b/daemon/api_snaps.go @@ -487,7 +487,7 @@ func postSnaps(c *Command, r *http.Request, user *auth.UserState) Response { return BadRequest("unknown content type: %s", contentType) } - return sideloadOrTrySnap(c, r.Body, params["boundary"]) + return sideloadOrTrySnap(c, r.Body, params["boundary"], user) } func snapOpMany(c *Command, r *http.Request, user *auth.UserState) Response { @@ -607,6 +607,11 @@ func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionRes // TODO: use a per-request context updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil) if err != nil { + if opts.IsRefreshOfAllSnaps { + if err := assertstateRestoreValidationSetsTracking(st); err != nil && !errors.Is(err, state.ErrNoState) { + return nil, err + } + } return nil, err } diff --git a/daemon/api_snaps_test.go b/daemon/api_snaps_test.go index a9c12444b9..9c4131befa 100644 --- a/daemon/api_snaps_test.go +++ b/daemon/api_snaps_test.go @@ -596,6 +596,35 @@ func (s *snapsSuite) TestRefreshAllNoChanges(c *check.C) { c.Check(refreshSnapAssertions, check.Equals, true) } +func (s *snapsSuite) TestRefreshAllRestoresValidationSets(c *check.C) { + refreshSnapAssertions := false + var refreshAssertionsOpts *assertstate.RefreshAssertionsOptions + defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { + refreshSnapAssertions = true + refreshAssertionsOpts = opts + return nil + })() + + defer daemon.MockAssertstateRestoreValidationSetsTracking(func(s *state.State) error { + return nil + })() + + defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { + return nil, nil, fmt.Errorf("boom") + })() + + d := s.daemon(c) + inst := &daemon.SnapInstruction{Action: "refresh"} + st := d.Overlord().State() + st.Lock() + _, err := inst.DispatchForMany()(inst, st) + st.Unlock() + c.Assert(err, check.ErrorMatches, "boom") + c.Check(refreshSnapAssertions, check.Equals, true) + c.Assert(refreshAssertionsOpts, check.NotNil) + c.Check(refreshAssertionsOpts.IsRefreshOfAllSnaps, check.Equals, true) +} + func (s *snapsSuite) TestRefreshMany(c *check.C) { refreshSnapAssertions := false var refreshAssertionsOpts *assertstate.RefreshAssertionsOptions diff --git a/daemon/export_api_snaps_test.go b/daemon/export_api_snaps_test.go index 61173ecf87..1a4322d90d 100644 --- a/daemon/export_api_snaps_test.go +++ b/daemon/export_api_snaps_test.go @@ -21,6 +21,7 @@ package daemon import ( "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" ) @@ -31,3 +32,11 @@ func MakeAboutSnap(info *snap.Info, snapst *snapstate.SnapState) aboutSnap { var ( MapLocal = mapLocal ) + +func MockAssertstateRestoreValidationSetsTracking(f func(*state.State) error) (restore func()) { + old := assertstateRestoreValidationSetsTracking + assertstateRestoreValidationSetsTracking = f + return func() { + assertstateRestoreValidationSetsTracking = old + } +} diff --git a/daemon/export_test.go b/daemon/export_test.go index 051aa8e8f5..fcd53c5e2d 100644 --- a/daemon/export_test.go +++ b/daemon/export_test.go @@ -195,6 +195,14 @@ func MockSnapstateRemoveMany(mock func(*state.State, []string) ([]string, []*sta } } +func MockSnapstateInstallPathMany(f func(context.Context, *state.State, []*snap.SideInfo, []string, int, *snapstate.Flags) ([]*state.TaskSet, error)) func() { + old := snapstateInstallPathMany + snapstateInstallPathMany = f + return func() { + snapstateInstallPathMany = old + } +} + type ( RespJSON = respJSON FileResponse = fileResponse diff --git a/data/env/snapd.fish.in b/data/env/snapd.fish.in index ca7c8358ad..f2a6591249 100644 --- a/data/env/snapd.fish.in +++ b/data/env/snapd.fish.in @@ -1,8 +1,6 @@ # Expand $PATH to include the directory where snappy applications go. set -u snap_bin_path "@SNAP_MOUNT_DIR@/bin" -if ! contains $snap_bin_path $PATH - set PATH $PATH $snap_bin_path -end +fish_add_path -aP $snap_bin_path # Desktop files (used by desktop environments within both X11 and Wayland) are # looked for in XDG_DATA_DIRS; make sure it includes the relevant directory for diff --git a/data/selinux/snappy.te b/data/selinux/snappy.te index f463baba4c..948280ae7d 100644 --- a/data/selinux/snappy.te +++ b/data/selinux/snappy.te @@ -350,6 +350,11 @@ ifndef(`distro_rhel7',` timedatex_dbus_chat(snappy_t) ') +# kernel-module-load interface may inspect or write files under /etc/modprobe.d +optional_policy(` + modutils_manage_module_config(snappy_t) +') + # only pops up in cloud images where cloud-init.target is incorrectly labeled allow snappy_t init_var_run_t:lnk_file read; @@ -432,6 +437,10 @@ ifndef(`distro_rhel7',` allow snappy_t snappy_cli_t:process { getpgid sigkill }; allow snappy_t unconfined_service_t:process { getpgid sigkill }; +# Snapd invokes systemd-detect-virt, which may make poke /proc/xen/, but does +# not transition to a separate type and has no interface policy +kernel_read_xen_state(snappy_t) + ######################################## # # snap-update-ns, snap-dicsard-ns local policy @@ -1 +1 @@ -packaging/ubuntu-16.04/ \ No newline at end of file +packaging/ubuntu-16.04 \ No newline at end of file diff --git a/dirs/dirs.go b/dirs/dirs.go index d25ef34e08..676ce36373 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -51,6 +51,7 @@ var ( SnapMountPolicyDir string SnapUdevRulesDir string SnapKModModulesDir string + SnapKModModprobeDir string LocaleDir string SnapMetaDir string SnapdSocket string @@ -266,6 +267,12 @@ func SnapSystemdConfDirUnder(rootdir string) string { return filepath.Join(rootdir, "/etc/systemd/system.conf.d") } +// SnapSystemdConfDirUnder returns the path to the systemd conf dir under +// rootdir. +func SnapServicesDirUnder(rootdir string) string { + return filepath.Join(rootdir, "/etc/systemd/system") +} + // SnapBootAssetsDirUnder returns the path to boot assets directory under a // rootdir. func SnapBootAssetsDirUnder(rootdir string) string { @@ -407,6 +414,7 @@ func SetRootDir(rootdir string) { SnapUdevRulesDir = filepath.Join(rootdir, "/etc/udev/rules.d") SnapKModModulesDir = filepath.Join(rootdir, "/etc/modules-load.d/") + SnapKModModprobeDir = filepath.Join(rootdir, "/etc/modprobe.d/") LocaleDir = filepath.Join(rootdir, "/usr/share/locale") ClassicDir = filepath.Join(rootdir, "/writable/classic") diff --git a/gadget/gadget.go b/gadget/gadget.go index 54df9b4ba8..fcaaaecd90 100644 --- a/gadget/gadget.go +++ b/gadget/gadget.go @@ -462,7 +462,11 @@ func InfoFromGadgetYaml(gadgetYaml []byte, model Model) (*Info, error) { // basic validation var bootloadersFound int knownFsLabelsPerVolume := make(map[string]map[string]bool, len(gi.Volumes)) - for name, v := range gi.Volumes { + for name := range gi.Volumes { + v := gi.Volumes[name] + if v == nil { + return nil, fmt.Errorf("volume %q stanza is empty", name) + } // set the VolumeName for the volume v.Name = name if err := validateVolume(v, knownFsLabelsPerVolume); err != nil { diff --git a/gadget/gadget_test.go b/gadget/gadget_test.go index 0a2fea054e..fb61376fc7 100644 --- a/gadget/gadget_test.go +++ b/gadget/gadget_test.go @@ -621,6 +621,19 @@ func (s *gadgetYamlTestSuite) TestCoreConfigDefaults(c *C) { }) } +var mockGadgetWithEmptyVolumes = `device-tree-origin: kernel +volumes: + lun-0: +` + +func (s *gadgetYamlTestSuite) TestRegressionGadgetWithEmptyVolume(c *C) { + err := ioutil.WriteFile(s.gadgetYamlPath, []byte(mockGadgetWithEmptyVolumes), 0644) + c.Assert(err, IsNil) + + _, err = gadget.ReadInfo(s.dir, nil) + c.Assert(err, ErrorMatches, `volume "lun-0" stanza is empty`) +} + func (s *gadgetYamlTestSuite) TestReadGadgetDefaultsMultiline(c *C) { err := ioutil.WriteFile(s.gadgetYamlPath, mockClassicGadgetMultilineDefaultsYaml, 0644) c.Assert(err, IsNil) diff --git a/gadget/install/encrypt.go b/gadget/install/encrypt.go index b3a44b77b7..ac10571680 100644 --- a/gadget/install/encrypt.go +++ b/gadget/install/encrypt.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/gadget/install/encrypt_test.go b/gadget/install/encrypt_test.go index 290a20b2b3..a43e8b1145 100644 --- a/gadget/install/encrypt_test.go +++ b/gadget/install/encrypt_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/gadget/install/export_secboot_test.go b/gadget/install/export_secboot_test.go index 257c2e7cf2..b21824f5c0 100644 --- a/gadget/install/export_secboot_test.go +++ b/gadget/install/export_secboot_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/gadget/install/install.go b/gadget/install/install.go index b44599c4f8..f8424e35ca 100644 --- a/gadget/install/install.go +++ b/gadget/install/install.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/gadget/install/install_dummy.go b/gadget/install/install_dummy.go index 6149b916f8..2698d5ee47 100644 --- a/gadget/install/install_dummy.go +++ b/gadget/install/install_dummy.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build nosecboot // +build nosecboot /* diff --git a/gadget/install/install_test.go b/gadget/install/install_test.go index 4dc479d0c6..8acfeba9f2 100644 --- a/gadget/install/install_test.go +++ b/gadget/install/install_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/gadget/install/mount_other.go b/gadget/install/mount_other.go index fedfdca6ce..c3ec6022c8 100644 --- a/gadget/install/mount_other.go +++ b/gadget/install/mount_other.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !linux // +build !linux /* diff --git a/gadget/install/partition.go b/gadget/install/partition.go index 4fc6719df3..a13b1ec318 100644 --- a/gadget/install/partition.go +++ b/gadget/install/partition.go @@ -66,6 +66,13 @@ func createMissingPartitions(dl *gadget.OnDiskVolume, pv *gadget.LaidOutVolume) return nil, err } + // run udevadm settle to wait for udev events that may have been triggered + // by reloading the partition table to be processed, as we need the udev + // database to be freshly updated + if out, err := exec.Command("udevadm", "settle", "--timeout=180").CombinedOutput(); err != nil { + return nil, fmt.Errorf("cannot wait for udev to settle after reloading partition table: %v", osutil.OutputErr(out, err)) + } + // Make sure the devices for the partitions we created are available if err := ensureNodesExist(created, 5*time.Second); err != nil { return nil, fmt.Errorf("partition not available: %v", err) @@ -177,42 +184,57 @@ func deviceName(name string, index int) string { // removeCreatedPartitions removes partitions added during a previous install. func removeCreatedPartitions(lv *gadget.LaidOutVolume, dl *gadget.OnDiskVolume) error { - indexes := make([]string, 0, len(dl.Structure)) + sfdiskIndexes := make([]string, 0, len(dl.Structure)) + // up to 3 possible partitions are creatable and thus removable: + // ubuntu-data, ubuntu-boot, and ubuntu-save + deletedIndexes := make(map[int]bool, 3) for i, s := range dl.Structure { if wasCreatedDuringInstall(lv, s) { logger.Noticef("partition %s was created during previous install", s.Node) - indexes = append(indexes, strconv.Itoa(i+1)) + sfdiskIndexes = append(sfdiskIndexes, strconv.Itoa(i+1)) + deletedIndexes[i] = true } } - if len(indexes) == 0 { + if len(sfdiskIndexes) == 0 { return nil } // Delete disk partitions - logger.Debugf("delete disk partitions %v", indexes) - cmd := exec.Command("sfdisk", append([]string{"--no-reread", "--delete", dl.Device}, indexes...)...) + logger.Debugf("delete disk partitions %v", sfdiskIndexes) + cmd := exec.Command("sfdisk", append([]string{"--no-reread", "--delete", dl.Device}, sfdiskIndexes...)...) if output, err := cmd.CombinedOutput(); err != nil { return osutil.OutputErr(output, err) } - // Reload the partition table + // Reload the partition table - note that this specifically does not trigger + // udev events to remove the deleted devices, see the doc-comment in + // reloadPartitionTable for more details if err := reloadPartitionTable(dl.Device); err != nil { return err } - // run udevadm settle to wait for udev events that may have been triggered - // by reloading the partition table to be processed, as we need the udev - // database to be freshly updated and complete before updating the partition - // information for the OnDiskVolume - // TODO: is 3 minute timeout reasonable for this? - if out, err := exec.Command("udevadm", "settle", "--timeout=180").CombinedOutput(); err != nil { - return fmt.Errorf("cannot wait for udev to settle after reloading partition table: %v", osutil.OutputErr(out, err)) + // Remove the partitions we deleted from the OnDiskVolume - note that we + // specifically don't try to just re-build the OnDiskVolume since doing + // so correctly requires using only information from the partition table + // we just updated with sfdisk (since we used --no-reread above, and we can't + // really tell the kernel to re-read the partition table without hitting + // EBUSY as the disk is still mounted even though the deleted partitions + // were deleted), but to do so would essentially just be testing that sfdisk + // updated the partition table in a way we expect. The partition parsing + // code we use to build the OnDiskVolume also must not be reliant on using + // sfdisk (since it has to work in the initrd where we don't have sfdisk), + // so either that code would just be a duplication of what sfdisk is doing + // or that code would fail to update the deleted partitions anyways since + // at this point the only thing that knows about the deleted partitions is + // the physical partition table on the disk. + newStructure := make([]gadget.OnDiskStructure, 0, len(dl.Structure)-len(deletedIndexes)) + for i, structure := range dl.Structure { + if !deletedIndexes[i] { + newStructure = append(newStructure, structure) + } } - // Re-read the partition table from the device to update our partition list - if err := gadget.UpdatePartitionList(dl); err != nil { - return err - } + dl.Structure = newStructure // Ensure all created partitions were removed if remaining := createdDuringInstall(lv, dl); len(remaining) > 0 { diff --git a/gadget/install/partition_test.go b/gadget/install/partition_test.go index 7d007002d0..92733a1862 100644 --- a/gadget/install/partition_test.go +++ b/gadget/install/partition_test.go @@ -264,6 +264,9 @@ func (s *partitionTestSuite) TestCreatePartitions(c *C) { restore := disks.MockDeviceNameToDiskMapping(m) defer restore() + cmdUdevadm := testutil.MockCommand(c, "udevadm", "") + defer cmdUdevadm.Restore() + calls := 0 restore = install.MockEnsureNodesExist(func(ds []gadget.OnDiskStructure, timeout time.Duration) error { calls++ @@ -294,6 +297,10 @@ func (s *partitionTestSuite) TestCreatePartitions(c *C) { c.Assert(s.cmdPartx.Calls(), DeepEquals, [][]string{ {"partx", "-u", "/dev/node"}, }) + + c.Assert(cmdUdevadm.Calls(), DeepEquals, [][]string{ + {"udevadm", "settle", "--timeout=180"}, + }) } func (s *partitionTestSuite) TestRemovePartitionsTrivial(c *C) { @@ -320,12 +327,8 @@ func (s *partitionTestSuite) TestRemovePartitionsTrivial(c *C) { func (s *partitionTestSuite) TestRemovePartitions(c *C) { m := map[string]*disks.MockDiskMapping{ "/dev/node": { - DevNum: "42:0", - // this is so that the updated version will be found after we delete - // the partitions and reload the partition table - // XXX: this is a bit of a hack but is easier than mocking every - // individual call to find a disk in order - DevNode: "/dev/updated-node", + DevNum: "42:0", + DevNode: "/dev/node", // assume GPT backup header section is 34 sectors long DiskSizeInBytes: (8388574 + 34) * 512, DiskUsableSectorEnd: 8388574 + 1, @@ -375,29 +378,6 @@ func (s *partitionTestSuite) TestRemovePartitions(c *C) { }, }, }, - "/dev/updated-node": { - DevNum: "42:0", - DevNode: "/dev/updated-node", - DiskSizeInBytes: (8388574 + 34) * 512, - DiskUsableSectorEnd: 8388574 + 1, - DiskSchema: "gpt", - ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - SectorSizeBytes: 512, - Structure: []disks.Partition{ - // only the first partition - { - KernelDeviceNode: "/dev/node1", - StartInBytes: 2048 * 512, - SizeInBytes: 2048 * 512, - PartitionType: "21686148-6449-6E6F-744E-656564454649", - PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", - PartitionLabel: "BIOS Boot", - Major: 42, - Minor: 1, - StructureIndex: 1, - }, - }, - }, } restore := disks.MockDeviceNameToDiskMapping(m) @@ -421,22 +401,84 @@ func (s *partitionTestSuite) TestRemovePartitions(c *C) { c.Assert(err, IsNil) c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--no-reread", "--delete", "/dev/updated-node", "3"}, + {"sfdisk", "--no-reread", "--delete", "/dev/node", "3"}, }) - c.Assert(cmdUdevadm.Calls(), DeepEquals, [][]string{ - {"udevadm", "settle", "--timeout=180"}, + // check that the OnDiskVolume was updated as expected + c.Assert(dl.Structure, DeepEquals, []gadget.OnDiskStructure{ + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Name: "BIOS Boot", + Size: 1024 * 1024, + Type: "21686148-6449-6E6F-744E-656564454649", + ID: "2E59D969-52AB-430B-88AC-F83873519F6F", + }, + StartOffset: 1024 * 1024, + Index: 1, + }, + Node: "/dev/node1", + Size: 1024 * 1024, + }, + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Label: "ubuntu-seed", + Name: "Recovery", + Size: 2457600 * 512, + Type: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + ID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + Filesystem: "vfat", + }, + + StartOffset: 1024*1024 + 1024*1024, + Index: 2, + }, + + Node: "/dev/node2", + Size: 2457600 * 512, + }, }) } -func (s *partitionTestSuite) TestRemovePartitionsDoesNotRemoveError(c *C) { - cmdSfdisk := testutil.MockCommand(c, "sfdisk", "") - defer cmdSfdisk.Restore() +const gadgetContentDifferentOrder = `volumes: + pc: + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + content: + - image: pc-boot.img + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 + content: + - image: pc-core.img + - name: Writable + role: system-data + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1200M + - name: Recovery + role: system-seed + filesystem: vfat + # UEFI will boot the ESP partition by default first + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + size: 1200M + content: + - source: grubx64.efi + target: EFI/boot/grubx64.efi +` +func (s *partitionTestSuite) TestRemovePartitionsNonAdjacent(c *C) { m := map[string]*disks.MockDiskMapping{ "/dev/node": { - DevNum: "42:0", - DevNode: "/dev/node", + DevNum: "42:0", + DevNode: "/dev/node", + // assume GPT backup header section is 34 sectors long DiskSizeInBytes: (8388574 + 34) * 512, DiskUsableSectorEnd: 8388574 + 1, DiskSchema: "gpt", @@ -446,7 +488,7 @@ func (s *partitionTestSuite) TestRemovePartitionsDoesNotRemoveError(c *C) { // all 3 partitions present { KernelDeviceNode: "/dev/node1", - StartInBytes: 2048 * 512, + StartInBytes: 1024 * 1024, SizeInBytes: 2048 * 512, PartitionType: "21686148-6449-6E6F-744E-656564454649", PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", @@ -457,31 +499,31 @@ func (s *partitionTestSuite) TestRemovePartitionsDoesNotRemoveError(c *C) { }, { KernelDeviceNode: "/dev/node2", - StartInBytes: 4096 * 512, + StartInBytes: 1024*1024 + 1024*1024, SizeInBytes: 2457600 * 512, - PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - PartitionUUID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", - PartitionLabel: "Recovery", + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + PartitionUUID: "F940029D-BFBB-4887-9D44-321E85C63866", + PartitionLabel: "Writable", Major: 42, Minor: 2, StructureIndex: 2, - FilesystemType: "vfat", - FilesystemUUID: "A644-B807", - FilesystemLabel: "ubuntu-seed", + FilesystemType: "ext4", + FilesystemUUID: "8781-433a", + FilesystemLabel: "ubuntu-data", }, { KernelDeviceNode: "/dev/node3", - StartInBytes: 2461696 * 512, + StartInBytes: 1024*1024 + 1024*1024 + 2457600*512, SizeInBytes: 2457600 * 512, - PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", - PartitionUUID: "F940029D-BFBB-4887-9D44-321E85C63866", - PartitionLabel: "Writable", + PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + PartitionUUID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + PartitionLabel: "Recovery", Major: 42, Minor: 3, StructureIndex: 3, - FilesystemType: "ext4", - FilesystemUUID: "8781-433a", - FilesystemLabel: "ubuntu-data", + FilesystemType: "vfat", + FilesystemUUID: "A644-B807", + FilesystemLabel: "ubuntu-seed", }, }, }, @@ -490,22 +532,61 @@ func (s *partitionTestSuite) TestRemovePartitionsDoesNotRemoveError(c *C) { restore := disks.MockDeviceNameToDiskMapping(m) defer restore() + cmdSfdisk := testutil.MockCommand(c, "sfdisk", "") + defer cmdSfdisk.Restore() + + cmdUdevadm := testutil.MockCommand(c, "udevadm", "") + defer cmdUdevadm.Restore() + dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) - err = makeMockGadget(s.gadgetRoot, gadgetContent) + err = makeMockGadget(s.gadgetRoot, gadgetContentDifferentOrder) c.Assert(err, IsNil) pv, err := gadgettest.MustLayOutSingleVolumeFromGadget(s.gadgetRoot, "", uc20Mod) c.Assert(err, IsNil) - cmdUdevadm := testutil.MockCommand(c, "udevadm", "") - defer cmdUdevadm.Restore() - err = install.RemoveCreatedPartitions(pv, dl) - c.Assert(err, ErrorMatches, "cannot remove partitions: /dev/node3") + c.Assert(err, IsNil) - c.Assert(cmdUdevadm.Calls(), DeepEquals, [][]string{ - {"udevadm", "settle", "--timeout=180"}, + c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ + {"sfdisk", "--no-reread", "--delete", "/dev/node", "2"}, + }) + + // check that the OnDiskVolume was updated as expected + c.Assert(dl.Structure, DeepEquals, []gadget.OnDiskStructure{ + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Name: "BIOS Boot", + Size: 1024 * 1024, + Type: "21686148-6449-6E6F-744E-656564454649", + ID: "2E59D969-52AB-430B-88AC-F83873519F6F", + }, + StartOffset: 1024 * 1024, + Index: 1, + }, + Node: "/dev/node1", + Size: 1024 * 1024, + }, + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Label: "ubuntu-seed", + Name: "Recovery", + Size: 2457600 * 512, + Type: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + ID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + Filesystem: "vfat", + }, + + StartOffset: 1024*1024 + 1024*1024 + 2457600*512, + Index: 3, + }, + + Node: "/dev/node3", + Size: 2457600 * 512, + }, }) } diff --git a/gadget/ondisk.go b/gadget/ondisk.go index 52a0ead5b5..88ff3e6121 100644 --- a/gadget/ondisk.go +++ b/gadget/ondisk.go @@ -157,18 +157,3 @@ func OnDiskVolumeFromDisk(disk disks.Disk) (*OnDiskVolume, error) { return dl, nil } - -// UpdatePartitionList re-reads the partitioning data from the device and -// updates the volume structures in the specified volume. -func UpdatePartitionList(dl *OnDiskVolume) error { - layout, err := OnDiskVolumeFromDevice(dl.Device) - if err != nil { - return fmt.Errorf("cannot read disk layout: %v", err) - } - if dl.ID != layout.ID { - return fmt.Errorf("partition table IDs don't match") - } - - dl.Structure = layout.Structure - return nil -} diff --git a/gadget/ondisk_test.go b/gadget/ondisk_test.go index 94c9783653..2d3e302311 100644 --- a/gadget/ondisk_test.go +++ b/gadget/ondisk_test.go @@ -432,90 +432,3 @@ func (s *ondiskTestSuite) TestDeviceInfoMBR(c *C) { }, }) } - -func (s *ondiskTestSuite) TestUpdatePartitionList(c *C) { - // start with a single partition - m := map[string]*disks.MockDiskMapping{ - "/dev/node": { - DevNum: "42:0", - DevNode: "/dev/node", - DiskSizeInBytes: (8388574 + 1) * 512, - DiskSchema: "gpt", - ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - SectorSizeBytes: 512, - Structure: []disks.Partition{ - { - KernelDeviceNode: "/dev/node1", - StartInBytes: 2048 * 512, - SizeInBytes: 2048 * 512, - PartitionType: "21686148-6449-6E6F-744E-656564454649", - PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", - PartitionLabel: "BIOS Boot", - Major: 42, - Minor: 1, - StructureIndex: 1, - }, - }, - }, - } - - restore := disks.MockDeviceNameToDiskMapping(m) - defer restore() - - dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") - c.Assert(err, IsNil) - - c.Assert(len(dl.Structure), Equals, 1) - c.Assert(dl.Structure[0].Node, Equals, "/dev/node1") - - // add a partition - m2 := map[string]*disks.MockDiskMapping{ - "/dev/node": { - DevNum: "42:0", - DevNode: "/dev/node", - DiskSizeInBytes: (8388574 + 1) * 512, - DiskSchema: "gpt", - ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - SectorSizeBytes: 512, - Structure: []disks.Partition{ - { - KernelDeviceNode: "/dev/node1", - StartInBytes: 2048 * 512, - SizeInBytes: 2048 * 512, - PartitionType: "21686148-6449-6E6F-744E-656564454649", - PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", - PartitionLabel: "BIOS Boot", - Major: 42, - Minor: 1, - StructureIndex: 1, - }, - { - KernelDeviceNode: "/dev/node2", - StartInBytes: 4096 * 512, - SizeInBytes: 2457600 * 512, - PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - PartitionUUID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", - PartitionLabel: "ubuntu-seed", - Major: 42, - Minor: 2, - StructureIndex: 2, - FilesystemType: "vfat", - FilesystemUUID: "A644-B807", - FilesystemLabel: "ubuntu-seed", - }, - }, - }, - } - - restore = disks.MockDeviceNameToDiskMapping(m2) - defer restore() - - // update the partition list - err = gadget.UpdatePartitionList(dl) - c.Assert(err, IsNil) - - // check if the partition list was updated - c.Assert(len(dl.Structure), Equals, 2) - c.Assert(dl.Structure[0].Node, Equals, "/dev/node1") - c.Assert(dl.Structure[1].Node, Equals, "/dev/node2") -} diff --git a/interfaces/builtin/kernel_module_load.go b/interfaces/builtin/kernel_module_load.go new file mode 100644 index 0000000000..541804f045 --- /dev/null +++ b/interfaces/builtin/kernel_module_load.go @@ -0,0 +1,229 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package builtin + +import ( + "errors" + "fmt" + "regexp" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/kmod" + "github.com/snapcore/snapd/snap" +) + +const kernelModuleLoadSummary = `allows constrained control over kernel module loading` + +const kernelModuleLoadBaseDeclarationPlugs = ` + kernel-module-load: + allow-installation: false + deny-auto-connection: true +` + +const kernelModuleLoadBaseDeclarationSlots = ` + kernel-module-load: + allow-installation: + slot-snap-type: + - core + deny-connection: true +` + +var modulesAttrTypeError = errors.New(`kernel-module-load "modules" attribute must be a list of dictionaries`) + +// kernelModuleLoadInterface allows creating transient and persistent modules +type kernelModuleLoadInterface struct { + commonInterface +} + +type loadOption int + +const ( + loadNone loadOption = iota + loadDenied + loadOnBoot +) + +type ModuleInfo struct { + name string + load loadOption + options string +} + +var kernelModuleNameRegexp = regexp.MustCompile(`^[-a-zA-Z0-9_]+$`) +var kernelModuleOptionsRegexp = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9_]*(=[[:graph:]]+)? *)+$`) + +func enumerateModules(plug interfaces.Attrer, handleModule func(moduleInfo *ModuleInfo) error) error { + modulesAttr, ok := plug.Lookup("modules") + if !ok { + return nil + } + modules, ok := modulesAttr.([]interface{}) + if !ok { + return modulesAttrTypeError + } + + for _, m := range modules { + module, ok := m.(map[string]interface{}) + if !ok { + return modulesAttrTypeError + } + + name, ok := module["name"].(string) + if !ok { + return errors.New(`kernel-module-load "name" must be a string`) + } + + var load loadOption + if loadAttr, found := module["load"]; found { + loadString, ok := loadAttr.(string) + if !ok { + return errors.New(`kernel-module-load "load" must be a string`) + } + + switch loadString { + case "denied": + load = loadDenied + case "on-boot": + load = loadOnBoot + default: + return fmt.Errorf(`kernel-module-load "load" value is unrecognized: %q`, loadString) + } + } + + var options string + if optionsAttr, found := module["options"]; found { + options, ok = optionsAttr.(string) + if !ok { + return errors.New(`kernel-module-load "options" must be a string`) + } + } + + moduleInfo := &ModuleInfo{ + name: name, + load: load, + options: options, + } + + if err := handleModule(moduleInfo); err != nil { + return err + } + } + + return nil +} + +func validateNameAttr(name string) error { + if !kernelModuleNameRegexp.MatchString(name) { + return errors.New(`kernel-module-load "name" attribute is not a valid module name`) + } + + return nil +} + +func validateOptionsAttr(moduleInfo *ModuleInfo) error { + if moduleInfo.options == "" { + return nil + } + + if moduleInfo.load == loadDenied { + return errors.New(`kernel-module-load "options" attribute incompatible with "load: denied"`) + } + + if !kernelModuleOptionsRegexp.MatchString(moduleInfo.options) { + return fmt.Errorf(`kernel-module-load "options" attribute contains invalid characters: %q`, moduleInfo.options) + } + + return nil +} + +func validateModuleInfo(moduleInfo *ModuleInfo) error { + if err := validateNameAttr(moduleInfo.name); err != nil { + return err + } + + if err := validateOptionsAttr(moduleInfo); err != nil { + return err + } + + if moduleInfo.options == "" && moduleInfo.load == loadNone { + return errors.New(`kernel-module-load: must specify at least "load" or "options"`) + } + + return nil +} + +func (iface *kernelModuleLoadInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error { + numModulesEntries := 0 + err := enumerateModules(plug, func(moduleInfo *ModuleInfo) error { + numModulesEntries++ + return validateModuleInfo(moduleInfo) + }) + if err != nil { + return err + } + + if numModulesEntries == 0 { + return modulesAttrTypeError + } + + return nil +} + +func (iface *kernelModuleLoadInterface) KModConnectedPlug(spec *kmod.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + err := enumerateModules(plug, func(moduleInfo *ModuleInfo) error { + var err error + switch moduleInfo.load { + case loadDenied: + err = spec.DisallowModule(moduleInfo.name) + case loadOnBoot: + err = spec.AddModule(moduleInfo.name) + if err != nil { + break + } + fallthrough + case loadNone: + if len(moduleInfo.options) > 0 { + err = spec.SetModuleOptions(moduleInfo.name, moduleInfo.options) + } + default: + // we can panic, this will be catched on validation + panic("Unsupported module load option") + } + return err + }) + return err +} + +func (iface *kernelModuleLoadInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool { + return true +} + +func init() { + registerIface(&kernelModuleLoadInterface{ + commonInterface: commonInterface{ + name: "kernel-module-load", + summary: kernelModuleLoadSummary, + baseDeclarationPlugs: kernelModuleLoadBaseDeclarationPlugs, + baseDeclarationSlots: kernelModuleLoadBaseDeclarationSlots, + implicitOnCore: true, + implicitOnClassic: true, + }, + }) +} diff --git a/interfaces/builtin/kernel_module_load_test.go b/interfaces/builtin/kernel_module_load_test.go new file mode 100644 index 0000000000..8689fa6c2d --- /dev/null +++ b/interfaces/builtin/kernel_module_load_test.go @@ -0,0 +1,193 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package builtin_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/kmod" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type KernelModuleLoadInterfaceSuite struct { + testutil.BaseTest + + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&KernelModuleLoadInterfaceSuite{ + iface: builtin.MustInterface("kernel-module-load"), +}) + +const kernelModuleLoadConsumerYaml = `name: consumer +version: 0 +plugs: + kmod: + interface: kernel-module-load + modules: + - name: forbidden + load: denied + - name: mymodule1 + load: on-boot + options: p1=3 p2=true p3 + - name: mymodule2 + options: param_1=ok param_2=false +apps: + app: + plugs: [kmod] +` + +const kernelModuleLoadCoreYaml = `name: core +version: 0 +type: os +slots: + kernel-module-load: +` + +func (s *KernelModuleLoadInterfaceSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + s.plug, s.plugInfo = MockConnectedPlug(c, kernelModuleLoadConsumerYaml, nil, "kmod") + s.slot, s.slotInfo = MockConnectedSlot(c, kernelModuleLoadCoreYaml, nil, "kernel-module-load") +} + +func (s *KernelModuleLoadInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "kernel-module-load") +} + +func (s *KernelModuleLoadInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *KernelModuleLoadInterfaceSuite) TestSanitizePlug(c *C) { + c.Check(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) + c.Check(interfaces.BeforeConnectPlug(s.iface, s.plug), IsNil) +} + +func (s *KernelModuleLoadInterfaceSuite) TestSanitizePlugUnhappy(c *C) { + var kernelModuleLoadYaml = `name: consumer +version: 0 +plugs: + kmod: + interface: kernel-module-load + %s +apps: + app: + plugs: [kmod] +` + data := []struct { + plugYaml string + expectedError string + }{ + { + "", // missing "modules" attribute + `kernel-module-load "modules" attribute must be a list of dictionaries`, + }, + { + "modules: a string", + `kernel-module-load "modules" attribute must be a list of dictionaries`, + }, + { + "modules: [this, is, a, list]", + `kernel-module-load "modules" attribute must be a list of dictionaries`, + }, + { + "modules:\n - name: [this, is, a, list]", + `kernel-module-load "name" must be a string`, + }, + { + "modules:\n - name: pcspkr", + `kernel-module-load: must specify at least "load" or "options"`, + }, + { + "modules:\n - name: pcspkr\n load: [yes, no]", + `kernel-module-load "load" must be a string`, + }, + { + "modules:\n - name: pcspkr\n load: maybe", + `kernel-module-load "load" value is unrecognized: "maybe"`, + }, + { + "modules:\n - name: pcspkr\n options: [one, two]", + `kernel-module-load "options" must be a string`, + }, + { + "modules:\n - name: pcspkr\n options: \"a\\nnewline\"", + `kernel-module-load "options" attribute contains invalid characters: "a\\nnewline"`, + }, + { + "modules:\n - name: pcspkr\n options: \"5tartWithNumber=1\"", + `kernel-module-load "options" attribute contains invalid characters: "5tartWithNumber=1"`, + }, + { + "modules:\n - name: pcspkr\n options: \"no-dashes\"", + `kernel-module-load "options" attribute contains invalid characters: "no-dashes"`, + }, + { + "modules:\n - name: pcspkr\n load: denied\n options: p1=true", + `kernel-module-load "options" attribute incompatible with "load: denied"`, + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(kernelModuleLoadYaml, testData.plugYaml) + plug, _ := MockConnectedPlug(c, snapYaml, nil, "kmod") + err := interfaces.BeforeConnectPlug(s.iface, plug) + c.Check(err, ErrorMatches, testData.expectedError, Commentf("yaml: %s", testData.plugYaml)) + } +} + +func (s *KernelModuleLoadInterfaceSuite) TestKModSpec(c *C) { + spec := &kmod.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Check(spec.Modules(), DeepEquals, map[string]bool{ + "mymodule1": true, + }) + c.Check(spec.ModuleOptions(), DeepEquals, map[string]string{ + "mymodule1": "p1=3 p2=true p3", + "mymodule2": "param_1=ok param_2=false", + }) + c.Check(spec.DisallowedModules(), DeepEquals, []string{"forbidden"}) +} + +func (s *KernelModuleLoadInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Assert(si.ImplicitOnCore, Equals, true) + c.Assert(si.ImplicitOnClassic, Equals, true) + c.Assert(si.Summary, Equals, `allows constrained control over kernel module loading`) + c.Assert(si.BaseDeclarationSlots, testutil.Contains, "kernel-module-load") +} + +func (s *KernelModuleLoadInterfaceSuite) TestAutoConnect(c *C) { + c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true) +} + +func (s *KernelModuleLoadInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff --git a/interfaces/builtin/mount_control.go b/interfaces/builtin/mount_control.go new file mode 100644 index 0000000000..4a0b82af35 --- /dev/null +++ b/interfaces/builtin/mount_control.go @@ -0,0 +1,490 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package builtin + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/utils" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/systemd" +) + +const mountControlSummary = `allows creating transient and persistent mounts` + +const mountControlBaseDeclarationPlugs = ` + mount-control: + allow-installation: false + deny-auto-connection: true +` + +const mountControlBaseDeclarationSlots = ` + mount-control: + allow-installation: + slot-snap-type: + - core + deny-connection: true +` + +var mountAttrTypeError = errors.New(`mount-control "mount" attribute must be a list of dictionaries`) + +const mountControlConnectedPlugSecComp = ` +# Description: Allow mount and umount syscall access. No filtering here, as we +# rely on AppArmor to filter the mount operations. +mount +umount +umount2 +` + +// The reason why this list is not shared with osutil.MountOptsToCommonFlags or +// other parts of the codebase is that this one only contains the options which +// have been deemed safe and have been vetted by the security team. +var allowedMountOptions = []string{ + "async", + "atime", + "bind", + "diratime", + "dirsync", + "iversion", + "lazytime", + "nofail", + "noiversion", + "nomand", + "noatime", + "nodev", + "nodiratime", + "noexec", + "nolazytime", + "norelatime", + "nosuid", + "nostrictatime", + "nouser", + "relatime", + "strictatime", + "sync", + "ro", + "rw", +} + +// A few mount flags are special in that if they are specified, the filesystem +// type is ignored. We list them here, and we will ensure that the plug +// declaration does not specify a type, if any of them is present among the +// options. +var optionsWithoutFsType = []string{ + "bind", + // Note: the following flags should also fall into this list, but we are + // not currently allowing them (and don't plan to): + // - "make-private" + // - "make-shared" + // - "make-slave" + // - "make-unbindable" + // - "move" + // - "remount" +} + +// List of allowed filesystem types. This can be extended, keeping in mind that +// the filesystems in the following list were considered either dangerous or +// not relevant for this interface: +// bpf +// cgroup +// cgroup2 +// debugfs +// devpts +// ecryptfs +// hugetlbfs +// overlayfs +// proc +// securityfs +// sysfs +// tracefs +var allowedFSTypes = []string{ + "aufs", + "autofs", + "btrfs", + "ext2", + "ext3", + "ext4", + "hfs", + "iso9660", + "jfs", + "msdos", + "ntfs", + "ramfs", + "reiserfs", + "squashfs", + "tmpfs", + "ubifs", + "udf", + "ufs", + "vfat", + "zfs", + "xfs", +} + +// mountControlInterface allows creating transient and persistent mounts +type mountControlInterface struct { + commonInterface +} + +// The "what" and "where" attributes end up in the AppArmor profile, surrounded +// by double quotes; to ensure that a malicious snap cannot inject arbitrary +// rules by specifying something like +// where: $SNAP_DATA/foo", /** rw, # +// which would generate a profile line like: +// mount options=() "$SNAP_DATA/foo", /** rw, #" +// (which would grant read-write access to the whole filesystem), it's enough +// to exclude the `"` character: without it, whatever is written in the +// attribute will not be able to escape being treated like a pattern. +// +// To be safe, there's more to be done: the pattern also needs to be valid, as +// a malformed one (for example, a pattern having an unmatched `}`) would cause +// apparmor_parser to fail loading the profile. For this situation, we use the +// PathPattern interface to validate the pattern. +// +// Besides that, we are also excluding the `@` character, which is used to mark +// AppArmor variables (tunables): when generating the profile we lack the +// knowledge of which variables have been defined, so it's safer to exclude +// them. +// The what attribute regular expression here is intentionally permissive of +// nearly any path, and due to the super-privileged nature of this interface it +// is expected that sensible values of what are enforced by the store manual +// review queue and security teams. +var ( + whatRegexp = regexp.MustCompile(`^(none|/[^"@]*)$`) + whereRegexp = regexp.MustCompile(`^(\$SNAP_COMMON|\$SNAP_DATA)?/[^\$"@]+$`) +) + +// Excluding spaces and other characters which might allow constructing a +// malicious string like +// auto) options=() /malicious/content /var/lib/snapd/hostfs/...,\n mount fstype=( +var typeRegexp = regexp.MustCompile(`^[a-z0-9]+$`) + +type MountInfo struct { + what string + where string + persistent bool + types []string + options []string +} + +func parseStringList(mountEntry map[string]interface{}, fieldName string) ([]string, error) { + var list []string + value, ok := mountEntry[fieldName] + if ok { + interfaceList, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf(`mount-control "%s" must be an array of strings (got %q)`, fieldName, value) + } + for i, iface := range interfaceList { + valueString, ok := iface.(string) + if !ok { + return nil, fmt.Errorf(`mount-control "%s" element %d not a string (%q)`, fieldName, i+1, iface) + } + list = append(list, valueString) + } + } + return list, nil +} + +func enumerateMounts(plug interfaces.Attrer, fn func(mountInfo *MountInfo) error) error { + mountAttr, ok := plug.Lookup("mount") + if !ok { + return nil + } + mounts, ok := mountAttr.([]interface{}) + if !ok { + return mountAttrTypeError + } + + for _, m := range mounts { + mount, ok := m.(map[string]interface{}) + if !ok { + return mountAttrTypeError + } + + what, ok := mount["what"].(string) + if !ok { + return fmt.Errorf(`mount-control "what" must be a string`) + } + + where, ok := mount["where"].(string) + if !ok { + return fmt.Errorf(`mount-control "where" must be a string`) + } + + persistent := false + persistentValue, ok := mount["persistent"] + if ok { + if persistent, ok = persistentValue.(bool); !ok { + return fmt.Errorf(`mount-control "persistent" must be a boolean`) + } + } + + types, err := parseStringList(mount, "type") + if err != nil { + return err + } + + options, err := parseStringList(mount, "options") + if err != nil { + return err + } + + mountInfo := &MountInfo{ + what: what, + where: where, + persistent: persistent, + types: types, + options: options, + } + + if err := fn(mountInfo); err != nil { + return err + } + } + + return nil +} + +func validateWhatAttr(what string) error { + if !whatRegexp.MatchString(what) { + return fmt.Errorf(`mount-control "what" attribute is invalid: must start with / and not contain special characters`) + } + + if !cleanSubPath(what) { + return fmt.Errorf(`mount-control "what" pattern is not clean: %q`, what) + } + + if _, err := utils.NewPathPattern(what); err != nil { + return fmt.Errorf(`mount-control "what" setting cannot be used: %v`, err) + } + + return nil +} + +func validateWhereAttr(where string) error { + if !whereRegexp.MatchString(where) { + return fmt.Errorf(`mount-control "where" attribute must start with $SNAP_COMMON, $SNAP_DATA or / and not contain special characters`) + } + + if !cleanSubPath(where) { + return fmt.Errorf(`mount-control "where" pattern is not clean: %q`, where) + } + + if _, err := utils.NewPathPattern(where); err != nil { + return fmt.Errorf(`mount-control "where" setting cannot be used: %v`, err) + } + + return nil +} + +func validateMountTypes(types []string) error { + includesTmpfs := false + for _, t := range types { + if !typeRegexp.MatchString(t) { + return fmt.Errorf(`mount-control filesystem type invalid: %q`, t) + } + if !strutil.ListContains(allowedFSTypes, t) { + return fmt.Errorf(`mount-control forbidden filesystem type: %q`, t) + } + if t == "tmpfs" { + includesTmpfs = true + } + } + + if includesTmpfs && len(types) > 1 { + return errors.New(`mount-control filesystem type "tmpfs" cannot be listed with other types`) + } + return nil +} + +func validateMountOptions(options []string) error { + if len(options) == 0 { + return errors.New(`mount-control "options" cannot be empty`) + } + for _, o := range options { + if !strutil.ListContains(allowedMountOptions, o) { + return fmt.Errorf(`mount-control option unrecognized or forbidden: %q`, o) + } + } + return nil +} + +// Find the first option which is incompatible with a FS type declaration +func optionIncompatibleWithFsType(options []string) string { + for _, o := range options { + if strutil.ListContains(optionsWithoutFsType, o) { + return o + } + } + return "" +} + +func validateMountInfo(mountInfo *MountInfo) error { + if err := validateWhatAttr(mountInfo.what); err != nil { + return err + } + + if err := validateWhereAttr(mountInfo.where); err != nil { + return err + } + + if err := validateMountTypes(mountInfo.types); err != nil { + return err + } + + if err := validateMountOptions(mountInfo.options); err != nil { + return err + } + + // Check if any options are incompatible with specifying a FS type + fsExclusiveOption := optionIncompatibleWithFsType(mountInfo.options) + if fsExclusiveOption != "" && len(mountInfo.types) > 0 { + return fmt.Errorf(`mount-control option %q is incompatible with specifying filesystem type`, fsExclusiveOption) + } + + // "what" must be set to "none" iff the type is "tmpfs" + isTmpfs := len(mountInfo.types) == 1 && mountInfo.types[0] == "tmpfs" + if mountInfo.what == "none" { + if !isTmpfs { + return errors.New(`mount-control "what" attribute can be "none" only with "tmpfs"`) + } + } else if isTmpfs { + return fmt.Errorf(`mount-control "what" attribute must be "none" with "tmpfs"; found %q instead`, mountInfo.what) + } + + return nil +} + +func (iface *mountControlInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error { + // The systemd.ListMountUnits() method works by issuing the command + // "systemctl show *.mount", but globbing was only added in systemd v209. + if err := systemd.EnsureAtLeast(209); err != nil { + return err + } + + hasMountEntries := false + err := enumerateMounts(plug, func(mountInfo *MountInfo) error { + hasMountEntries = true + return validateMountInfo(mountInfo) + }) + if err != nil { + return err + } + + if !hasMountEntries { + return mountAttrTypeError + } + + return nil +} + +func (iface *mountControlInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + mountControlSnippet := bytes.NewBuffer(nil) + emit := func(f string, args ...interface{}) { + fmt.Fprintf(mountControlSnippet, f, args...) + } + snapInfo := plug.Snap() + + emit(` + # Rules added by the mount-control interface + capability sys_admin, # for mount + + owner @{PROC}/@{pid}/mounts r, + owner @{PROC}/@{pid}/mountinfo r, + owner @{PROC}/self/mountinfo r, + + /{,usr/}bin/mount ixr, + /{,usr/}bin/umount ixr, + # mount/umount (via libmount) track some mount info in these files + /run/mount/utab* wrlk, +`) + + // No validation is occurring here, as it was already performed in + // BeforeConnectPlug() + enumerateMounts(plug, func(mountInfo *MountInfo) error { + + source := mountInfo.what + target := mountInfo.where + if target[0] == '$' { + matches := whereRegexp.FindStringSubmatchIndex(target) + if matches == nil || len(matches) < 4 { + // This cannot really happen, as the string wouldn't pass the validation + return fmt.Errorf(`internal error: "where" fails to match regexp: %q`, mountInfo.where) + } + // the first two elements in "matches" are the boundaries of the whole + // string; the next two are the boundaries of the first match, which is + // what we care about as it contains the environment variable we want + // to expand: + variableStart, variableEnd := matches[2], matches[3] + variable := target[variableStart:variableEnd] + expanded := snapInfo.ExpandSnapVariables(variable) + target = expanded + target[variableEnd:] + } + + var typeRule string + if optionIncompatibleWithFsType(mountInfo.options) != "" { + // In this rule the FS type will not match unless it's empty + typeRule = "" + } else { + var types []string + if len(mountInfo.types) > 0 { + types = mountInfo.types + } else { + types = allowedFSTypes + } + typeRule = "fstype=(" + strings.Join(types, ",") + ")" + } + + options := strings.Join(mountInfo.options, ",") + + emit(" mount %s options=(%s) \"%s\" -> \"%s\",\n", typeRule, options, source, target) + emit(" umount \"%s\",\n", target) + return nil + }) + + spec.AddSnippet(mountControlSnippet.String()) + return nil +} + +func (iface *mountControlInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool { + return true +} + +func init() { + registerIface(&mountControlInterface{ + commonInterface: commonInterface{ + name: "mount-control", + summary: mountControlSummary, + baseDeclarationPlugs: mountControlBaseDeclarationPlugs, + baseDeclarationSlots: mountControlBaseDeclarationSlots, + implicitOnCore: true, + implicitOnClassic: true, + connectedPlugSecComp: mountControlConnectedPlugSecComp, + }, + }) +} diff --git a/interfaces/builtin/mount_control_test.go b/interfaces/builtin/mount_control_test.go new file mode 100644 index 0000000000..2cb6a46e81 --- /dev/null +++ b/interfaces/builtin/mount_control_test.go @@ -0,0 +1,316 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package builtin_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/seccomp" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/systemd" + "github.com/snapcore/snapd/testutil" +) + +type MountControlInterfaceSuite struct { + testutil.BaseTest + + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&MountControlInterfaceSuite{ + iface: builtin.MustInterface("mount-control"), +}) + +const mountControlConsumerYaml = `name: consumer +version: 0 +plugs: + mntctl: + interface: mount-control + mount: + - what: /dev/sd* + where: /media/** + type: [ext2, ext3, ext4] + options: [rw, sync] + - what: /usr/** + where: $SNAP_COMMON/** + options: [bind] + - what: /dev/sda{0,1} + where: $SNAP_COMMON/** + options: [ro] + - what: /dev/sda[0-1] + where: $SNAP_COMMON/{foo,other,**} + options: [sync] +apps: + app: + plugs: [mntctl] +` + +const mountControlCoreYaml = `name: core +version: 0 +type: os +slots: + mount-control: +` + +func (s *MountControlInterfaceSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + s.plug, s.plugInfo = MockConnectedPlug(c, mountControlConsumerYaml, nil, "mntctl") + s.slot, s.slotInfo = MockConnectedSlot(c, mountControlCoreYaml, nil, "mount-control") + + s.AddCleanup(systemd.MockSystemdVersion(210, nil)) +} + +func (s *MountControlInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "mount-control") +} + +func (s *MountControlInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *MountControlInterfaceSuite) TestSanitizePlug(c *C) { + c.Check(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) + c.Check(interfaces.BeforeConnectPlug(s.iface, s.plug), IsNil) +} + +func (s *MountControlInterfaceSuite) TestSanitizePlugOldSystemd(c *C) { + restore := systemd.MockSystemdVersion(208, nil) + defer restore() + err := interfaces.BeforeConnectPlug(s.iface, s.plug) + c.Assert(err, ErrorMatches, `systemd version 208 is too old \(expected at least 209\)`) +} + +func (s *MountControlInterfaceSuite) TestSanitizePlugUnhappy(c *C) { + var mountControlYaml = `name: consumer +version: 0 +plugs: + mntctl: + interface: mount-control + %s +apps: + app: + plugs: [mntctl] +` + data := []struct { + plugYaml string + expectedError string + }{ + { + "", // missing "mount" attribute + `mount-control "mount" attribute must be a list of dictionaries`, + }, + { + "mount: a string", + `mount-control "mount" attribute must be a list of dictionaries`, + }, + { + "mount: [this, is, a, list]", + `mount-control "mount" attribute must be a list of dictionaries`, + }, + { + "mount:\n - what: [this, is, a, list]\n where: /media/**", + `mount-control "what" must be a string`, + }, + { + "mount:\n - what: /path/\n where: [this, is, a, list]", + `mount-control "where" must be a string`, + }, + { + "mount:\n - what: /\n where: /\n persistent: string", + `mount-control "persistent" must be a boolean`, + }, + { + "mount:\n - what: /\n where: /\n type: string", + `mount-control "type" must be an array of strings.*`, + }, + { + "mount:\n - what: /\n where: /\n type: [true, false]", + `mount-control "type" element 1 not a string.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [auto)]", + `mount-control filesystem type invalid.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [upperCase]", + `mount-control filesystem type invalid.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [two words]", + `mount-control filesystem type invalid.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n", + `mount-control "options" cannot be empty`, + }, + { + "mount:\n - what: /\n where: /\n options: string", + `mount-control "options" must be an array of strings.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n options: []", + `mount-control "options" cannot be empty`, + }, + { + "mount:\n - what: here\n where: /mnt", + `mount-control "what" attribute is invalid: must start with / and not contain special characters`, + }, + { + "mount:\n - what: /double\"quote\n where: /mnt", + `mount-control "what" attribute is invalid: must start with / and not contain special characters`, + }, + { + "mount:\n - what: /variables/are/not/@{allowed}\n where: /mnt", + `mount-control "what" attribute is invalid: must start with / and not contain special characters`, + }, + { + "mount:\n - what: /invalid}pattern\n where: /mnt", + `mount-control "what" setting cannot be used: invalid closing brace, no matching open.*`, + }, + { + "mount:\n - what: /\n where: /\n options: [ro]", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: /media/no\"quotes", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: /media/no@{variables}", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: $SNAP_DATA/$SNAP_DATA", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: /$SNAP_DATA", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: /media/invalid[path", + `mount-control "where" setting cannot be used: missing closing bracket ']'.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n options: [sync,invalid]", + `mount-control option unrecognized or forbidden: "invalid"`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [ext4,debugfs]", + `mount-control forbidden filesystem type: "debugfs"`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [ext4]\n options: [rw,bind]", + `mount-control option "bind" is incompatible with specifying filesystem type`, + }, + { + "mount:\n - what: /tmp/..\n where: /media/*", + `mount-control "what" pattern is not clean:.*`, + }, + { + "mount:\n - what: /\n where: /media/../etc", + `mount-control "where" pattern is not clean:.*`, + }, + { + "mount:\n - what: none\n where: /media/*\n options: [rw]", + `mount-control "what" attribute can be "none" only with "tmpfs"`, + }, + { + "mount:\n - what: none\n where: /media/*\n options: [rw]\n type: [ext4,ntfs]", + `mount-control "what" attribute can be "none" only with "tmpfs"`, + }, + { + "mount:\n - what: none\n where: /media/*\n options: [rw]\n type: [tmpfs,ext4]", + `mount-control filesystem type "tmpfs" cannot be listed with other types`, + }, + { + "mount:\n - what: /\n where: /media/*\n options: [rw]\n type: [tmpfs]", + `mount-control "what" attribute must be "none" with "tmpfs"; found "/" instead`, + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(mountControlYaml, testData.plugYaml) + plug, _ := MockConnectedPlug(c, snapYaml, nil, "mntctl") + err := interfaces.BeforeConnectPlug(s.iface, plug) + c.Check(err, ErrorMatches, testData.expectedError, Commentf("Yaml: %s", testData.plugYaml)) + } +} + +func (s *MountControlInterfaceSuite) TestSecCompSpec(c *C) { + spec := &seccomp.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "mount\n") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "umount\n") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "umount2\n") +} + +func (s *MountControlInterfaceSuite) TestAppArmorSpec(c *C) { + spec := &apparmor.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `capability sys_admin,`) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/{,usr/}bin/mount ixr,`) + + expectedMountLine1 := `mount fstype=(ext2,ext3,ext4) options=(rw,sync) "/dev/sd*" -> "/media/**",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine1) + + expectedMountLine2 := `mount options=(bind) "/usr/**" -> "/var/snap/consumer/common/**",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine2) + + expectedMountLine3 := `mount fstype=(` + + `aufs,autofs,btrfs,ext2,ext3,ext4,hfs,iso9660,jfs,msdos,ntfs,ramfs,` + + `reiserfs,squashfs,tmpfs,ubifs,udf,ufs,vfat,zfs,xfs` + + `) options=(ro) "/dev/sda{0,1}" -> "/var/snap/consumer/common/**",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine3) + + expectedMountLine4 := `mount fstype=(` + + `aufs,autofs,btrfs,ext2,ext3,ext4,hfs,iso9660,jfs,msdos,ntfs,ramfs,` + + `reiserfs,squashfs,tmpfs,ubifs,udf,ufs,vfat,zfs,xfs` + + `) options=(sync) "/dev/sda[0-1]" -> "/var/snap/consumer/common/{foo,other,**}",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine4) +} + +func (s *MountControlInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Assert(si.ImplicitOnCore, Equals, true) + c.Assert(si.ImplicitOnClassic, Equals, true) + c.Assert(si.Summary, Equals, `allows creating transient and persistent mounts`) + c.Assert(si.BaseDeclarationSlots, testutil.Contains, "mount-control") +} + +func (s *MountControlInterfaceSuite) TestAutoConnect(c *C) { + c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true) +} + +func (s *MountControlInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff --git a/interfaces/builtin/opengl.go b/interfaces/builtin/opengl.go index fe1217d533..c41719fd4c 100644 --- a/interfaces/builtin/opengl.go +++ b/interfaces/builtin/opengl.go @@ -127,6 +127,7 @@ unix (bind,listen) type=seqpacket addr="@cuda-uvmfd-[0-9a-f]*", # /sys/devices /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/config r, /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/revision r, +/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/boot_vga r, /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}class r, /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}device r, /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}vendor r, diff --git a/interfaces/builtin/shared_memory.go b/interfaces/builtin/shared_memory.go new file mode 100644 index 0000000000..b398d5f8a7 --- /dev/null +++ b/interfaces/builtin/shared_memory.go @@ -0,0 +1,236 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package builtin + +import ( + "bytes" + "errors" + "fmt" + "io" + "strings" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/snap" +) + +const sharedMemorySummary = `allows two snaps to use predefined shared memory objects` + +const sharedMemoryBaseDeclarationPlugs = ` + shared-memory: + allow-installation: true + allow-connection: + slot-attributes: + shared-memory: $PLUG(shared-memory) + allow-auto-connection: + slot-publisher-id: + - $PLUG_PUBLISHER_ID + slot-attributes: + shared-memory: $PLUG(shared-memory) +` + +const sharedMemoryBaseDeclarationSlots = ` + shared-memory: + allow-installation: false + deny-connection: true + deny-auto-connection: true +` + +func validateSharedMemoryPath(path string) error { + if len(path) == 0 { + return fmt.Errorf("shared-memory interface path is empty") + } + + if strings.TrimSpace(path) != path { + return fmt.Errorf("shared-memory interface path has leading or trailing spaces: %q", path) + } + + // TODO: allow "*" as a globbing character; figure out if more AARE should be allowed + if err := apparmor.ValidateNoAppArmorRegexp(path); err != nil { + return fmt.Errorf("shared-memory interface path is invalid: %v", err) + } + + // TODO: consider whether we should remove this check and allow full SHM path + if strings.Contains(path, "/") { + return fmt.Errorf("shared-memory interface path should not contain '/': %q", path) + } + + // The check above protects from most unclean paths, but one could still specify ".." + if !cleanSubPath(path) { + return fmt.Errorf("shared-memory interface path is not clean: %q", path) + } + + return nil +} + +func stringListAttribute(attrer interfaces.Attrer, key string) ([]string, error) { + parseError := func(key string, value interface{}) error { + return fmt.Errorf(`shared-memory %q attribute must be a list of strings, not "%v"`, key, value) + } + attr, ok := attrer.Lookup(key) + if !ok { + return nil, nil + } + + attrList, ok := attr.([]interface{}) + if !ok || len(attrList) == 0 { + return nil, parseError(key, attr) + } + + stringList := make([]string, 0, len(attrList)) + for _, value := range attrList { + s, ok := value.(string) + if !ok { + return nil, parseError(key, attrList) + } + stringList = append(stringList, s) + } + return stringList, nil +} + +// sharedMemoryInterface allows sharing sharedMemory between snaps +type sharedMemoryInterface struct{} + +func (iface *sharedMemoryInterface) Name() string { + return "shared-memory" +} + +func (iface *sharedMemoryInterface) StaticInfo() interfaces.StaticInfo { + return interfaces.StaticInfo{ + Summary: sharedMemorySummary, + BaseDeclarationPlugs: sharedMemoryBaseDeclarationPlugs, + BaseDeclarationSlots: sharedMemoryBaseDeclarationSlots, + AffectsPlugOnRefresh: true, + } +} + +func (iface *sharedMemoryInterface) BeforePrepareSlot(slot *snap.SlotInfo) error { + sharedMemoryAttr, isSet := slot.Attrs["shared-memory"] + sharedMemory, ok := sharedMemoryAttr.(string) + if isSet && !ok { + return fmt.Errorf(`shared-memory "shared-memory" attribute must be a string, not %v`, + slot.Attrs["shared-memory"]) + } + if sharedMemory == "" { + if slot.Attrs == nil { + slot.Attrs = make(map[string]interface{}) + } + // shared-memory defaults to "slot" name if unspecified + slot.Attrs["shared-memory"] = slot.Name + } + + readPaths, err := stringListAttribute(slot, "read") + if err != nil { + return err + } + + writePaths, err := stringListAttribute(slot, "write") + if err != nil { + return err + } + + // We perform the same validation for read-only and writable paths, so + // let's just put them all in the same array + allPaths := append(readPaths, writePaths...) + if len(allPaths) == 0 { + return errors.New(`shared memory interface requires at least a valid "read" or "write" attribute`) + } + + for _, path := range allPaths { + if err := validateSharedMemoryPath(path); err != nil { + return err + } + } + + return nil +} + +type sharedMemorySnippetType int + +const ( + snippetForSlot sharedMemorySnippetType = iota + snippetForPlug +) + +func writeSharedMemoryPaths(w io.Writer, slot *interfaces.ConnectedSlot, + snippetType sharedMemorySnippetType) { + emitWritableRule := func(path string) { + // Ubuntu 14.04 uses /run/shm instead of the most common /dev/shm + fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" rwk,\n", path) + } + + // All checks were already done in BeforePrepare{Plug,Slot} + writePaths, _ := stringListAttribute(slot, "write") + for _, path := range writePaths { + emitWritableRule(path) + } + readPaths, _ := stringListAttribute(slot, "read") + for _, path := range readPaths { + if snippetType == snippetForPlug { + // grant read-only access + fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" r,\n", path) + } else { + // the slot must still be granted write access, because the "read" + // and "write" attributes are meant to affect the plug only + emitWritableRule(path) + } + } +} + +func (iface *sharedMemoryInterface) BeforePreparePlug(plug *snap.PlugInfo) error { + sharedMemoryAttr, isSet := plug.Attrs["shared-memory"] + sharedMemory, ok := sharedMemoryAttr.(string) + if isSet && !ok { + return fmt.Errorf(`shared-memory "shared-memory" attribute must be a string, not %v`, + plug.Attrs["shared-memory"]) + } + if sharedMemory == "" { + if plug.Attrs == nil { + plug.Attrs = make(map[string]interface{}) + } + // shared-memory defaults to "plug" name if unspecified + plug.Attrs["shared-memory"] = plug.Name + } + + return nil +} + +func (iface *sharedMemoryInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + sharedMemorySnippet := &bytes.Buffer{} + writeSharedMemoryPaths(sharedMemorySnippet, slot, snippetForPlug) + spec.AddSnippet(sharedMemorySnippet.String()) + return nil +} + +func (iface *sharedMemoryInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + sharedMemorySnippet := &bytes.Buffer{} + writeSharedMemoryPaths(sharedMemorySnippet, slot, snippetForSlot) + spec.AddSnippet(sharedMemorySnippet.String()) + return nil +} + +func (iface *sharedMemoryInterface) AutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool { + // allow what declarations allowed + return true +} + +func init() { + registerIface(&sharedMemoryInterface{}) +} diff --git a/interfaces/builtin/shared_memory_test.go b/interfaces/builtin/shared_memory_test.go new file mode 100644 index 0000000000..a67012407f --- /dev/null +++ b/interfaces/builtin/shared_memory_test.go @@ -0,0 +1,304 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package builtin_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type SharedMemoryInterfaceSuite struct { + testutil.BaseTest + + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&SharedMemoryInterfaceSuite{ + iface: builtin.MustInterface("shared-memory"), +}) + +const sharedMemoryConsumerYaml = `name: consumer +version: 0 +plugs: + shmem: + interface: shared-memory + shared-memory: foo +apps: + app: + plugs: [shmem] +` + +const sharedMemoryProviderYaml = `name: provider +version: 0 +slots: + shmem: + interface: shared-memory + shared-memory: foo + write: [ bar ] + read: [ bar-ro ] +apps: + app: + slots: [shmem] +` + +func (s *SharedMemoryInterfaceSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + s.plug, s.plugInfo = MockConnectedPlug(c, sharedMemoryConsumerYaml, nil, "shmem") + s.slot, s.slotInfo = MockConnectedSlot(c, sharedMemoryProviderYaml, nil, "shmem") +} + +func (s *SharedMemoryInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "shared-memory") +} + +func (s *SharedMemoryInterfaceSuite) TestSanitizePlug(c *C) { + c.Check(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) + c.Check(interfaces.BeforeConnectPlug(s.iface, s.plug), IsNil) +} + +func (s *SharedMemoryInterfaceSuite) TestSanitizePlugUnhappy(c *C) { + var sharedMemoryYaml = `name: consumer +version: 0 +plugs: + shmem: + interface: shared-memory + %s +apps: + app: + plugs: [shmem] +` + data := []struct { + plugYaml string + expectedError string + }{ + { + "shared-memory: [one two]", + `shared-memory "shared-memory" attribute must be a string, not \[one two\]`, + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(sharedMemoryYaml, testData.plugYaml) + _, plug := MockConnectedPlug(c, snapYaml, nil, "shmem") + err := interfaces.BeforePreparePlug(s.iface, plug) + c.Check(err, ErrorMatches, testData.expectedError, Commentf("yaml: %s", testData.plugYaml)) + } +} + +func (s *SharedMemoryInterfaceSuite) TestPlugShmAttribute(c *C) { + var plugYamlTemplate = `name: consumer +version: 0 +plugs: + shmem: + interface: shared-memory + %s +apps: + app: + plugs: [shmem] +` + + data := []struct { + plugYaml string + expectedName string + }{ + { + "", // missing "shared-memory" attribute + "shmem", // use the name of the plug + }, + { + "shared-memory: shmemFoo", + "shmemFoo", + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(plugYamlTemplate, testData.plugYaml) + _, plug := MockConnectedPlug(c, snapYaml, nil, "shmem") + err := interfaces.BeforePreparePlug(s.iface, plug) + c.Assert(err, IsNil) + c.Check(plug.Attrs["shared-memory"], Equals, testData.expectedName, + Commentf(`yaml: %q`, testData.plugYaml)) + } +} + +func (s *SharedMemoryInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *SharedMemoryInterfaceSuite) TestSanitizeSlotUnhappy(c *C) { + var sharedMemoryYaml = `name: provider +version: 0 +slots: + shmem: + interface: shared-memory + %s +apps: + app: + slots: [shmem] +` + data := []struct { + slotYaml string + expectedError string + }{ + { + "shared-memory: 12", + `shared-memory "shared-memory" attribute must be a string, not 12`, + }, + { + "", // missing "write" attribute + `shared memory interface requires at least a valid "read" or "write" attribute`, + }, + { + "write: a string", + `shared-memory "write" attribute must be a list of strings, not "a string"`, + }, + { + "read: [Mixed, 12, False, list]", + `shared-memory "read" attribute must be a list of strings, not "\[Mixed 12 false list\]"`, + }, + { + `read: ["ok", "trailing-space "]`, + `shared-memory interface path has leading or trailing spaces: "trailing-space "`, + }, + { + `write: [" leading-space"]`, + `shared-memory interface path has leading or trailing spaces: " leading-space"`, + }, + { + `write: [""]`, + `shared-memory interface path is empty`, + }, + { + `write: [mem/**]`, + `shared-memory interface path is invalid: "mem/\*\*" contains a reserved apparmor char.*`, + }, + { + `read: [..]`, + `shared-memory interface path is not clean: ".."`, + }, + { + `write: [/dev/shm/bar]`, + `shared-memory interface path should not contain '/': "/dev/shm/bar"`, + }, + { + `write: [mem/../etc]`, + `shared-memory interface path should not contain '/': "mem/../etc"`, + }, + { + "write: [valid]\n read: [../invalid]", + `shared-memory interface path should not contain '/': "../invalid"`, + }, + { + "read: [valid]\n write: [../invalid]", + `shared-memory interface path should not contain '/': "../invalid"`, + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(sharedMemoryYaml, testData.slotYaml) + _, slot := MockConnectedSlot(c, snapYaml, nil, "shmem") + err := interfaces.BeforePrepareSlot(s.iface, slot) + c.Check(err, ErrorMatches, testData.expectedError, Commentf("yaml: %s", testData.slotYaml)) + } +} + +func (s *SharedMemoryInterfaceSuite) TestSlotShmAttribute(c *C) { + var slotYamlTemplate = `name: consumer +version: 0 +slots: + shmem: + interface: shared-memory + write: [foo] + %s +apps: + app: + slots: [shmem] +` + + data := []struct { + slotYaml string + expectedName string + }{ + { + "", // missing "shared-memory" attribute + "shmem", // use the name of the slot + }, + { + "shared-memory: shmemBar", + "shmemBar", + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(slotYamlTemplate, testData.slotYaml) + _, slot := MockConnectedSlot(c, snapYaml, nil, "shmem") + err := interfaces.BeforePrepareSlot(s.iface, slot) + c.Assert(err, IsNil) + c.Check(slot.Attrs["shared-memory"], Equals, testData.expectedName, + Commentf(`yaml: %q`, testData.slotYaml)) + } +} + +func (s *SharedMemoryInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Check(si.ImplicitOnCore, Equals, false) + c.Check(si.ImplicitOnClassic, Equals, false) + c.Check(si.Summary, Equals, `allows two snaps to use predefined shared memory objects`) + c.Check(si.BaseDeclarationSlots, testutil.Contains, "shared-memory") +} + +func (s *SharedMemoryInterfaceSuite) TestAppArmorSpec(c *C) { + spec := &apparmor.Specification{} + + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + plugSnippet := spec.SnippetForTag("snap.consumer.app") + + c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.slot), IsNil) + slotSnippet := spec.SnippetForTag("snap.provider.app") + + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.provider.app"}) + + c.Check(plugSnippet, testutil.Contains, `"/{dev,run}/shm/bar" rwk,`) + c.Check(plugSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro" r,`) + + // Slot has read-write permissions to all paths + c.Check(slotSnippet, testutil.Contains, `"/{dev,run}/shm/bar" rwk,`) + c.Check(slotSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro" rwk,`) +} + +func (s *SharedMemoryInterfaceSuite) TestAutoConnect(c *C) { + c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true) +} + +func (s *SharedMemoryInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff --git a/interfaces/kmod/backend.go b/interfaces/kmod/backend.go index 0690ba06b4..75e2b9a011 100644 --- a/interfaces/kmod/backend.go +++ b/interfaces/kmod/backend.go @@ -67,9 +67,56 @@ func (b *Backend) Name() interfaces.SecuritySystem { return "kmod" } -// Setup creates a conf file with list of kernel modules required by given snap, -// writes it in /etc/modules-load.d/ directory and immediately loads the modules -// using /sbin/modprobe. The devMode is ignored. +// setupModules creates a conf file with list of kernel modules required by +// given snap, writes it in /etc/modules-load.d/ directory and immediately +// loads the modules using /sbin/modprobe. The devMode is ignored. +func (b *Backend) setupModules(snapInfo *snap.Info, spec *Specification) error { + content, modules := deriveContent(spec, snapInfo) + // synchronize the content with the filesystem + glob := interfaces.SecurityTagGlob(snapInfo.InstanceName()) + dir := dirs.SnapKModModulesDir + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("cannot create directory for kmod files %q: %s", dir, err) + } + + changed, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, content) + if err != nil { + return err + } + + if len(changed) > 0 { + b.loadModules(modules) + } + return nil +} + +// setupModprobe creates a configuration file under /etc/modprobe.d/ according +// to the specification: this allows to either specify the load parameters for +// a module, or prevent it from being loaded. +// TODO: consider whether +// - a newly blocklisted module should get unloaded +// - a module whose option change should get reloaded +func (b *Backend) setupModprobe(snapInfo *snap.Info, spec *Specification) error { + dir := dirs.SnapKModModprobeDir + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("cannot create directory for kmod files %q: %s", dir, err) + } + + glob := interfaces.SecurityTagGlob(snapInfo.InstanceName()) + dirContents := prepareModprobeDirContents(spec, snapInfo) + _, _, err := osutil.EnsureDirState(dirs.SnapKModModprobeDir, glob, dirContents) + if err != nil { + return err + } + + return nil +} + +// Setup will make the kmod backend generate the needed system files (such as +// those under /etc/modules-load.d/ and /etc/modprobe.d/) and call the +// appropriate system commands so that the desired kernel module configuration +// will be applied. +// The devMode is ignored. // // If the method fails it should be re-tried (with a sensible strategy) by the caller. func (b *Backend) Setup(snapInfo *snap.Info, confinement interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) error { @@ -80,22 +127,16 @@ func (b *Backend) Setup(snapInfo *snap.Info, confinement interfaces.ConfinementO return fmt.Errorf("cannot obtain kmod specification for snap %q: %s", snapName, err) } - content, modules := deriveContent(spec.(*Specification), snapInfo) - // synchronize the content with the filesystem - glob := interfaces.SecurityTagGlob(snapName) - dir := dirs.SnapKModModulesDir - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("cannot create directory for kmod files %q: %s", dir, err) + err = b.setupModprobe(snapInfo, spec.(*Specification)) + if err != nil { + return err } - changed, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, content) + err = b.setupModules(snapInfo, spec.(*Specification)) if err != nil { return err } - if len(changed) > 0 { - b.loadModules(modules) - } return nil } @@ -106,8 +147,20 @@ func (b *Backend) Setup(snapInfo *snap.Info, confinement interfaces.ConfinementO // If the method fails it should be re-tried (with a sensible strategy) by the caller. func (b *Backend) Remove(snapName string) error { glob := interfaces.SecurityTagGlob(snapName) - _, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, nil) - return err + var errors []error + if _, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, nil); err != nil { + errors = append(errors, err) + } + + if _, _, err := osutil.EnsureDirState(dirs.SnapKModModprobeDir, glob, nil); err != nil { + errors = append(errors, err) + } + + if len(errors) > 0 { + return fmt.Errorf("cannot remove kernel modules config files: %v", errors) + } + + return nil } func deriveContent(spec *Specification, snapInfo *snap.Info) (map[string]osutil.FileState, []string) { @@ -134,6 +187,31 @@ func deriveContent(spec *Specification, snapInfo *snap.Info) (map[string]osutil. return content, modules } +func prepareModprobeDirContents(spec *Specification, snapInfo *snap.Info) map[string]osutil.FileState { + disallowedModules := spec.DisallowedModules() + if len(disallowedModules) == 0 && len(spec.moduleOptions) == 0 { + return nil + } + + contents := "# Generated by snapd. Do not edit\n\n" + // First, write down the list of disallowed modules + for _, module := range disallowedModules { + contents += fmt.Sprintf("blacklist %s\n", module) + } + // Then, write down the module options + for module, options := range spec.moduleOptions { + contents += fmt.Sprintf("options %s %s\n", module, options) + } + + fileName := fmt.Sprintf("%s.conf", snap.SecurityTag(snapInfo.InstanceName())) + return map[string]osutil.FileState{ + fileName: &osutil.MemoryFileState{ + Content: []byte(contents), + Mode: 0644, + }, + } +} + func (b *Backend) NewSpecification() interfaces.Specification { return &Specification{} } diff --git a/interfaces/kmod/backend_test.go b/interfaces/kmod/backend_test.go index 92d6dfed70..ab8b5b3f27 100644 --- a/interfaces/kmod/backend_test.go +++ b/interfaces/kmod/backend_test.go @@ -118,6 +118,63 @@ func (s *backendSuite) TestRemovingSnapRemovesModulesConf(c *C) { } } +func (s *backendSuite) TestInstallingSnapCreatesModprobeConf(c *C) { + s.Iface.KModPermanentSlotCallback = func(spec *kmod.Specification, slot *snap.SlotInfo) error { + spec.AddModule("module1") + spec.SetModuleOptions("module1", "opt1=true opt2=2") + spec.DisallowModule("module2") + return nil + } + + modulesPath := filepath.Join(dirs.SnapKModModulesDir, "snap.samba.conf") + c.Assert(osutil.FileExists(modulesPath), Equals, false) + modprobePath := filepath.Join(dirs.SnapKModModprobeDir, "snap.samba.conf") + c.Assert(osutil.FileExists(modprobePath), Equals, false) + + for _, opts := range testedConfinementOpts { + s.modprobeCmd.ForgetCalls() + snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) + + c.Assert(osutil.FileExists(modulesPath), Equals, true) + c.Assert(modulesPath, testutil.FileEquals, "# This file is automatically generated.\nmodule1\n") + + c.Assert(osutil.FileExists(modprobePath), Equals, true) + c.Assert(modprobePath, testutil.FileEquals, `# Generated by snapd. Do not edit + +blacklist module2 +options module1 opt1=true opt2=2 +`) + + c.Assert(s.modprobeCmd.Calls(), DeepEquals, [][]string{ + {"modprobe", "--syslog", "module1"}, + }) + s.RemoveSnap(c, snapInfo) + } +} + +func (s *backendSuite) TestRemovingSnapRemovesModprobeConf(c *C) { + s.Iface.KModPermanentSlotCallback = func(spec *kmod.Specification, slot *snap.SlotInfo) error { + spec.AddModule("module1") + spec.SetModuleOptions("module1", "opt1=true opt2=2") + spec.DisallowModule("module2") + return nil + } + + modulesPath := filepath.Join(dirs.SnapKModModulesDir, "snap.samba.conf") + c.Assert(osutil.FileExists(modulesPath), Equals, false) + modprobePath := filepath.Join(dirs.SnapKModModprobeDir, "snap.samba.conf") + c.Assert(osutil.FileExists(modprobePath), Equals, false) + + for _, opts := range testedConfinementOpts { + snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) + c.Assert(osutil.FileExists(modulesPath), Equals, true) + c.Assert(osutil.FileExists(modprobePath), Equals, true) + s.RemoveSnap(c, snapInfo) + c.Assert(osutil.FileExists(modulesPath), Equals, false) + c.Assert(osutil.FileExists(modprobePath), Equals, false) + } +} + func (s *backendSuite) TestSecurityIsStable(c *C) { // NOTE: Hand out a permanent snippet so that .conf file is generated. s.Iface.KModPermanentSlotCallback = func(spec *kmod.Specification, slot *snap.SlotInfo) error { diff --git a/interfaces/kmod/spec.go b/interfaces/kmod/spec.go index 8e6db0d7e4..d2851b1c5b 100644 --- a/interfaces/kmod/spec.go +++ b/interfaces/kmod/spec.go @@ -20,6 +20,7 @@ package kmod import ( + "sort" "strings" "github.com/snapcore/snapd/interfaces" @@ -33,6 +34,9 @@ import ( // setup process. type Specification struct { modules map[string]bool + + moduleOptions map[string]string + disallowedModules map[string]bool } // AddModule adds a kernel module, trimming spaces and ignoring duplicated modules. @@ -57,6 +61,45 @@ func (spec *Specification) Modules() map[string]bool { return result } +// SetModuleOptions specifies which options to use when loading the given kernel module. +func (spec *Specification) SetModuleOptions(module, options string) error { + if spec.moduleOptions == nil { + spec.moduleOptions = make(map[string]string) + } + spec.moduleOptions[module] = options + return nil +} + +// moduleOptions returns the load options for each kernel module +func (spec *Specification) ModuleOptions() map[string]string { + return spec.moduleOptions +} + +// DisallowModule adds a kernel module to the list of disallowed modules. +func (spec *Specification) DisallowModule(module string) error { + m := strings.TrimSpace(module) + if m == "" { + return nil + } + if spec.disallowedModules == nil { + spec.disallowedModules = make(map[string]bool) + } + spec.disallowedModules[m] = true + return nil +} + +// DisallowedModules returns the list of disallowed modules. +func (spec *Specification) DisallowedModules() []string { + result := make([]string, 0, len(spec.disallowedModules)) + for k, v := range spec.disallowedModules { + if v { + result = append(result, k) + } + } + sort.Strings(result) + return result +} + // Implementation of methods required by interfaces.Specification // AddConnectedPlug records kmod-specific side-effects of having a connected plug. diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go index e4a8ea6379..c3a33c873c 100644 --- a/interfaces/policy/basedeclaration_test.go +++ b/interfaces/policy/basedeclaration_test.go @@ -627,6 +627,7 @@ var ( "hidraw": {"core", "gadget"}, "i2c": {"core", "gadget"}, "iio": {"core", "gadget"}, + "kernel-module-load": {"core"}, "kubernetes-support": {"core"}, "location-control": {"app"}, "location-observe": {"app"}, @@ -636,6 +637,7 @@ var ( "mir": {"app"}, "microstack-support": {"core"}, "modem-manager": {"app", "core"}, + "mount-control": {"core"}, "mpris": {"app"}, "netlink-driver": {"core", "gadget"}, "network-manager": {"app", "core"}, @@ -669,6 +671,7 @@ var ( "classic-support": nil, "docker": nil, "lxd": nil, + "shared-memory": nil, } restrictedPlugInstallation = map[string][]string{ @@ -735,9 +738,11 @@ func (s *baseDeclSuite) TestPlugInstallation(c *C) { "gpio-control": true, "ion-memory-control": true, "kernel-module-control": true, + "kernel-module-load": true, "kubernetes-support": true, "lxd-support": true, "microstack-support": true, + "mount-control": true, "multipass-support": true, "packagekit-control": true, "personal-files": true, @@ -798,6 +803,7 @@ func (s *baseDeclSuite) TestConnection(c *C) { "mir": true, "online-accounts-service": true, "raw-volume": true, + "shared-memory": true, "storage-framework-service": true, "thumbnailer-service": true, "ubuntu-download-manager": true, @@ -975,13 +981,16 @@ func (s *baseDeclSuite) TestSanity(c *C) { "gpio-control": true, "ion-memory-control": true, "kernel-module-control": true, + "kernel-module-load": true, "kubernetes-support": true, "lxd-support": true, "microstack-support": true, + "mount-control": true, "multipass-support": true, "packagekit-control": true, "personal-files": true, "sd-control": true, + "shared-memory": true, "snap-refresh-control": true, "snap-themes-control": true, "snapd-control": true, diff --git a/interfaces/utils/export_test.go b/interfaces/utils/export_test.go new file mode 100644 index 0000000000..9f2ee6076c --- /dev/null +++ b/interfaces/utils/export_test.go @@ -0,0 +1,26 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package utils + +var ( + CreateRegex = createRegex + GlobDefault = globDefault + GlobNull = globNull +) diff --git a/interfaces/utils/path_patterns.go b/interfaces/utils/path_patterns.go new file mode 100644 index 0000000000..098e6f2954 --- /dev/null +++ b/interfaces/utils/path_patterns.go @@ -0,0 +1,177 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package utils + +import ( + "fmt" + "regexp" +) + +type PathPattern struct { + pattern string + regex *regexp.Regexp +} + +const maxGroupDepth = 50 + +type GlobFlags int + +const ( + globDefault GlobFlags = 1 << iota + globNull +) + +// createRegex converts the apparmor-like glob sequence into a regex. Loosely +// using this as reference: +// https://gitlab.com/apparmor/apparmor/-/blob/master/parser/parser_regex.c#L107 +func createRegex(pattern string, glob GlobFlags) (string, error) { + regex := "^" + + appendGlob := func(defaultGlob, nullGlob string) { + var pattern string + switch glob { + case globDefault: + pattern = defaultGlob + case globNull: + pattern = nullGlob + } + regex += pattern + } + + const ( + noSlashOrNull = `[^/\x00]` + noSlash = `[^/]` + ) + + escapeNext := false + currentGroupLevel := 0 + inCharClass := false + skipNext := false + itemCountInGroup := new([maxGroupDepth + 1]int) + for i, ch := range pattern { + if escapeNext { + regex += regexp.QuoteMeta(string(ch)) + escapeNext = false + continue + } + if skipNext { + skipNext = false + continue + } + if inCharClass && ch != '\\' && ch != ']' { + // no characters are special other than '\' and ']' + regex += string(ch) + continue + } + switch ch { + case '\\': + escapeNext = true + case '*': + if regex[len(regex)-1] == '/' { + // if the * is at the end of the pattern or is followed by a + // '/' we don't want it to match an empty string: + // /foo/* -> should not match /foo/ + // /foo/*bar -> should match /foo/bar + // /*/foo -> should not match //foo + pos := i + 1 + for len(pattern) > pos && pattern[pos] == '*' { + pos++ + } + if len(pattern) <= pos || pattern[pos] == '/' { + appendGlob(noSlashOrNull, noSlash) + } + } + + if len(pattern) > i+1 && pattern[i+1] == '*' { + // Handle ** + appendGlob("[^\\x00]*", ".*") + skipNext = true + } else { + appendGlob(noSlashOrNull+"*", noSlash+"*") + } + case '?': + appendGlob(noSlashOrNull, noSlash) + case '[': + inCharClass = true + regex += string(ch) + case ']': + if !inCharClass { + return "", fmt.Errorf("pattern contains unmatching ']': %q", pattern) + } + inCharClass = false + regex += string(ch) + case '{': + currentGroupLevel++ + if currentGroupLevel > maxGroupDepth { + return "", fmt.Errorf("maximum group depth exceeded: %q", pattern) + } + itemCountInGroup[currentGroupLevel] = 0 + regex += "(" + case '}': + if currentGroupLevel <= 0 { + return "", fmt.Errorf("invalid closing brace, no matching open { found: %q", pattern) + } + if itemCountInGroup[currentGroupLevel] == 0 { + return "", fmt.Errorf("invalid number of items between {}: %q", pattern) + } + currentGroupLevel-- + regex += ")" + case ',': + if currentGroupLevel > 0 { + itemCountInGroup[currentGroupLevel]++ + regex += "|" + } else { + return "", fmt.Errorf("cannot use ',' outside of group or character class") + } + default: + // take literal character (with quoting if needed) + regex += regexp.QuoteMeta(string(ch)) + } + } + + if currentGroupLevel > 0 { + return "", fmt.Errorf("missing %d closing brace(s): %q", currentGroupLevel, pattern) + } + if inCharClass { + return "", fmt.Errorf("missing closing bracket ']': %q", pattern) + } + if escapeNext { + return "", fmt.Errorf("expected character after '\\': %q", pattern) + } + + regex += "$" + return regex, nil +} + +func NewPathPattern(pattern string) (*PathPattern, error) { + regexPattern, err := createRegex(pattern, globDefault) + if err != nil { + return nil, err + } + + regex := regexp.MustCompile(regexPattern) + + pp := &PathPattern{pattern, regex} + return pp, nil +} + +func (pp *PathPattern) Matches(path string) bool { + return pp.regex.MatchString(path) +} diff --git a/interfaces/utils/path_patterns_test.go b/interfaces/utils/path_patterns_test.go new file mode 100644 index 0000000000..c453dd88a1 --- /dev/null +++ b/interfaces/utils/path_patterns_test.go @@ -0,0 +1,133 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package utils_test + +import ( + "regexp" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces/utils" +) + +type pathPatternsSuite struct{} + +var _ = Suite(&pathPatternsSuite{}) + +func (s *pathPatternsSuite) TestRegexCreationHappy(c *C) { + // to save some typing: + d := utils.GlobDefault + n := utils.GlobNull + + data := []struct { + pattern string + glob utils.GlobFlags + expectedRegex string + }{ + {`/media/user/`, d, `^/media/user/$`}, + {`/dev/sd*`, d, `^/dev/sd[^/\x00]*$`}, + {`/dev/sd*`, n, `^/dev/sd[^/]*$`}, + {`/dev/sd?`, d, `^/dev/sd[^/\x00]$`}, + {`/dev/sd?`, n, `^/dev/sd[^/]$`}, + {`/etc/**`, d, `^/etc/[^/\x00][^\x00]*$`}, + {`/home/*/.bashrc`, d, `^/home/[^/\x00][^/\x00]*/\.bashrc$`}, + {`/home/*/.bashrc`, n, `^/home/[^/][^/]*/\.bashrc$`}, + {`/media/{user,loser}/`, d, `^/media/(user|loser)/$`}, + {`/nested/{a,b{c,d}}/`, d, `^/nested/(a|b(c|d))/$`}, + {`/media/\{in-braces\}/`, d, `^/media/\{in-braces\}/$`}, + {`/media/\[in-brackets\]/`, d, `^/media/\[in-brackets\]/$`}, + {`/dev/sd[abc][0-9]`, d, `^/dev/sd[abc][0-9]$`}, + {`/quoted/bracket/[ab\]c]`, d, `^/quoted/bracket/[ab\]c]$`}, + {`{[,],}`, d, `^([,]|)$`}, + {`/path/with/comma[,]`, d, `^/path/with/comma[,]$`}, + {`/$pecial/c^aracters`, d, `^/\$pecial/c\^aracters$`}, + {`/in/char/class[^$]`, d, `^/in/char/class[^$]$`}, + } + + for _, testData := range data { + pattern := testData.pattern + expectedRegex := testData.expectedRegex + regex, err := utils.CreateRegex(pattern, testData.glob) + c.Assert(err, IsNil, Commentf("%s", pattern)) + c.Assert(regex, Equals, expectedRegex, Commentf("%s", pattern)) + // Also, make sure that the obtained regex is valid + _, err = regexp.Compile(regex) + c.Assert(err, IsNil, Commentf("%s", pattern)) + } +} + +func (s *pathPatternsSuite) TestRegexCreationUnhappy(c *C) { + data := []struct { + pattern string + expectedError string + }{ + {`/media/{}/`, `invalid number of items between {}:.*`}, + {`/media/{some/things`, `missing 1 closing brace\(s\):.*`}, + {`/media/}`, `invalid closing brace, no matching open { found:.*`}, + {`/media/[abc`, `missing closing bracket ']':.*`}, + {`/media/]`, `pattern contains unmatching ']':.*`}, + {`/media\`, `expected character after '\\':.*`}, + // 123456789012345678901234567890123456789012345678901, 51 of them + {`/{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{`, `maximum group depth exceeded:.*`}, + {`/comma/not/in/group/a,b`, `cannot use ',' outside of group or character class`}, + } + + for _, testData := range data { + pattern := testData.pattern + expectedError := testData.expectedError + pathPattern, err := utils.NewPathPattern(pattern) + c.Assert(pathPattern, IsNil, Commentf("%s", pattern)) + c.Assert(err, ErrorMatches, expectedError, Commentf("%s", pattern)) + } +} + +func (s *pathPatternsSuite) TestPatternMatches(c *C) { + data := []struct { + pattern string + testPath string + expectedMatch bool + }{ + {`/same/path/`, `/same/path/`, true}, + {`/path/*`, `/path/here`, true}, + {`/path/*`, `/path/too/deep`, false}, + {`/path/**`, `/path/here`, true}, + {`/path/**`, `/path/here/too`, true}, + {`/dev/sd?`, `/dev/sda`, true}, + {`/dev/sd?`, `/dev/sdb1`, false}, + {`/media/{user,loser}/`, `/media/user/`, true}, + {`/media/{user,loser}/`, `/media/other/`, false}, + {`/nested/{a,b{c,d}}/`, `/nested/a/`, true}, + {`/nested/{a,b{c,d}}/`, `/nested/bd/`, true}, + {`/nested/{a,b{c,d}}/`, `/nested/ad/`, false}, + {`/dev/sd[abc][0-9]`, `/dev/sda0`, true}, + {`/dev/sd[abc][0-9]`, `/dev/sdb4`, true}, + {`/dev/sd[abc][0-9]`, `/dev/sda10`, false}, + {`/dev/sd[abc][0-9]`, `/dev/sdd0`, false}, + } + + for _, testData := range data { + pattern := testData.pattern + testPath := testData.testPath + expectedMatch := testData.expectedMatch + pathPattern, err := utils.NewPathPattern(pattern) + c.Assert(err, IsNil, Commentf("%s", pattern)) + c.Assert(pathPattern.Matches(testPath), Equals, expectedMatch, Commentf("%s", pattern)) + } +} diff --git a/mkversion.sh b/mkversion.sh index 2fe1edf51a..7d6d108d21 100755 --- a/mkversion.sh +++ b/mkversion.sh @@ -130,4 +130,5 @@ EOF cat <<EOF > "$PKG_BUILDDIR/data/info" VERSION=$v +SNAPD_APPARMOR_REEXEC=0 EOF diff --git a/osutil/chattr_32.go b/osutil/chattr_32.go index 92e509b8fb..96b9ee2560 100644 --- a/osutil/chattr_32.go +++ b/osutil/chattr_32.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build arm || 386 || ppc // +build arm 386 ppc /* diff --git a/osutil/chattr_64.go b/osutil/chattr_64.go index acd7c2738f..e9f5a8a675 100644 --- a/osutil/chattr_64.go +++ b/osutil/chattr_64.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build amd64 || arm64 || ppc64le || riscv64 || s390x // +build amd64 arm64 ppc64le riscv64 s390x /* diff --git a/osutil/cp_other.go b/osutil/cp_other.go index 5cc206ba2c..b36d08de44 100644 --- a/osutil/cp_other.go +++ b/osutil/cp_other.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !linux // +build !linux /* diff --git a/osutil/export_fault_test.go b/osutil/export_fault_test.go index 6097f244d9..fd12be7759 100644 --- a/osutil/export_fault_test.go +++ b/osutil/export_fault_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build faultinject // +build faultinject /* diff --git a/osutil/faultinject.go b/osutil/faultinject.go index 05305865be..691e1d48e5 100644 --- a/osutil/faultinject.go +++ b/osutil/faultinject.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build faultinject // +build faultinject /* diff --git a/osutil/faultinject_dummy.go b/osutil/faultinject_dummy.go index 674915edf7..c68e597215 100644 --- a/osutil/faultinject_dummy.go +++ b/osutil/faultinject_dummy.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !faultinject // +build !faultinject /* diff --git a/osutil/faultinject_dummy_test.go b/osutil/faultinject_dummy_test.go index 310e882e67..6c5bc8121f 100644 --- a/osutil/faultinject_dummy_test.go +++ b/osutil/faultinject_dummy_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !faultinject // +build !faultinject /* diff --git a/osutil/faultinject_test.go b/osutil/faultinject_test.go index deff115e5c..ff299a936b 100644 --- a/osutil/faultinject_test.go +++ b/osutil/faultinject_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build faultinject // +build faultinject /* diff --git a/osutil/group_cgo.go b/osutil/group_cgo.go index 8a70e1b901..1d00b454c5 100644 --- a/osutil/group_cgo.go +++ b/osutil/group_cgo.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build cgo // +build cgo /* diff --git a/osutil/group_no_cgo.go b/osutil/group_no_cgo.go index 752cae6561..8d18cf2acd 100644 --- a/osutil/group_no_cgo.go +++ b/osutil/group_no_cgo.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !cgo // +build !cgo package osutil diff --git a/osutil/settime_32bit.go b/osutil/settime_32bit.go index 983556a955..42ee0cf6e9 100644 --- a/osutil/settime_32bit.go +++ b/osutil/settime_32bit.go @@ -1,5 +1,6 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build (386 || arm) && linux // +build 386 arm // +build linux diff --git a/osutil/settime_64bit.go b/osutil/settime_64bit.go index d0587271b0..665b785b22 100644 --- a/osutil/settime_64bit.go +++ b/osutil/settime_64bit.go @@ -1,8 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- -// +build !386 -// +build !arm -// +build linux +//go:build !386 && !arm && linux +// +build !386,!arm,linux /* * Copyright (C) 2021 Canonical Ltd diff --git a/osutil/sys/sysnum_16_linux.go b/osutil/sys/sysnum_16_linux.go index fe3783cdca..6d01a57abf 100644 --- a/osutil/sys/sysnum_16_linux.go +++ b/osutil/sys/sysnum_16_linux.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build arm || 386 // +build arm 386 /* diff --git a/osutil/sys/sysnum_32_linux.go b/osutil/sys/sysnum_32_linux.go index 0449aa5129..36f67fc48a 100644 --- a/osutil/sys/sysnum_32_linux.go +++ b/osutil/sys/sysnum_32_linux.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build amd64 || arm64 || ppc || ppc64le || riscv64 || s390x // +build amd64 arm64 ppc ppc64le riscv64 s390x /* diff --git a/osutil/udev/netlink/rawsockstop_other.go b/osutil/udev/netlink/rawsockstop_other.go index 9eaacc6339..8d9ce41579 100644 --- a/osutil/udev/netlink/rawsockstop_other.go +++ b/osutil/udev/netlink/rawsockstop_other.go @@ -1,4 +1,6 @@ +//go:build !arm64 // +build !arm64 + // don't remove the newline between the above statement and the package statement // or else the build constraint will be ignored and assumed to be part of the package comment! diff --git a/overlord/assertstate/assertstate.go b/overlord/assertstate/assertstate.go index bfd85cf612..d0b63a0687 100644 --- a/overlord/assertstate/assertstate.go +++ b/overlord/assertstate/assertstate.go @@ -344,6 +344,10 @@ func delayedCrossMgrInit() { snapstate.AutoAliases = AutoAliases // hook the helper for getting enforced validation sets snapstate.EnforcedValidationSets = EnforcedValidationSets + // hook the helper for saving current validation sets to the stack + snapstate.AddCurrentTrackingToValidationSetsStack = addCurrentTrackingToValidationSetsHistory + // hook the helper for restoring validation sets tracking from the stack + snapstate.RestoreValidationSetsTracking = RestoreValidationSetsTracking } // AutoRefreshAssertions tries to refresh all assertions diff --git a/overlord/assertstate/validation_set_tracking.go b/overlord/assertstate/validation_set_tracking.go index 5a85d26b45..c037158960 100644 --- a/overlord/assertstate/validation_set_tracking.go +++ b/overlord/assertstate/validation_set_tracking.go @@ -261,3 +261,19 @@ func ValidationSetsHistory(st *state.State) ([]map[string]*ValidationSetTracking } return vshist, nil } + +// RestoreValidationSetsTracking restores validation-sets state to the last state +// stored in the validation-sets-stack. It should only be called when the stack +// is not empty, otherwise an error is returned. +func RestoreValidationSetsTracking(st *state.State) error { + trackingState, err := validationSetsHistoryTop(st) + if err != nil { + return err + } + if len(trackingState) == 0 { + // we should never be called when there is nothing in the stack + return state.ErrNoState + } + st.Set("validation-sets", trackingState) + return nil +} diff --git a/overlord/assertstate/validation_set_tracking_test.go b/overlord/assertstate/validation_set_tracking_test.go index 718ed79a13..11facaa4d4 100644 --- a/overlord/assertstate/validation_set_tracking_test.go +++ b/overlord/assertstate/validation_set_tracking_test.go @@ -409,3 +409,56 @@ func (s *validationSetTrackingSuite) TestAddToValidationSetsHistoryRemovesOldEnt }, }) } + +func (s *validationSetTrackingSuite) TestRestoreValidationSetsTrackingNoHistory(c *C) { + s.st.Lock() + defer s.st.Unlock() + + c.Assert(assertstate.RestoreValidationSetsTracking(s.st), Equals, state.ErrNoState) +} + +func (s *validationSetTrackingSuite) TestRestoreValidationSetsTracking(c *C) { + s.st.Lock() + defer s.st.Unlock() + + tr1 := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 1, + Current: 2, + } + assertstate.UpdateValidationSet(s.st, &tr1) + + c.Assert(assertstate.AddCurrentTrackingToValidationSetsHistory(s.st), IsNil) + + all, err := assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 1) + + tr2 := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "baz", + Mode: assertstate.Enforce, + Current: 5, + } + assertstate.UpdateValidationSet(s.st, &tr2) + + all, err = assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + // two validation sets are now tracked + c.Check(all, DeepEquals, map[string]*assertstate.ValidationSetTracking{ + "foo/bar": &tr1, + "foo/baz": &tr2, + }) + + // restore + c.Assert(assertstate.RestoreValidationSetsTracking(s.st), IsNil) + + // and we're back at one validation set being tracked + all, err = assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Check(all, DeepEquals, map[string]*assertstate.ValidationSetTracking{ + "foo/bar": &tr1, + }) +} diff --git a/overlord/configstate/configcore/certs.go b/overlord/configstate/configcore/certs.go index 6e482bbd5c..f45727aafa 100644 --- a/overlord/configstate/configcore/certs.go +++ b/overlord/configstate/configcore/certs.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff --git a/overlord/configstate/configcore/cloud.go b/overlord/configstate/configcore/cloud.go index a0cc2a901e..9bf050ef30 100644 --- a/overlord/configstate/configcore/cloud.go +++ b/overlord/configstate/configcore/cloud.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff --git a/overlord/configstate/configcore/handlers.go b/overlord/configstate/configcore/handlers.go index b773fd125b..16ac8b28bd 100644 --- a/overlord/configstate/configcore/handlers.go +++ b/overlord/configstate/configcore/handlers.go @@ -101,6 +101,9 @@ func init() { // when applying so there is no validation handler, see LP:1952740 addFSOnlyHandler(nil, handleHostnameConfiguration, coreOnly) + // tmpfs.size + addFSOnlyHandler(validateTmpfsSettings, handleTmpfsConfiguration, coreOnly) + sysconfig.ApplyFilesystemOnlyDefaultsImpl = filesystemOnlyApply } diff --git a/overlord/configstate/configcore/proxy.go b/overlord/configstate/configcore/proxy.go index f6724c12e7..9c199f1fb9 100644 --- a/overlord/configstate/configcore/proxy.go +++ b/overlord/configstate/configcore/proxy.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff --git a/overlord/configstate/configcore/refresh.go b/overlord/configstate/configcore/refresh.go index acf465f6d7..7b1bd8e878 100644 --- a/overlord/configstate/configcore/refresh.go +++ b/overlord/configstate/configcore/refresh.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff --git a/overlord/configstate/configcore/runwithstate.go b/overlord/configstate/configcore/runwithstate.go index 7246fd5550..7dee3f6950 100644 --- a/overlord/configstate/configcore/runwithstate.go +++ b/overlord/configstate/configcore/runwithstate.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff --git a/overlord/configstate/configcore/snapshots.go b/overlord/configstate/configcore/snapshots.go index 80b0a824d1..5e88f26287 100644 --- a/overlord/configstate/configcore/snapshots.go +++ b/overlord/configstate/configcore/snapshots.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff --git a/overlord/configstate/configcore/tmp.go b/overlord/configstate/configcore/tmp.go new file mode 100644 index 0000000000..cc66fa797f --- /dev/null +++ b/overlord/configstate/configcore/tmp.go @@ -0,0 +1,145 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package configcore + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/sysconfig" +) + +const ( + mntStaticOptions = "mode=1777,strictatime,nosuid,nodev" + tmpfsMountPoint = "/tmp" + tmpMntServOverrideSubDir = "tmp.mount.d" + tmpMntServOverrideFile = "override.conf" +) + +func init() { + // add supported configuration of this module + supportedConfigurations["core.tmp.size"] = true +} + +func validTmpfsSize(sizeStr string) error { + if sizeStr == "" { + return nil + } + + // TODO allow also percentages. That is allowed for CPU quotas so + // it is probably fine to allow that for tmp.size too. + size, err := quantity.ParseSize(sizeStr) + if err != nil { + return err + } + + // Do not allow less than 16mb + // 0 is special and means unlimited + if size > 0 && size < 16*quantity.SizeMiB { + return fmt.Errorf("size is less than 16Mb") + } + + return nil +} + +func validateTmpfsSettings(tr config.ConfGetter) error { + tmpfsSz, err := coreCfg(tr, "tmp.size") + if err != nil { + return err + } + + return validTmpfsSize(tmpfsSz) +} + +func handleTmpfsConfiguration(_ sysconfig.Device, tr config.ConfGetter, opts *fsOnlyContext) error { + tmpfsSz, err := coreCfg(tr, "tmp.size") + if err != nil { + return err + } + + // Create override configuration file for tmp.mount service + + // Create /etc/systemd/system/tmp.mount.d if needed + var overrDir string + if opts == nil { + // runtime system + overrDir = dirs.SnapServicesDir + } else { + overrDir = dirs.SnapServicesDirUnder(opts.RootDir) + } + overrDir = filepath.Join(overrDir, tmpMntServOverrideSubDir) + + // Write service config override if needed + options := mntStaticOptions + dirContent := make(map[string]osutil.FileState, 1) + cfgFilePath := filepath.Join(overrDir, tmpMntServOverrideFile) + modify := true + if tmpfsSz != "" { + if err := os.MkdirAll(overrDir, 0755); err != nil { + return err + } + options = fmt.Sprintf("%s,size=%s", options, tmpfsSz) + content := fmt.Sprintf("[Mount]\nOptions=%s\n", options) + dirContent[tmpMntServOverrideFile] = &osutil.MemoryFileState{ + Content: []byte(content), + Mode: 0644, + } + oldContent, err := ioutil.ReadFile(cfgFilePath) + if err == nil && content == string(oldContent) { + modify = false + } + } else { + // Use default tmpfs size if empty setting (50%, see tmpfs(5)) + options = fmt.Sprintf("%s,size=50%%", options) + // In this case, we are removing the file, so we will + // not do anything if the file is not there alreay. + if _, err := os.Stat(cfgFilePath); errors.Is(err, os.ErrNotExist) { + modify = false + } + } + + // Re-starting the tmp.mount service will fail if some process + // is using a file in /tmp, so instead of doing that we use + // the remount option for the mount command, which will not + // fail in that case. There is however the possibility of a + // failure in case we are reducing the size to something + // smaller than the currently used space in the mount. We + // return an error in that case. + if opts == nil && modify { + if output, err := exec.Command("mount", "-o", "remount,"+options, tmpfsMountPoint).CombinedOutput(); err != nil { + return fmt.Errorf("cannot remount tmpfs with new size: %s (%s)", err.Error(), output) + } + } + + glob := tmpMntServOverrideFile + if _, _, err = osutil.EnsureDirState(overrDir, glob, dirContent); err != nil { + return err + } + + return nil +} diff --git a/overlord/configstate/configcore/tmp_test.go b/overlord/configstate/configcore/tmp_test.go new file mode 100644 index 0000000000..fba5711a0d --- /dev/null +++ b/overlord/configstate/configcore/tmp_test.go @@ -0,0 +1,224 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package configcore_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/configstate/configcore" + "github.com/snapcore/snapd/testutil" +) + +type tmpfsSuite struct { + configcoreSuite + + servOverridePath string + servOverrideDir string +} + +var _ = Suite(&tmpfsSuite{}) + +func (s *tmpfsSuite) SetUpTest(c *C) { + s.configcoreSuite.SetUpTest(c) + + s.servOverrideDir = filepath.Join(dirs.SnapServicesDir, "tmp.mount.d") + s.servOverridePath = filepath.Join(s.servOverrideDir, "override.conf") +} + +// Configure with different valid values +func (s *tmpfsSuite) TestConfigureTmpfsGoodVals(c *C) { + expectedMountCalls := [][]string{} + mountCmd := testutil.MockCommand(c, "mount", "") + defer mountCmd.Restore() + + for _, size := range []string{"104857600", "16M", "7G", "0"} { + + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, IsNil) + + c.Check(s.servOverridePath, testutil.FileEquals, + fmt.Sprintf("[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=%s\n", size)) + mntOpts := fmt.Sprintf("remount,mode=1777,strictatime,nosuid,nodev,size=%s", size) + expectedMountCalls = append(expectedMountCalls, []string{"mount", "-o", mntOpts, "/tmp"}) + } + + c.Check(s.systemctlArgs, HasLen, 0) + c.Check(mountCmd.Calls(), DeepEquals, expectedMountCalls) +} + +// Configure with different invalid values +func (s *tmpfsSuite) TestConfigureTmpfsBadVals(c *C) { + for _, size := range []string{"100p", "0x123", "10485f7600", "20%%", + "20%", "100m", "10k", "10K", "10g"} { + + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, ErrorMatches, `invalid suffix .*`) + + _, err = os.Stat(s.servOverridePath) + c.Assert(os.IsNotExist(err), Equals, true) + } + + c.Assert(s.systemctlArgs, IsNil) +} + +func (s *tmpfsSuite) TestConfigureTmpfsTooSmall(c *C) { + for _, size := range []string{"1", "16777215"} { + + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, ErrorMatches, `size is less than 16Mb`) + + _, err = os.Stat(s.servOverridePath) + c.Assert(os.IsNotExist(err), Equals, true) + } + + c.Assert(s.systemctlArgs, IsNil) +} + +// Ensure things are fine if destination folder already existed +func (s *tmpfsSuite) TestConfigureTmpfsAllConfDirExistsAlready(c *C) { + mountCmd := testutil.MockCommand(c, "mount", "") + defer mountCmd.Restore() + + // make tmp.mount.d directory already + err := os.MkdirAll(s.servOverrideDir, 0755) + c.Assert(err, IsNil) + + size := "100M" + err = configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, IsNil) + c.Check(s.servOverridePath, testutil.FileEquals, + fmt.Sprintf("[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=%s\n", size)) + + c.Check(s.systemctlArgs, HasLen, 0) + c.Check(mountCmd.Calls(), DeepEquals, + [][]string{{"mount", "-o", "remount,mode=1777,strictatime,nosuid,nodev,size=100M", "/tmp"}}) +} + +// Test cfg file is not updated if we set the same size that is already set +func (s *tmpfsSuite) TestConfigureTmpfsNoFileUpdate(c *C) { + err := os.MkdirAll(s.servOverrideDir, 0755) + c.Assert(err, IsNil) + size := "100M" + content := "[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=" + size + "\n" + err = ioutil.WriteFile(s.servOverridePath, []byte(content), 0644) + c.Assert(err, IsNil) + + info, err := os.Stat(s.servOverridePath) + c.Assert(err, IsNil) + + fileModTime := info.ModTime() + + // To make sure the times will differ if the file is newly written + time.Sleep(100 * time.Millisecond) + + err = configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, IsNil) + c.Check(s.servOverridePath, testutil.FileEquals, content) + + info, err = os.Stat(s.servOverridePath) + c.Assert(err, IsNil) + c.Assert(info.ModTime(), Equals, fileModTime) + + c.Check(s.systemctlArgs, HasLen, 0) +} + +// Test that config file is removed when unsetting +func (s *tmpfsSuite) TestConfigureTmpfsRemovesIfUnset(c *C) { + mountCmd := testutil.MockCommand(c, "mount", "") + defer mountCmd.Restore() + + err := os.MkdirAll(s.servOverrideDir, 0755) + c.Assert(err, IsNil) + + // add canary to ensure we don't touch other files + canary := filepath.Join(s.servOverrideDir, "05-canary.conf") + err = ioutil.WriteFile(canary, nil, 0644) + c.Assert(err, IsNil) + + content := "[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=1G\n" + err = ioutil.WriteFile(s.servOverridePath, []byte(content), 0644) + c.Assert(err, IsNil) + + err = configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": "", + }, + }) + c.Assert(err, IsNil) + + // ensure the file got deleted + c.Check(osutil.FileExists(s.servOverridePath), Equals, false) + // but the canary is still here + c.Check(osutil.FileExists(canary), Equals, true) + + // the default was applied + c.Check(s.systemctlArgs, HasLen, 0) + c.Check(mountCmd.Calls(), DeepEquals, + [][]string{{"mount", "-o", "remount,mode=1777,strictatime,nosuid,nodev,size=50%", "/tmp"}}) +} + +// Test applying on image preparation +func (s *tmpfsSuite) TestFilesystemOnlyApply(c *C) { + conf := configcore.PlainCoreConfig(map[string]interface{}{ + "tmp.size": "16777216", + }) + + tmpDir := c.MkDir() + c.Assert(configcore.FilesystemOnlyApply(coreDev, tmpDir, conf), IsNil) + + tmpfsOverrCfg := filepath.Join(tmpDir, + "/etc/systemd/system/tmp.mount.d/override.conf") + c.Check(tmpfsOverrCfg, testutil.FileEquals, + "[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=16777216\n") +} diff --git a/overlord/configstate/configcore/vitality.go b/overlord/configstate/configcore/vitality.go index 63b7f95ee4..733775b0fb 100644 --- a/overlord/configstate/configcore/vitality.go +++ b/overlord/configstate/configcore/vitality.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff --git a/overlord/devicestate/devicemgr.go b/overlord/devicestate/devicemgr.go index de97e10139..453452eba2 100644 --- a/overlord/devicestate/devicemgr.go +++ b/overlord/devicestate/devicemgr.go @@ -1754,7 +1754,12 @@ func (m *DeviceManager) StoreContextBackend() storecontext.Backend { var timeutilIsNTPSynchronized = timeutil.IsNTPSynchronized func (m *DeviceManager) ntpSyncedOrWaitedLongerThan(maxWait time.Duration) bool { - if m.ntpSyncedOrTimedOut || time.Now().After(startTime.Add(maxWait)) { + if m.ntpSyncedOrTimedOut { + return true + } + if time.Now().After(startTime.Add(maxWait)) { + logger.Noticef("no NTP sync after %v, trying auto-refresh anyway", maxWait) + m.ntpSyncedOrTimedOut = true return true } diff --git a/overlord/devicestate/devicestate.go b/overlord/devicestate/devicestate.go index d40b32bcbd..847a3b6c55 100644 --- a/overlord/devicestate/devicestate.go +++ b/overlord/devicestate/devicestate.go @@ -328,14 +328,14 @@ func getAllRequiredSnapsForModel(model *asserts.Model) *naming.SnapSet { return naming.NewSnapSet(reqSnaps) } -var errNoDownloadInstallEdge = fmt.Errorf("download and checks edge not found") +var errNoBeforeLocalModificationsEdge = fmt.Errorf("before-local-modifications edge not found") -// extractDownloadInstallEdgesFromTs extracts the first, last download +// extractBeforeLocalModificationsEdgesTs extracts the first, last download // phase and install phase tasks from a TaskSet -func extractDownloadInstallEdgesFromTs(ts *state.TaskSet) (firstDl, lastDl, firstInst, lastInst *state.Task, err error) { - edgeTask := ts.MaybeEdge(snapstate.DownloadAndChecksDoneEdge) +func extractBeforeLocalModificationsEdgesTs(ts *state.TaskSet) (firstDl, lastDl, firstInst, lastInst *state.Task, err error) { + edgeTask := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) if edgeTask == nil { - return nil, nil, nil, nil, errNoDownloadInstallEdge + return nil, nil, nil, nil, errNoBeforeLocalModificationsEdge } tasks := ts.Tasks() // we know we always start with downloads @@ -479,9 +479,11 @@ func remodelEssentialSnapTasks(ctx context.Context, st *state.State, ms modelSna return nil, err } if ts != nil { - if edgeTask := ts.MaybeEdge(snapstate.DownloadAndChecksDoneEdge); edgeTask != nil { - // we have downloads and checks done edge, so - // the update is not a simple + if edgeTask := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge); edgeTask != nil { + // no task is marked as being last + // before local modifications are + // introduced, indicating that the + // update is a simple // switch-snap-channel return ts, nil } else { @@ -632,7 +634,9 @@ func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Mo // Terminology // A <- B means B waits for A // "download,verify" are part of the "Download" phase - // "link,start" is part of "Install" phase + // "link,start" is part of "Install" phase which introduces + // system modifications. The last task of the "Download" phase + // is marked with LastBeforeLocalModificationsEdge. // // - all tasks inside ts{Download,Install} already wait for // each other so the chains look something like this: @@ -649,15 +653,16 @@ func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Mo // verify2 <- download3 (added) // install1 <- install2 (added) // install2 <- install3 (added) - downloadStart, downloadLast, installFirst, installLast, err := extractDownloadInstallEdgesFromTs(ts) + downloadStart, downloadLast, installFirst, installLast, err := extractBeforeLocalModificationsEdgesTs(ts) if err != nil { - if err == errNoDownloadInstallEdge { + if err == errNoBeforeLocalModificationsEdge { // there is no task in the task set marked with - // download edges, which can happen when there - // is a simple channel switch if the snap which - // is part of remodel has the same revision in - // the current channel and one that will be used - // after remodel + // as being last before system modification + // edge, which can happen when there is a simple + // channel switch if the snap which is part of + // remodel has the same revision in the current + // channel and one that will be used after + // remodel continue } return nil, fmt.Errorf("cannot remodel: %v", err) diff --git a/overlord/devicestate/devicestate_remodel_test.go b/overlord/devicestate/devicestate_remodel_test.go index 0f029356e9..094e4aebec 100644 --- a/overlord/devicestate/devicestate_remodel_test.go +++ b/overlord/devicestate/devicestate_remodel_test.go @@ -264,7 +264,7 @@ func (s *deviceMgrRemodelSuite) testRemodelTasksSwitchTrack(c *C, whatRefreshes tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -283,7 +283,7 @@ func (s *deviceMgrRemodelSuite) testRemodelTasksSwitchTrack(c *C, whatRefreshes tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -359,7 +359,7 @@ func (s *deviceMgrRemodelSuite) testRemodelSwitchTasks(c *C, whatsNew, whatNewTr tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -417,7 +417,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelRequiredSnaps(c *C) { tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -529,7 +529,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelSwitchKernelTrack(c *C) { tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -546,7 +546,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelSwitchKernelTrack(c *C) { tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -694,7 +694,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelStoreSwitch(c *C) { tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -838,7 +838,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelClash(c *C) { tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -909,7 +909,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelClashInProgress(c *C) { tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1408,7 +1408,7 @@ volumes: }) tGadgetUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1558,7 +1558,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsParanoidCheck(c *C) { }) tGadgetUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1604,7 +1604,7 @@ func (s *deviceMgrSuite) TestRemodelSwitchBase(c *C) { tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1658,7 +1658,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20RequiredSnapsAndRecoverySystem(c tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1879,7 +1879,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelGadgetBaseSnaps(c *C) tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -2549,12 +2549,10 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseGadgetSnapsInstal c.Assert(tLinkKernel.Summary(), Equals, `Make snap "pc-kernel-new" (222) available to the system during remodel`) c.Assert(tSwitchChannelBase.Kind(), Equals, "switch-snap-channel") c.Assert(tSwitchChannelBase.Summary(), Equals, `Switch core20-new channel to latest/stable`) - c.Assert(tSwitchChannelBase.WaitTasks(), HasLen, 0) c.Assert(tLinkBase.Kind(), Equals, "link-snap") c.Assert(tLinkBase.Summary(), Equals, `Make snap "core20-new" (223) available to the system during remodel`) c.Assert(tSwitchChannelGadget.Kind(), Equals, "switch-snap-channel") c.Assert(tSwitchChannelGadget.Summary(), Equals, `Switch pc-new channel to 20/stable`) - c.Assert(tSwitchChannelGadget.WaitTasks(), HasLen, 0) c.Assert(tUpdateAssetsFromGadget.Kind(), Equals, "update-gadget-assets") c.Assert(tUpdateAssetsFromGadget.Summary(), Equals, `Update assets from gadget "pc-new" (224) for remodel`) c.Assert(tUpdateCmdlineFromGadget.Kind(), Equals, "update-gadget-cmdline") @@ -2568,19 +2566,29 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseGadgetSnapsInstal c.Assert(tSetModel.Summary(), Equals, "Set new model assertion") // check the ordering, prepare/link are part of download edge and come first c.Assert(tSwitchChannelKernel.WaitTasks(), HasLen, 0) - c.Check(tUpdateAssetsFromKernel.WaitTasks(), DeepEquals, []*state.Task{ + c.Assert(tSwitchChannelBase.WaitTasks(), DeepEquals, []*state.Task{ tSwitchChannelKernel, }) - c.Check(tLinkKernel.WaitTasks(), DeepEquals, []*state.Task{ - tUpdateAssetsFromKernel, - }) - c.Assert(tLinkBase.WaitTasks(), DeepEquals, []*state.Task{ + c.Assert(tSwitchChannelGadget.WaitTasks(), DeepEquals, []*state.Task{ tSwitchChannelBase, }) - c.Assert(tCreateRecovery.WaitTasks(), DeepEquals, []*state.Task{}) + c.Assert(tCreateRecovery.WaitTasks(), DeepEquals, []*state.Task{ + tSwitchChannelGadget, + }) c.Assert(tFinalizeRecovery.WaitTasks(), DeepEquals, []*state.Task{ // recovery system being created tCreateRecovery, + tSwitchChannelGadget, + }) + c.Check(tUpdateAssetsFromKernel.WaitTasks(), DeepEquals, []*state.Task{ + tSwitchChannelKernel, tSwitchChannelGadget, + tCreateRecovery, tFinalizeRecovery, + }) + c.Check(tLinkKernel.WaitTasks(), DeepEquals, []*state.Task{ + tUpdateAssetsFromKernel, + }) + c.Assert(tLinkBase.WaitTasks(), DeepEquals, []*state.Task{ + tSwitchChannelBase, tLinkKernel, }) // setModel waits for everything in the change c.Assert(tSetModel.WaitTasks(), DeepEquals, []*state.Task{ @@ -2596,8 +2604,12 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseGadgetSnapsInstal c.Assert(systemSetupData, DeepEquals, map[string]interface{}{ "label": expectedLabel, "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel), - // none of the tasks are downloads so they were not tracked - "snap-setup-tasks": nil, + // tasks carrying snap-setup are tracked + "snap-setup-tasks": []interface{}{ + tSwitchChannelKernel.ID(), + tSwitchChannelBase.ID(), + tSwitchChannelGadget.ID(), + }, }) } @@ -2618,7 +2630,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseSnapsInstalledSna tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -3675,7 +3687,7 @@ func (s *deviceMgrRemodelSuite) testUC20RemodelSetModel(c *C, tc uc20RemodelSetM tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -3954,7 +3966,7 @@ func (s *deviceMgrRemodelSuite) TestUC20RemodelSetModelWithReboot(c *C) { tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() diff --git a/overlord/devicestate/handlers_test.go b/overlord/devicestate/handlers_test.go index ec1794ed05..01b6663d23 100644 --- a/overlord/devicestate/handlers_test.go +++ b/overlord/devicestate/handlers_test.go @@ -332,7 +332,7 @@ func (s *deviceMgrSuite) TestDoPrepareRemodeling(c *C) { tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() diff --git a/overlord/managers_test.go b/overlord/managers_test.go index 5ae037ba23..759259bcd7 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -1953,7 +1953,7 @@ type: os st.Lock() c.Assert(err, IsNil) - // final steps will are post poned until we are in the restarted snapd + // final steps will are postponed until we are in the restarted snapd ok, rst := restart.Pending(st) c.Assert(ok, Equals, true) c.Assert(rst, Equals, restart.RestartSystem) @@ -1970,10 +1970,9 @@ type: os "snap_mode": boot.TryStatus, }) - // simulate successful restart happened - restart.MockPending(st, restart.RestartUnset) - bloader.BootVars["snap_mode"] = boot.DefaultStatus - bloader.SetBootBase("core_x1.snap") + // simulate successful restart happened, technically "core" is of type + // "os", but for the purpose of the mock it is handled like a base + s.mockSuccessfulReboot(c, bloader, []snap.Type{snap.TypeBase}) st.Unlock() err = s.o.Settle(settleTimeout) @@ -1981,6 +1980,99 @@ type: os c.Assert(err, IsNil) c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err())) + + c.Assert(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core_x1.snap", + "snap_try_core": "", + "snap_try_kernel": "", + "snap_mode": "", + }) +} + +func (s *mgrsSuite) TestInstallCoreSnapUpdatesBootloaderEnvAndFailWithRollback(c *C) { + bloader := boottest.MockUC16Bootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + bloader.SetBootBase("core_99.snap") + + restore := release.MockOnClassic(false) + defer restore() + + model := s.brands.Model("my-brand", "my-model", modelDefaults) + + const packageOS = ` +name: core +version: 16.04-1 +type: os +` + snapPath := makeTestSnap(c, packageOS) + + st := s.o.State() + st.Lock() + defer st.Unlock() + + // setup model assertion + assertstatetest.AddMany(st, s.brands.AccountsAndKeys("my-brand")...) + devicestatetest.SetDevice(st, &auth.DeviceState{ + Brand: "my-brand", + Model: "my-model", + Serial: "serialserialserial", + }) + err := assertstate.Add(st, model) + c.Assert(err, IsNil) + + ts, _, err := snapstate.InstallPath(st, &snap.SideInfo{RealName: "core"}, snapPath, "", "", snapstate.Flags{}) + c.Assert(err, IsNil) + chg := st.NewChange("install-snap", "...") + chg.AddAll(ts) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + // final steps will be postponed until we are in the restarted snapd + ok, rst := restart.Pending(st) + c.Assert(ok, Equals, true) + c.Assert(rst, Equals, restart.RestartSystem) + + t := findKind(chg, "auto-connect") + c.Assert(t, NotNil) + c.Assert(t.Status(), Equals, state.DoingStatus, Commentf("install-snap change failed with: %v", chg.Err())) + + // this is already set + c.Assert(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core_99.snap", + "snap_try_core": "core_x1.snap", + "snap_try_kernel": "", + "snap_mode": boot.TryStatus, + }) + + // simulate a reboot in which bootloader updates the env + s.mockRollbackAcrossReboot(c, bloader, []snap.Type{snap.TypeBase}) + + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + c.Assert(chg.Status(), Equals, state.ErrorStatus, Commentf("install-snap change did not fail")) + tLink := findKind(chg, "link-snap") + c.Assert(tLink, NotNil) + c.Assert(tLink.Status(), Equals, state.UndoneStatus) + + c.Assert(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core_99.snap", + "snap_try_core": "", + "snap_try_kernel": "", + "snap_mode": "", + }) } type rebootEnv interface { @@ -2110,6 +2202,14 @@ type: kernel` c.Assert(err, IsNil) c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err())) + + c.Assert(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core18_2.snap", + "snap_try_core": "", + "snap_kernel": "pc-kernel_x1.snap", + "snap_try_kernel": "", + "snap_mode": "", + }) } func (s *mgrsSuite) TestInstallKernelSnapUndoUpdatesBootloaderEnv(c *C) { @@ -2219,6 +2319,23 @@ type: kernel` }) restarting, _ = restart.Pending(st) c.Check(restarting, Equals, true) + + // pretend we restarted back to the old kernel + s.mockSuccessfulReboot(c, bloader, []snap.Type{snap.TypeKernel}) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + // and we undo the bootvars and trigger a reboot + c.Check(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core18_2.snap", + "snap_try_core": "", + "snap_try_kernel": "", + "snap_kernel": "pc-kernel_123.snap", + "snap_mode": "", + }) } func (s *mgrsSuite) TestInstallKernelSnap20UpdatesBootloaderEnv(c *C) { @@ -6207,7 +6324,7 @@ base: core22 const oldPcGadgetSnapYaml = ` version: 1.0 -name: pc +name: old-pc type: gadget base: core20 ` @@ -7481,6 +7598,170 @@ func (s *mgrsSuite) TestRemodelUC20BackToPreviousGadget(c *C) { c.Check(i, Equals, len(tasks)) } +func (s *mgrsSuite) TestRemodelUC20ExistingGadgetSnapDifferentChannel(c *C) { + // a remodel where the target model uses a gadget that is already + // present (possibly due to being used by one of the previous models) + // but tracks a different channel than what the new model ordains + s.testRemodelUC20WithRecoverySystemSimpleSetUp(c) + c.Assert(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "proc"), 0755), IsNil) + restore := osutil.MockProcCmdline(filepath.Join(dirs.GlobalRootDir, "proc/cmdline")) + defer restore() + newModel := s.brands.Model("can0nical", "my-model", uc20ModelDefaults, map[string]interface{}{ + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": fakeSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "old-pc", + "id": fakeSnapID("old-pc"), + "type": "gadget", + "default-channel": "20/edge", + }, + }, + "revision": "1", + }) + bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Role: bootloader.RoleRecovery}) + c.Assert(err, IsNil) + + st := s.o.State() + st.Lock() + defer st.Unlock() + + a11, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-name": "old-pc", + "snap-id": fakeSnapID("old-pc"), + "publisher-id": "can0nical", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + c.Assert(assertstate.Add(st, a11), IsNil) + c.Assert(s.storeSigning.Add(a11), IsNil) + + snapInfo := s.makeInstalledSnapInStateForRemodel(c, "old-pc", snap.R(1), "20/beta") + // there already is a snap revision assertion for this snap, just serve + // it in the mock store + s.serveSnap(snapInfo.MountFile(), "1") + + now := time.Now() + expectedLabel := now.Format("20060102") + + updater := &mockUpdater{} + restore = gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + // use a mock updater pretends an update was applied + return updater, nil + }) + defer restore() + + chg, err := devicestate.Remodel(st, newModel) + c.Assert(err, IsNil) + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil, Commentf(s.logbuf.String())) + // gadget update has not been applied yet + c.Check(updater.updateCalls, Equals, 0) + + // first comes a reboot to the new recovery system + c.Check(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) + c.Check(devicestate.RemodelingChange(st), NotNil) + restarting, kind := restart.Pending(st) + c.Check(restarting, Equals, true) + c.Assert(kind, Equals, restart.RestartSystemNow) + m, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(m.CurrentRecoverySystems, DeepEquals, []string{"1234", expectedLabel}) + c.Check(m.GoodRecoverySystems, DeepEquals, []string{"1234"}) + vars, err := bl.GetBootVars("try_recovery_system", "recovery_system_status") + c.Assert(err, IsNil) + c.Assert(vars, DeepEquals, map[string]string{ + "try_recovery_system": expectedLabel, + "recovery_system_status": "try", + }) + // simulate successful reboot to recovery and back + restart.MockPending(st, restart.RestartUnset) + // this would be done by snap-bootstrap in initramfs + err = bl.SetBootVars(map[string]string{ + "try_recovery_system": expectedLabel, + "recovery_system_status": "tried", + }) + c.Assert(err, IsNil) + // reset, so that after-reboot handling of tried system is executed + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + // update has been called for all 3 structures because of the remodel + // policy (there is no content bump, so there would be no updates + // otherwise) + c.Check(updater.updateCalls, Equals, 3) + // a reboot was requested, as mock updated were applied + restarting, kind = restart.Pending(st) + c.Check(restarting, Equals, true) + c.Assert(kind, Equals, restart.RestartSystem) + + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz", + }) + + // pretend we have the right command line + c.Assert(ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "proc/cmdline"), + []byte("snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz"), 0444), + IsNil) + + // run post boot code again + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + // verify command lines again + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz", + }) + + c.Check(chg.Status(), Equals, state.DoneStatus, Commentf("remodel change failed: %v", chg.Err())) + + var snapst snapstate.SnapState + err = snapstate.Get(st, "old-pc", &snapst) + c.Assert(err, IsNil) + // and the gadget tracking channel is the same as in the model + c.Check(snapst.TrackingChannel, Equals, "20/edge") + + // ensure sorting is correct + tasks := chg.Tasks() + sort.Sort(byReadyTime(tasks)) + + var i int + + // prepare first + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Switch snap "old-pc" from channel "20/beta" to "20/edge"`)) + i++ + // then recovery system + i += validateRecoverySystemTasks(c, tasks[i:], expectedLabel) + // then gadget switch with update of assets and kernel command line + i += validateGadgetSwitchTasks(c, tasks[i:], "old-pc", "1") + // finally new model assertion + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Set new model assertion`)) + i++ + c.Check(i, Equals, len(tasks)) +} + func (s *mgrsSuite) TestCheckRefreshFailureWithConcurrentRemoveOfConnectedSnap(c *C) { hookMgr := s.o.HookManager() c.Assert(hookMgr, NotNil) diff --git a/overlord/snapshotstate/backend/export_test.go b/overlord/snapshotstate/backend/export_test.go index 03ab11ffb5..aa33050d1b 100644 --- a/overlord/snapshotstate/backend/export_test.go +++ b/overlord/snapshotstate/backend/export_test.go @@ -48,14 +48,6 @@ func MockIsTesting(newIsTesting bool) func() { } } -func MockUserLookupId(newLookupId func(string) (*user.User, error)) func() { - oldLookupId := userLookupId - userLookupId = newLookupId - return func() { - userLookupId = oldLookupId - } -} - func MockOsOpen(newOsOpen func(string) (*os.File, error)) func() { oldOsOpen := osOpen osOpen = newOsOpen diff --git a/overlord/snapshotstate/backend/helpers.go b/overlord/snapshotstate/backend/helpers.go index 0099e7d9b0..95eab16e6a 100644 --- a/overlord/snapshotstate/backend/helpers.go +++ b/overlord/snapshotstate/backend/helpers.go @@ -27,9 +27,7 @@ import ( "os/exec" "os/user" "path/filepath" - "strconv" "strings" - "syscall" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" @@ -97,7 +95,7 @@ var ( func usersForUsernamesImpl(usernames []string, opts *dirs.SnapDirOptions) ([]*user.User, error) { if len(usernames) == 0 { - return allUsers(opts) + return snap.AllUsers(opts) } users := make([]*user.User, 0, len(usernames)) for _, username := range usernames { @@ -133,59 +131,6 @@ func usersForUsernamesImpl(usernames []string, opts *dirs.SnapDirOptions) ([]*us return users, nil } -func allUsers(opts *dirs.SnapDirOptions) ([]*user.User, error) { - ds, err := filepath.Glob(snap.DataHomeGlob(opts)) - if err != nil { - // can't happen? - return nil, err - } - - users := make([]*user.User, 1, len(ds)+1) - root, err := user.LookupId("0") - if err != nil { - return nil, err - } - users[0] = root - seen := make(map[uint32]bool, len(ds)+1) - seen[0] = true - var st syscall.Stat_t - for _, d := range ds { - err := syscall.Stat(d, &st) - if err != nil { - continue - } - if seen[st.Uid] { - continue - } - seen[st.Uid] = true - usr, err := userLookupId(strconv.FormatUint(uint64(st.Uid), 10)) - if err != nil { - // Treat all non-nil errors as user.Unknown{User,Group}Error's, as - // currently Go's handling of returned errno from get{pw,gr}nam_r - // in the cgo implementation of user.Lookup is lacking, and thus - // user.Unknown{User,Group}Error is returned only when errno is 0 - // and the list of users/groups is empty, but as per the man page - // for get{pw,gr}nam_r, there are many other errno's that typical - // systems could return to indicate that the user/group wasn't - // found, however unfortunately the POSIX standard does not actually - // dictate what errno should be used to indicate "user/group not - // found", and so even if Go is more robust, it may not ever be - // fully robust. See from the man page: - // - // > It [POSIX.1-2001] does not call "not found" an error, hence - // > does not specify what value errno might have in this situation. - // > But that makes it impossible to recognize errors. - // - // See upstream Go issue: https://github.com/golang/go/issues/40334 - continue - } else { - users = append(users, usr) - } - } - - return users, nil -} - var ( sysGeteuid = sys.Geteuid execLookPath = exec.LookPath diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go index d70e02cdc0..b35c62859d 100644 --- a/overlord/snapstate/backend.go +++ b/overlord/snapstate/backend.go @@ -83,7 +83,7 @@ type managerBackend interface { UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, installRecord *backend.InstallRecord, dev boot.Device, meter progress.Meter) error UndoCopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter, opts *dirs.SnapDirOptions) error // cleanup - ClearTrashedData(oldSnap *snap.Info, opts *dirs.SnapDirOptions) + ClearTrashedData(oldSnap *snap.Info) // remove related UnlinkSnap(info *snap.Info, linkCtx backend.LinkContext, meter progress.Meter) error @@ -108,4 +108,8 @@ type managerBackend interface { RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (*osutil.FileLock, error) // (not a backend method because doInstall cannot access the backend) // WithSnapLock(info *snap.Info, action func() error) error + + // ~/.snap/data migration related + HideSnapData(snapName string) error + UndoHideSnapData(snapName string) error } diff --git a/overlord/snapstate/backend/copydata.go b/overlord/snapstate/backend/copydata.go index 657f520afa..36baf5cb2a 100644 --- a/overlord/snapstate/backend/copydata.go +++ b/overlord/snapstate/backend/copydata.go @@ -20,18 +20,24 @@ package backend import ( + "errors" + "fmt" + "io/ioutil" "os" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/snap" ) +var allUsers = snap.AllUsers + // CopySnapData makes a copy of oldSnap data for newSnap in its data directories. func (b Backend) CopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter, opts *dirs.SnapDirOptions) error { // deal with the old data or - // otherwise just create a empty data dir + // otherwise just create an empty data dir // Make sure the base data directory exists for instance snaps if newSnap.InstanceKey != "" { @@ -86,16 +92,137 @@ func (b Backend) UndoCopySnapData(newInfo, oldInfo *snap.Info, _ progress.Meter, } // ClearTrashedData removes the trash. It returns no errors on the assumption that it is called very late in the game. -func (b Backend) ClearTrashedData(oldSnap *snap.Info, opts *dirs.SnapDirOptions) { - dirs, err := snapDataDirs(oldSnap, opts) +func (b Backend) ClearTrashedData(oldSnap *snap.Info) { + dataDirs, err := snapDataDirs(oldSnap, nil) if err != nil { logger.Noticef("Cannot remove previous data for %q: %v", oldSnap.InstanceName(), err) return } - for _, d := range dirs { + opts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + hiddenDirs, err := snapDataDirs(oldSnap, opts) + if err != nil { + logger.Noticef("Cannot remove previous data for %q: %v", oldSnap.InstanceName(), err) + return + } + + // this will have duplicates but the second remove will just be ignored + dataDirs = append(dataDirs, hiddenDirs...) + for _, d := range dataDirs { if err := clearTrash(d); err != nil { logger.Noticef("Cannot remove %s: %v", d, err) } } } + +func (b Backend) HideSnapData(snapName string) error { + postMigrationOpts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + + users, err := allUsers(nil) + if err != nil { + return err + } + + for _, usr := range users { + uid, gid, err := osutil.UidGid(usr) + if err != nil { + return err + } + + // nothing to migrate + oldSnapDir := snap.UserSnapDir(usr.HomeDir, snapName, nil) + if _, err := os.Stat(oldSnapDir); errors.Is(err, os.ErrNotExist) { + continue + } else if err != nil { + return fmt.Errorf("cannot stat snap dir %q: %w", oldSnapDir, err) + } + + // create the new hidden snap dir + hiddenSnapDir := snap.SnapDir(usr.HomeDir, postMigrationOpts) + if err := osutil.MkdirAllChown(hiddenSnapDir, 0700, uid, gid); err != nil { + return fmt.Errorf("cannot create snap dir %q: %w", hiddenSnapDir, err) + } + + // move the snap's dir + newSnapDir := snap.UserSnapDir(usr.HomeDir, snapName, postMigrationOpts) + if err := osutil.AtomicRename(oldSnapDir, newSnapDir); err != nil { + return fmt.Errorf("cannot move %q to %q: %w", oldSnapDir, newSnapDir, err) + } + + // remove ~/snap if it's empty + if err := removeIfEmpty(snap.SnapDir(usr.HomeDir, nil)); err != nil { + return fmt.Errorf("failed to remove old snap dir: %w", err) + } + } + + return nil +} + +func (b Backend) UndoHideSnapData(snapName string) error { + postMigrationOpts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + + users, err := allUsers(postMigrationOpts) + if err != nil { + return err + } + + var firstErr error + handle := func(err error) { + // keep going, restore previous state as much as possible + if firstErr == nil { + firstErr = err + } else { + logger.Noticef(err.Error()) + } + } + + for _, usr := range users { + uid, gid, err := osutil.UidGid(usr) + if err != nil { + handle(err) + continue + } + + // skip it if wasn't migrated + hiddenSnapDir := snap.UserSnapDir(usr.HomeDir, snapName, postMigrationOpts) + if _, err := os.Stat(hiddenSnapDir); err != nil { + if !errors.Is(err, os.ErrNotExist) { + handle(fmt.Errorf("cannot read files in %q: %w", hiddenSnapDir, err)) + } + continue + } + + // ensure parent dirs exist + exposedDir := snap.SnapDir(usr.HomeDir, nil) + if err := osutil.MkdirAllChown(exposedDir, 0700, uid, gid); err != nil { + handle(fmt.Errorf("cannot create snap dir %q: %w", exposedDir, err)) + continue + } + + exposedSnapDir := snap.UserSnapDir(usr.HomeDir, snapName, nil) + if err := osutil.AtomicRename(hiddenSnapDir, exposedSnapDir); err != nil { + handle(fmt.Errorf("cannot move %q to %q: %w", hiddenSnapDir, exposedSnapDir, err)) + } + + // remove ~/.snap/data dir if empty + hiddenDir := snap.SnapDir(usr.HomeDir, postMigrationOpts) + if err := removeIfEmpty(hiddenDir); err != nil { + handle(fmt.Errorf("cannot remove dir %q: %w", hiddenDir, err)) + } + } + + return firstErr +} + +var removeIfEmpty = func(dir string) error { + files, err := ioutil.ReadDir(dir) + if err != nil { + return err + } + + if len(files) > 0 { + return nil + } + + return os.Remove(dir) +} diff --git a/overlord/snapstate/backend/copydata_test.go b/overlord/snapstate/backend/copydata_test.go index 5727899a36..422057a630 100644 --- a/overlord/snapstate/backend/copydata_test.go +++ b/overlord/snapstate/backend/copydata_test.go @@ -20,9 +20,11 @@ package backend_test import ( + "errors" "fmt" "io/ioutil" "os" + "os/user" "path/filepath" "regexp" "strconv" @@ -30,6 +32,7 @@ import ( . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/snapstate/backend" "github.com/snapcore/snapd/progress" @@ -325,7 +328,7 @@ func (s *copydataSuite) testCopyDataDoABA(c *C, opts *dirs.SnapDirOptions) { c.Check(s.populatedData("10.old"), Equals, "10\n") // but cleanup cleans it up, huzzah - s.be.ClearTrashedData(v1, opts) + s.be.ClearTrashedData(v1) c.Check(s.populatedData("10.old"), Equals, "") } @@ -576,5 +579,270 @@ func (s *copydataSuite) TestUndoCopyDataSameRevision(c *C) { } { c.Check(osutil.FileExists(fn), Equals, true, Commentf(fn)) } +} + +func (s *copydataSuite) TestHideSnapData(c *C) { + info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + + // mock user home + homedir := filepath.Join(s.tempdir, "home", "user") + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + restore := backend.MockAllUsers(func(*dirs.SnapDirOptions) ([]*user.User, error) { + return []*user.User{usr}, nil + }) + defer restore() + + // writes a file canary.home file to the rev dir of the "hello" snap + s.populateHomeData(c, "user", snap.R(10)) + + // write file in common + err = os.MkdirAll(info.UserCommonDataDir(homedir, nil), 0770) + c.Assert(err, IsNil) + + commonFilePath := filepath.Join(info.UserCommonDataDir(homedir, nil), "file.txt") + err = ioutil.WriteFile(commonFilePath, []byte("some content"), 0640) + c.Assert(err, IsNil) + + // make 'current' symlink + revDir := snap.UserDataDir(homedir, "hello", snap.R(10), nil) + // path must be relative, otherwise move would make it dangling + err = os.Symlink(filepath.Base(revDir), filepath.Join(revDir, "..", "current")) + c.Assert(err, IsNil) + + err = s.be.HideSnapData("hello") + c.Assert(err, IsNil) + + // check versioned file was moved + opts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + revFile := filepath.Join(info.UserDataDir(homedir, opts), "canary.home") + data, err := ioutil.ReadFile(revFile) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte("10\n")) + + // check common file was moved + commonFile := filepath.Join(info.UserCommonDataDir(homedir, opts), "file.txt") + data, err = ioutil.ReadFile(commonFile) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte("some content")) + + // check 'current' symlink has correct attributes and target + link := filepath.Join(homedir, dirs.HiddenSnapDataHomeDir, "hello", "current") + linkInfo, err := os.Lstat(link) + c.Assert(err, IsNil) + c.Assert(linkInfo.Mode()&os.ModeSymlink, Equals, os.ModeSymlink) + + target, err := os.Readlink(link) + c.Assert(err, IsNil) + c.Assert(target, Equals, "10") + + // check old '~/snap' folder was removed + _, err = os.Stat(snap.SnapDir(homedir, nil)) + c.Assert(errors.Is(err, os.ErrNotExist), Equals, true) +} + +func (s *copydataSuite) TestHideSnapDataSkipNoData(c *C) { + info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + + // mock user home + homedir := filepath.Join(s.tempdir, "home", "user") + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + // create user without snap dir (to be skipped) + usrNoSnapDir := &user.User{ + HomeDir: filepath.Join(s.tempdir, "home", "other-user"), + Name: "other-user", + Uid: "1001", + Gid: "1001", + } + restore := backend.MockAllUsers(func(_ *dirs.SnapDirOptions) ([]*user.User, error) { + return []*user.User{usr, usrNoSnapDir}, nil + }) + defer restore() + + s.populateHomeData(c, "user", snap.R(10)) + + // make 'current' symlink + revDir := info.UserDataDir(homedir, nil) + linkPath := filepath.Join(revDir, "..", "current") + err = os.Symlink(revDir, linkPath) + c.Assert(err, IsNil) + + // empty user dir is skipped + err = s.be.HideSnapData("hello") + c.Assert(err, IsNil) + + // only the user with snap data was migrated + newSnapDir := filepath.Join(homedir, dirs.HiddenSnapDataHomeDir) + matches, err := filepath.Glob(dirs.HiddenSnapDataHomeGlob) + c.Assert(err, IsNil) + c.Assert(matches, HasLen, 1) + c.Assert(matches[0], Equals, newSnapDir) +} + +func (s *copydataSuite) TestUndoHideSnapData(c *C) { + info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + + // mock user home dir + homedir := filepath.Join(s.tempdir, "home", "user") + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + restore := backend.MockAllUsers(func(_ *dirs.SnapDirOptions) ([]*user.User, error) { + return []*user.User{usr}, nil + }) + defer restore() + + // write file in revisioned dir + opts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + err = os.MkdirAll(info.UserDataDir(homedir, opts), 0770) + c.Assert(err, IsNil) + + hiddenRevFile := filepath.Join(info.UserDataDir(homedir, opts), "file.txt") + err = ioutil.WriteFile(hiddenRevFile, []byte("some content"), 0640) + c.Assert(err, IsNil) + + // write file in common + err = os.MkdirAll(info.UserCommonDataDir(homedir, opts), 0770) + c.Assert(err, IsNil) + + hiddenCommonFile := filepath.Join(info.UserCommonDataDir(homedir, opts), "file.txt") + err = ioutil.WriteFile(hiddenCommonFile, []byte("other content"), 0640) + c.Assert(err, IsNil) + + // make 'current' symlink + revDir := info.UserDataDir(homedir, opts) + // path must be relative otherwise the move would make it dangling + err = os.Symlink(filepath.Base(revDir), filepath.Join(revDir, "..", "current")) + c.Assert(err, IsNil) + + // undo migration + err = s.be.UndoHideSnapData("hello") + c.Assert(err, IsNil) + + // check versioned file was restored + revFile := filepath.Join(info.UserDataDir(homedir, nil), "file.txt") + data, err := ioutil.ReadFile(revFile) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte("some content")) + + // check common file was restored + commonFile := filepath.Join(info.UserCommonDataDir(homedir, nil), "file.txt") + data, err = ioutil.ReadFile(commonFile) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte("other content")) + + // check symlink points to revisioned dir + exposedDir := filepath.Join(homedir, dirs.UserHomeSnapDir) + target, err := os.Readlink(filepath.Join(exposedDir, "hello", "current")) + c.Assert(err, IsNil) + c.Assert(target, Equals, "10") + + // ~/.snap/data was removed + _, err = os.Stat(snap.SnapDir(homedir, opts)) + c.Assert(errors.Is(err, os.ErrNotExist), Equals, true) +} + +func (s *copydataSuite) TestCleanupAfterCopyAndMigration(c *C) { + homedir := filepath.Join(s.tempdir, "home", "user") + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + restore := backend.MockAllUsers(func(_ *dirs.SnapDirOptions) ([]*user.User, error) { + return []*user.User{usr}, nil + }) + defer restore() + + // add trashed data in exposed dir + s.populateHomeData(c, "user", snap.R(10)) + v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + exposedTrash := filepath.Join(homedir, "snap", "hello", "10.old") + c.Assert(os.MkdirAll(exposedTrash, 0770), IsNil) + + // add trashed data in hidden dir + s.populateHomeDataWithSnapDir(c, "user", dirs.HiddenSnapDataHomeDir, snap.R(10)) + hiddenTrash := filepath.Join(homedir, ".snap", "data", "hello", "10.old") + c.Assert(os.MkdirAll(exposedTrash, 0770), IsNil) + + s.be.ClearTrashedData(v1) + + // clear should remove both + exists, _, err := osutil.DirExists(exposedTrash) + c.Assert(err, IsNil) + c.Assert(exists, Equals, false) + + exists, _, err = osutil.DirExists(hiddenTrash) + c.Assert(err, IsNil) + c.Assert(exists, Equals, false) +} + +func (s *copydataSuite) TestRemoveIfEmpty(c *C) { + file := filepath.Join(s.tempdir, "random") + c.Assert(ioutil.WriteFile(file, []byte("stuff"), 0664), IsNil) + + // dir contains a file, shouldn't do anything + c.Assert(backend.RemoveIfEmpty(s.tempdir), IsNil) + files, err := ioutil.ReadDir(s.tempdir) + c.Assert(err, IsNil) + c.Check(files, HasLen, 1) + c.Check(filepath.Join(s.tempdir, files[0].Name()), testutil.FileEquals, "stuff") + + c.Assert(os.Remove(file), IsNil) + + // dir is empty, should be removed + c.Assert(backend.RemoveIfEmpty(s.tempdir), IsNil) + c.Assert(osutil.FileExists(file), Equals, false) +} + +func (s *copydataSuite) TestUndoHideKeepGoingPreserveFirstErr(c *C) { + firstTime := true + restore := backend.MockRemoveIfEmpty(func(dir string) error { + var err error + if firstTime { + err = errors.New("first error") + firstTime = false + } else { + err = errors.New("other error") + } + + return err + }) + defer restore() + + info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + + // mock two users so that the undo is done twice + var usrs []*user.User + for _, usrName := range []string{"usr1", "usr2"} { + homedir := filepath.Join(s.tempdir, "home", usrName) + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + opts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + err = os.MkdirAll(info.UserDataDir(homedir, opts), 0770) + c.Assert(err, IsNil) + + usrs = append(usrs, usr) + } + + restUsers := backend.MockAllUsers(func(_ *dirs.SnapDirOptions) ([]*user.User, error) { + return usrs, nil + }) + defer restUsers() + + buf, restLogger := logger.MockLogger() + defer restLogger() + err := s.be.UndoHideSnapData("hello") + // the first error is returned + c.Assert(err, ErrorMatches, `cannot remove dir ".*": first error`) + // the undo keeps going and logs the next error + c.Assert(buf, Matches, `.*cannot remove dir ".*": other error\n`) } diff --git a/overlord/snapstate/backend/export_test.go b/overlord/snapstate/backend/export_test.go index d11379922d..53f54323cd 100644 --- a/overlord/snapstate/backend/export_test.go +++ b/overlord/snapstate/backend/export_test.go @@ -21,11 +21,15 @@ package backend import ( "os/exec" + "os/user" + + "github.com/snapcore/snapd/dirs" ) var ( AddMountUnit = addMountUnit RemoveMountUnit = removeMountUnit + RemoveIfEmpty = removeIfEmpty ) func MockUpdateFontconfigCaches(f func() error) (restore func()) { @@ -43,3 +47,20 @@ func MockCommandFromSystemSnap(f func(string, ...string) (*exec.Cmd, error)) (re commandFromSystemSnap = old } } + +func MockAllUsers(f func(options *dirs.SnapDirOptions) ([]*user.User, error)) func() { + old := allUsers + allUsers = f + return func() { + allUsers = old + } + +} + +func MockRemoveIfEmpty(f func(dir string) error) func() { + old := removeIfEmpty + removeIfEmpty = f + return func() { + removeIfEmpty = old + } +} diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index 9eda7b741c..f9511b906d 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -905,7 +905,7 @@ apps: return info, nil } -func (f *fakeSnappyBackend) ClearTrashedData(si *snap.Info, opts *dirs.SnapDirOptions) { +func (f *fakeSnappyBackend) ClearTrashedData(si *snap.Info) { f.appendOp(&fakeOp{ op: "cleanup-trash", name: si.InstanceName(), @@ -1239,6 +1239,16 @@ func (f *fakeSnappyBackend) RunInhibitSnapForUnlink(info *snap.Info, hint runinh return osutil.NewFileLock(filepath.Join(f.lockDir, info.InstanceName()+".lock")) } +func (f *fakeSnappyBackend) HideSnapData(snapName string) error { + f.appendOp(&fakeOp{op: "hide-snap-data", name: snapName}) + return f.maybeErrForLastOp() +} + +func (f *fakeSnappyBackend) UndoHideSnapData(snapName string) error { + f.appendOp(&fakeOp{op: "undo-hide-snap-data", name: snapName}) + return f.maybeErrForLastOp() +} + func (f *fakeSnappyBackend) appendOp(op *fakeOp) { f.mu.Lock() defer f.mu.Unlock() diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go index c2b309c739..d68604f0ee 100644 --- a/overlord/snapstate/export_test.go +++ b/overlord/snapstate/export_test.go @@ -212,6 +212,8 @@ func MockAsyncPendingRefreshNotification(fn func(context.Context, *userclient.Cl var ( RefreshedSnaps = refreshedSnaps ReRefreshFilter = reRefreshFilter + + MaybeRestoreValidationSetsAndRevertSnaps = maybeRestoreValidationSetsAndRevertSnaps ) type UpdateFilter = updateFilter @@ -366,3 +368,27 @@ func MockSnapsToRefresh(f func(gatingTask *state.Task) ([]*refreshCandidate, err snapsToRefresh = old } } + +func MockAddCurrentTrackingToValidationSetsStack(f func(st *state.State) error) (restore func()) { + old := AddCurrentTrackingToValidationSetsStack + AddCurrentTrackingToValidationSetsStack = f + return func() { + AddCurrentTrackingToValidationSetsStack = old + } +} + +func MockRestoreValidationSetsTracking(f func(*state.State) error) (restore func()) { + old := RestoreValidationSetsTracking + RestoreValidationSetsTracking = f + return func() { + RestoreValidationSetsTracking = old + } +} + +func MockMaybeRestoreValidationSetsAndRevertSnaps(f func(st *state.State, refreshedSnaps []string) ([]*state.TaskSet, error)) (restore func()) { + old := maybeRestoreValidationSetsAndRevertSnaps + maybeRestoreValidationSetsAndRevertSnaps = f + return func() { + maybeRestoreValidationSetsAndRevertSnaps = old + } +} diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go index 3e206e0910..b463e80a86 100644 --- a/overlord/snapstate/handlers.go +++ b/overlord/snapstate/handlers.go @@ -34,6 +34,7 @@ import ( "gopkg.in/tomb.v2" + "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/dirs" @@ -268,7 +269,7 @@ func (m *SnapManager) doPrerequisites(t *state.Task, _ *tomb.Tomb) error { } // if a previous version of snapd persisted Prereq only, fill the contentAttrs. - // There will be not content attrs, so it will not update an outdated default provider + // There will be no content attrs, so it will not update an outdated default provider if len(snapsup.PrereqContentAttrs) == 0 && len(snapsup.Prereq) != 0 { snapsup.PrereqContentAttrs = make(map[string][]string, len(snapsup.Prereq)) @@ -1221,12 +1222,7 @@ func (m *SnapManager) cleanupCopySnapData(t *state.Task, _ *tomb.Tomb) error { return err } - opts, err := GetSnapDirOptions(st) - if err != nil { - return err - } - - m.backend.ClearTrashedData(info, opts) + m.backend.ClearTrashedData(info) return nil } @@ -3149,13 +3145,14 @@ func changeReadyUpToTask(task *state.Task) bool { } // refreshedSnaps returns the instance names of the snaps successfully refreshed -// in the last batch of refreshes before the given (re-refresh) task. +// in the last batch of refreshes before the given (re-refresh) task; failed is +// true if any of the snaps failed to refresh. // // It does this by advancing through the given task's change's tasks, keeping // track of the instance names from the first SnapSetup in every lane, stopping // when finding the given task, and resetting things when finding a different // re-refresh task (that indicates the end of a batch that isn't the given one). -func refreshedSnaps(reTask *state.Task) []string { +func refreshedSnaps(reTask *state.Task) (snapNames []string, failed bool) { // NOTE nothing requires reTask to be a check-rerefresh task, nor even to be in // a refresh-ish change, but it doesn't make much sense to call this otherwise. tid := reTask.ID() @@ -3197,15 +3194,16 @@ func refreshedSnaps(reTask *state.Task) []string { laneSnaps[lane] = snapsup.InstanceName() } - snapNames := make([]string, 0, len(laneSnaps)) + snapNames = make([]string, 0, len(laneSnaps)) for _, name := range laneSnaps { if name == "" { // the lane was unsuccessful + failed = true continue } snapNames = append(snapNames, name) } - return snapNames + return snapNames, failed } // reRefreshSetup holds the necessary details to re-refresh snaps that need it @@ -3241,14 +3239,50 @@ func (m *SnapManager) doCheckReRefresh(t *state.Task, tomb *tomb.Tomb) error { if !changeReadyUpToTask(t) { return &state.Retry{After: reRefreshRetryTimeout, Reason: "pending refreshes"} } - snaps := refreshedSnaps(t) + + snaps, failed := refreshedSnaps(t) + if len(snaps) > 0 { + if err := pruneRefreshCandidates(st, snaps...); err != nil { + return err + } + } + + // if any snap failed to refresh, reconsider validation set tracking + if failed { + tasksets, err := maybeRestoreValidationSetsAndRevertSnaps(st, snaps) + if err != nil { + return err + } + if len(tasksets) > 0 { + chg := t.Change() + for _, taskset := range tasksets { + chg.AddAll(taskset) + } + st.EnsureBefore(0) + t.SetStatus(state.DoneStatus) + return nil + } + // else - validation sets tracking got restored or wasn't affected, carry on + } + if len(snaps) == 0 { // nothing to do (maybe everything failed) return nil } - if err := pruneRefreshCandidates(st, snaps...); err != nil { - return err + // update validation sets stack: there are two possibilities + // - if maybeRestoreValidationSetsAndRevertSnaps restored previous tracking + // or refresh succeeded and it hasn't changed then this is a noop + // (AddCurrentTrackingToValidationSetsStack ignores tracking if identical + // to the topmost stack entry); + // - if maybeRestoreValidationSetsAndRevertSnaps kept new tracking + // because its constraints were met even after partial failure or + // refresh succeeded and tracking got updated, then + // this creates a new copy of validation-sets tracking data. + if AddCurrentTrackingToValidationSetsStack != nil { + if err := AddCurrentTrackingToValidationSetsStack(st); err != nil { + return err + } } var re reRefreshSetup @@ -3317,6 +3351,86 @@ func (m *SnapManager) doConditionalAutoRefresh(t *state.Task, tomb *tomb.Tomb) e return nil } +// maybeRestoreValidationSetsAndRevertSnaps restores validation-sets to their +// previous state using validation sets stack if there are any enforced +// validation sets and - if necessary - creates tasksets to revert some or all +// of the refreshed snaps to their previous revisions to satisfy the restored +// validation sets tracking. +var maybeRestoreValidationSetsAndRevertSnaps = func(st *state.State, refreshedSnaps []string) ([]*state.TaskSet, error) { + enforcedSets, err := EnforcedValidationSets(st) + if err != nil { + return nil, err + } + if enforcedSets == nil { + // no enforced validation sets, nothing to do + return nil, nil + } + + installedSnaps, ignoreValidation, err := InstalledSnaps(st) + if err != nil { + return nil, err + } + if err := enforcedSets.CheckInstalledSnaps(installedSnaps, ignoreValidation); err == nil { + // validation sets are still correct, nothing to do + return nil, nil + } + + // restore previous validation sets tracking state + if err := RestoreValidationSetsTracking(st); err != nil { + return nil, fmt.Errorf("cannot restore validation sets: %v", err) + } + + // no snaps were refreshed, after restoring validation sets tracking + // there is nothing else to do + if len(refreshedSnaps) == 0 { + return nil, nil + } + + // check installed snaps again against restored validation-sets. + // this may fail which is fine, but it tells us which snaps are + // at invalid revisions and need reverting. + // note: we need to fetch enforced sets again because of RestoreValidationSetsTracking. + enforcedSets, err = EnforcedValidationSets(st) + if err != nil { + return nil, err + } + if enforcedSets == nil { + return nil, fmt.Errorf("internal error: no enforced validation sets after restoring from the stack") + } + err = enforcedSets.CheckInstalledSnaps(installedSnaps, ignoreValidation) + if err == nil { + // all fine after restoring validation sets: this can happen if previous + // validation sets only required a snap (regardless of its revision), then + // after update they require a specific snap revision, so after restoring + // we are back with the good state. + return nil, nil + } + verr, ok := err.(*snapasserts.ValidationSetsValidationError) + if !ok { + return nil, err + } + if len(verr.WrongRevisionSnaps) == 0 { + // if we hit ValidationSetsValidationError but it's not about wrong revisions, + // then something is really broken (we shouldn't have invalid or missing required + // snaps at this point). + return nil, fmt.Errorf("internal error: unexpected validation error of installed snaps after unsuccesfull refresh: %v", verr) + } + // revert some or all snaps + var tss []*state.TaskSet + for _, snapName := range refreshedSnaps { + if verr.WrongRevisionSnaps[snapName] != nil { + // XXX: should we be extra paranoid and use RevertToRevision with + // the specific revision from verr.WrongRevisionSnaps? + ts, err := Revert(st, snapName, Flags{RevertStatus: NotBlocked}) + if err != nil { + return nil, err + } + tss = append(tss, ts) + } + } + return tss, nil +} + // InjectTasks makes all the halt tasks of the mainTask wait for extraTasks; // extraTasks join the same lane and change as the mainTask. func InjectTasks(mainTask *state.Task, extraTasks *state.TaskSet) { diff --git a/overlord/snapstate/handlers_rerefresh_test.go b/overlord/snapstate/handlers_rerefresh_test.go index e35f2d8253..05f714181d 100644 --- a/overlord/snapstate/handlers_rerefresh_test.go +++ b/overlord/snapstate/handlers_rerefresh_test.go @@ -28,9 +28,12 @@ import ( . "gopkg.in/check.v1" + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" . "github.com/snapcore/snapd/testutil" ) @@ -204,7 +207,7 @@ func (s *reRefreshSuite) TestDoCheckReRefreshAddsNewTasks(c *C) { // wrapper around snapstate.RefreshedSnaps for easier testing func refreshedSnaps(task *state.Task) string { - snaps := snapstate.RefreshedSnaps(task) + snaps, _ := snapstate.RefreshedSnaps(task) sort.Strings(snaps) return strings.Join(snaps, ",") } @@ -369,3 +372,228 @@ func (s *reRefreshSuite) TestFilterReturnsFalseIfEpochEqualZero(c *C) { c.Check(snapstate.ReRefreshFilter(&snap.Info{Epoch: snap.E("0")}, snapst), Equals, false) c.Check(snapstate.ReRefreshFilter(&snap.Info{Epoch: snap.Epoch{}}, snapst), Equals, false) } + +func (s *refreshSuite) TestMaybeRestoreValidationSetsAndRevertSnaps(c *C) { + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + return nil, nil + }) + defer restore() + + st := s.state + st.Lock() + defer st.Unlock() + + refreshedSnaps := []string{"foo", "bar"} + // nothing to do with no enforced validation sets + ts, err := snapstate.MaybeRestoreValidationSetsAndRevertSnaps(st, refreshedSnaps) + c.Assert(err, IsNil) + c.Check(ts, IsNil) +} + +func (s *validationSetsSuite) TestMaybeRestoreValidationSetsAndRevertSnapsOneRevert(c *C) { + var enforcedValidationSetsCalled int + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + enforcedValidationSetsCalled++ + + vs := snapasserts.NewValidationSets() + var snap1, snap2, snap3 map[string]interface{} + snap3 = map[string]interface{}{ + "id": "abcKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap3", + "presence": "required", + } + + switch enforcedValidationSetsCalled { + case 1: + // refreshed validation sets + snap1 = map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap1", + "presence": "required", + "revision": "3", + } + // require snap2 at revision 5 (if snap refresh succeeded, but it didn't, so + // current revision of the snap is wrong) + snap2 = map[string]interface{}{ + "id": "bgtKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap2", + "presence": "required", + "revision": "5", + } + case 2: + // validation sets restored from history + snap1 = map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap1", + "presence": "required", + "revision": "1", + } + snap2 = map[string]interface{}{ + "id": "bgtKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap2", + "presence": "required", + "revision": "2", + } + default: + c.Fatalf("unexpected call to EnforcedValidatioSets") + } + vsa1 := s.mockValidationSetAssert(c, "bar", "2", snap1, snap2, snap3) + vs.Add(vsa1.(*asserts.ValidationSet)) + return vs, nil + }) + defer restore() + + var restoreValidationSetsTrackingCalled int + restoreRestoreValidationSetsTracking := snapstate.MockRestoreValidationSetsTracking(func(*state.State) error { + restoreValidationSetsTrackingCalled++ + return nil + }) + defer restoreRestoreValidationSetsTracking() + + st := s.state + st.Lock() + defer st.Unlock() + + // snaps installed after partial refresh + si1 := &snap.SideInfo{RealName: "some-snap1", SnapID: "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(3)} + si11 := &snap.SideInfo{RealName: "some-snap1", SnapID: "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(1)} + snapstate.Set(s.state, "some-snap1", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si11, si1}, + Current: snap.R(3), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap1`, si1) + + // some-snap2 failed to refresh and remains at revision 2 + si2 := &snap.SideInfo{RealName: "some-snap2", SnapID: "bgtKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(2)} + snapstate.Set(s.state, "some-snap2", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si2}, + Current: snap.R(2), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap2`, si2) + + si3 := &snap.SideInfo{RealName: "some-snap3", SnapID: "abcKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(3)} + snapstate.Set(s.state, "some-snap3", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si3}, + Current: snap.R(3), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap3`, si3) + + // some-snap2 failed to refresh + refreshedSnaps := []string{"some-snap1", "some-snap3"} + ts, err := snapstate.MaybeRestoreValidationSetsAndRevertSnaps(st, refreshedSnaps) + c.Assert(err, IsNil) + + // we expect revert of snap1 + c.Assert(ts, HasLen, 1) + revertTasks := ts[0].Tasks() + c.Assert(taskKinds(revertTasks), DeepEquals, []string{ + "prerequisites", + "prepare-snap", + "stop-snap-services", + "remove-aliases", + "unlink-current-snap", + "setup-profiles", + "link-snap", + "auto-connect", + "set-auto-aliases", + "setup-aliases", + "start-snap-services", + "run-hook[configure]", + "run-hook[check-health]", + }) + + snapsup, err := snapstate.TaskSnapSetup(revertTasks[0]) + c.Assert(err, IsNil) + c.Check(snapsup.Flags, Equals, snapstate.Flags{Revert: true, RevertStatus: snapstate.NotBlocked}) + c.Check(snapsup.InstanceName(), Equals, "some-snap1") + c.Check(snapsup.Revision(), Equals, snap.R(1)) + + c.Check(restoreValidationSetsTrackingCalled, Equals, 1) + c.Check(enforcedValidationSetsCalled, Equals, 2) +} + +func (s *validationSetsSuite) TestMaybeRestoreValidationSetsAndRevertJustValidationSetsRestore(c *C) { + var enforcedValidationSetsCalled int + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + enforcedValidationSetsCalled++ + + vs := snapasserts.NewValidationSets() + var snap1, snap2 map[string]interface{} + snap2 = map[string]interface{}{ + "id": "abcKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap2", + "presence": "required", + } + + switch enforcedValidationSetsCalled { + case 1: + // refreshed validation sets + // snap1 revision 3 is now required (but snap wasn't refreshed) + snap1 = map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap1", + "presence": "required", + "revision": "3", + } + case 2: + // validation sets restored from history + snap1 = map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap1", + "presence": "required", + "revision": "1", + } + default: + c.Fatalf("unexpected call to EnforcedValidatioSets") + } + vsa1 := s.mockValidationSetAssert(c, "bar", "2", snap1, snap2) + vs.Add(vsa1.(*asserts.ValidationSet)) + return vs, nil + }) + defer restore() + + var restoreValidationSetsTrackingCalled int + restoreRestoreValidationSetsTracking := snapstate.MockRestoreValidationSetsTracking(func(*state.State) error { + restoreValidationSetsTrackingCalled++ + return nil + }) + defer restoreRestoreValidationSetsTracking() + + st := s.state + st.Lock() + defer st.Unlock() + + // snaps in the system after partial refresh + si1 := &snap.SideInfo{RealName: "some-snap1", SnapID: "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(1)} + snapstate.Set(s.state, "some-snap1", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si1}, + Current: snap.R(1), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap1`, si1) + + si3 := &snap.SideInfo{RealName: "some-snap2", SnapID: "abcKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(3)} + snapstate.Set(s.state, "some-snap2", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si3}, + Current: snap.R(3), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap2`, si3) + + refreshedSnaps := []string{"some-snap2"} + ts, err := snapstate.MaybeRestoreValidationSetsAndRevertSnaps(st, refreshedSnaps) + c.Assert(err, IsNil) + + // we expect no snap reverts + c.Assert(ts, HasLen, 0) + c.Check(restoreValidationSetsTrackingCalled, Equals, 1) + c.Check(enforcedValidationSetsCalled, Equals, 2) +} diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 7276af01bf..56ff8c8f85 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -66,14 +66,14 @@ const ( ) const ( - DownloadAndChecksDoneEdge = state.TaskSetEdge("download-and-checks-done") - BeginEdge = state.TaskSetEdge("begin") - BeforeHooksEdge = state.TaskSetEdge("before-hooks") - HooksEdge = state.TaskSetEdge("hooks") - BeforeMaybeRebootEdge = state.TaskSetEdge("before-maybe-reboot") - MaybeRebootEdge = state.TaskSetEdge("maybe-reboot") - MaybeRebootWaitEdge = state.TaskSetEdge("maybe-reboot-wait") - AfterMaybeRebootWaitEdge = state.TaskSetEdge("after-maybe-reboot-wait") + BeginEdge = state.TaskSetEdge("begin") + BeforeHooksEdge = state.TaskSetEdge("before-hooks") + HooksEdge = state.TaskSetEdge("hooks") + BeforeMaybeRebootEdge = state.TaskSetEdge("before-maybe-reboot") + MaybeRebootEdge = state.TaskSetEdge("maybe-reboot") + MaybeRebootWaitEdge = state.TaskSetEdge("maybe-reboot-wait") + AfterMaybeRebootWaitEdge = state.TaskSetEdge("after-maybe-reboot-wait") + LastBeforeLocalModificationsEdge = state.TaskSetEdge("last-before-local-modifications") ) var ErrNothingToDo = errors.New("nothing to do") @@ -173,7 +173,7 @@ type pathInfo struct { } func (i pathInfo) DownloadSize() int64 { - return i.DownloadInfo.Size + return i.Size } // SnapBase returns the base snap of the snap. @@ -566,15 +566,20 @@ func doInstall(st *state.State, snapst *SnapState, snapsup *SnapSetup, flags int if installHook != nil { installSet.MarkEdge(installHook, HooksEdge) } - ts.AddAllWithEdges(installSet) + // if snap is being installed from the store, then the last task before + // any system modifications are done is check validate-snap, otherwise + // it's the prepare-snap if checkAsserts != nil { - ts.MarkEdge(checkAsserts, DownloadAndChecksDoneEdge) + installSet.MarkEdge(checkAsserts, LastBeforeLocalModificationsEdge) + } else { + installSet.MarkEdge(prepare, LastBeforeLocalModificationsEdge) } - if flags&skipConfigure != 0 { return installSet, nil } + ts.AddAllWithEdges(installSet) + // we do not support configuration for bases or the "snapd" snap yet if snapsup.Type != snap.TypeBase && snapsup.Type != snap.TypeSnapd { confFlags := 0 @@ -1003,7 +1008,9 @@ func TryPath(st *state.State, name, path string, flags Flags) (*state.TaskSet, e // Install returns a set of tasks for installing a snap. // Note that the state must be locked by the caller. // -// The returned TaskSet will contain a DownloadAndChecksDoneEdge. +// The returned TaskSet will contain a LastBeforeLocalModificationsEdge +// identifying the last task before the first task that introduces system +// modifications. func Install(ctx context.Context, st *state.State, name string, opts *RevisionOptions, userID int, flags Flags) (*state.TaskSet, error) { return InstallWithDeviceContext(ctx, st, name, opts, userID, flags, nil, "") } @@ -1012,7 +1019,9 @@ func Install(ctx context.Context, st *state.State, name string, opts *RevisionOp // It will query for the snap with the given deviceCtx. // Note that the state must be locked by the caller. // -// The returned TaskSet will contain a DownloadAndChecksDoneEdge. +// The returned TaskSet will contain a LastBeforeLocalModificationsEdge +// identifying the last task before the first task that introduces system +// modifications. func InstallWithDeviceContext(ctx context.Context, st *state.State, name string, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext, fromChange string) (*state.TaskSet, error) { if opts == nil { opts = &RevisionOptions{} @@ -1879,7 +1888,10 @@ type RevisionOptions struct { // Update initiates a change updating a snap. // Note that the state must be locked by the caller. // -// The returned TaskSet will contain a DownloadAndChecksDoneEdge. +// The returned TaskSet can contain a LastBeforeLocalModificationsEdge +// identifying the last task before the first task that introduces system +// modifications. If no such edge is set, then none of the tasks introduce +// system modifications. func Update(st *state.State, name string, opts *RevisionOptions, userID int, flags Flags) (*state.TaskSet, error) { return UpdateWithDeviceContext(st, name, opts, userID, flags, nil, "") } @@ -1888,7 +1900,10 @@ func Update(st *state.State, name string, opts *RevisionOptions, userID int, fla // It will query for the snap with the given deviceCtx. // Note that the state must be locked by the caller. // -// The returned TaskSet will contain a DownloadAndChecksDoneEdge. +// The returned TaskSet can contain a LastBeforeLocalModificationsEdge +// identifying the last task before the first task that introduces system +// modifications. If no such edge is set, then none of the tasks introduce +// system modifications. func UpdateWithDeviceContext(st *state.State, name string, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext, fromChange string) (*state.TaskSet, error) { if opts == nil { opts = &RevisionOptions{} @@ -2069,6 +2084,10 @@ func infoForUpdate(st *state.State, snapst *SnapState, name string, opts *Revisi // into the Autorefresh function. var AutoRefreshAssertions func(st *state.State, userID int) error +var AddCurrentTrackingToValidationSetsStack func(st *state.State) error + +var RestoreValidationSetsTracking func(st *state.State) error + // AutoRefresh is the wrapper that will do a refresh of all the installed // snaps on the system. In addition to that it will also refresh important // assertions. @@ -2377,8 +2396,8 @@ func LinkNewBaseOrKernel(st *state.State, name string) (*state.TaskSet, error) { linkSnap.Set("snap-setup-task", prepareSnap.ID()) linkSnap.WaitFor(prev) ts.AddTask(linkSnap) - // we need this for remodel - ts.MarkEdge(prepareSnap, DownloadAndChecksDoneEdge) + // prepare-snap is the last task that carries no system modifications + ts.MarkEdge(prepareSnap, LastBeforeLocalModificationsEdge) return ts, nil } @@ -2423,6 +2442,14 @@ func AddLinkNewBaseOrKernel(st *state.State, ts *state.TaskSet) (*state.TaskSet, linkSnap.Set("snap-setup-task", snapSetupTask.ID()) linkSnap.WaitFor(prev) ts.AddTask(linkSnap) + // make sure that remodel can identify which tasks introduce actual + // changes to the system and order them correctly + if edgeTask := ts.MaybeEdge(LastBeforeLocalModificationsEdge); edgeTask == nil { + // no task in the task set is marked as last before system + // modifications are introduced, so we need to mark the last + // task in the set, as tasks introduced here modify system state + ts.MarkEdge(allTasks[len(allTasks)-1], LastBeforeLocalModificationsEdge) + } return ts, nil } @@ -2470,9 +2497,9 @@ func SwitchToNewGadget(st *state.State, name string) (*state.TaskSet, error) { gadgetCmdline.WaitFor(gadgetUpdate) gadgetCmdline.Set("snap-setup-task", prepareSnap.ID()) - // we need this for remodel ts := state.NewTaskSet(prepareSnap, gadgetUpdate, gadgetCmdline) - ts.MarkEdge(prepareSnap, DownloadAndChecksDoneEdge) + // prepare-snap is the last task that carries no system modifications + ts.MarkEdge(prepareSnap, LastBeforeLocalModificationsEdge) return ts, nil } @@ -2497,6 +2524,14 @@ func AddGadgetAssetsTasks(st *state.State, ts *state.TaskSet) (*state.TaskSet, e gadgetCmdline.Set("snap-setup-task", snapSetupTask.ID()) gadgetCmdline.WaitFor(gadgetUpdate) ts.AddTask(gadgetCmdline) + // make sure that remodel can identify which tasks introduce actual + // changes to the system and order them correctly + if edgeTask := ts.MaybeEdge(LastBeforeLocalModificationsEdge); edgeTask == nil { + // no task in the task set is marked as last before system + // modifications are introduced, so we need to mark the last + // task in the set, as tasks introduced here modify system state + ts.MarkEdge(allTasks[len(allTasks)-1], LastBeforeLocalModificationsEdge) + } return ts, nil } diff --git a/overlord/snapstate/snapstate_install_test.go b/overlord/snapstate/snapstate_install_test.go index 34eb41635d..536d4d2a2e 100644 --- a/overlord/snapstate/snapstate_install_test.go +++ b/overlord/snapstate/snapstate_install_test.go @@ -148,6 +148,12 @@ func verifyInstallTasks(c *C, typ snap.Type, opts, discards int, ts *state.TaskS expected := expectedDoInstallTasks(typ, opts, discards, nil, nil) c.Assert(kinds, DeepEquals, expected) + + if opts&noLastBeforeModificationsEdge == 0 { + te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) + c.Assert(te, NotNil) + c.Assert(te.Kind(), Equals, "validate-snap") + } } func (s *snapmgrTestSuite) TestInstallDevModeConfinementFiltering(c *C) { @@ -4594,7 +4600,12 @@ epoch: 42 } func (s *snapmgrTestSuite) TestInstallPathManyClassicAsUpdate(c *C) { - restore := snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) { + restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"}) + defer restore() + // this needs doing because dirs depends on the release info + dirs.SetRootDir(dirs.GlobalRootDir) + + restore = snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) { return &snap.Info{SuggestedName: name, Confinement: "classic"}, nil }) defer restore() @@ -4701,3 +4712,73 @@ version: 1 return brokenSnap, si } + +func (s *snapmgrTestSuite) TestInstallPathManyWithLocalPrereqAndBaseNoStore(c *C) { + s.state.Lock() + defer s.state.Unlock() + + tr := config.NewTransaction(s.state) + c.Assert(tr.Set("core", "experimental.check-disk-space-install", true), IsNil) + tr.Commit() + + // use the real disk check since it also includes store checks + restore := snapstate.MockInstallSize(snapstate.InstallSize) + defer restore() + + // no core, we'll install it as well + snapstate.Set(s.state, "core", nil) + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "prereq-snap", "core"} + yamls := []string{ + `name: some-snap +version: 1.0 +base: core +plugs: + myplug: + interface: content + content: mycontent + default-provider: prereq-snap +`, + `name: prereq-snap +version: 1.0 +base: core +slots: + myslot: + interface: content + content: mycontent`, + `name: core +version: 1.0 +type: base +`, + } + + for i, name := range snapNames { + paths = append(paths, makeTestSnap(c, yamls[i])) + si := &snap.SideInfo{ + RealName: name, + Revision: snap.R("1"), + } + sideInfos = append(sideInfos, si) + } + + tss, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, 0, nil) + c.Assert(err, IsNil) + c.Assert(tss, HasLen, 3) + + chg := s.state.NewChange("install", "install local snaps") + for _, ts := range tss { + chg.AddAll(ts) + } + + defer s.se.Stop() + s.settle(c) + + c.Assert(chg.Err(), IsNil) + c.Assert(chg.IsReady(), Equals, true) + + op := s.fakeBackend.ops.First("storesvc-snap-action") + c.Assert(op, IsNil) +} diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index 15eb0568e9..7d5c01ef1f 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -430,6 +430,7 @@ const ( updatesGadgetAssets updatesBootConfig noConfigure + noLastBeforeModificationsEdge ) func taskKinds(tasks []*state.Task) []string { @@ -4950,7 +4951,9 @@ func (s *snapmgrTestSuite) TestTransitionSnapdSnapWithCoreRunthrough(c *C) { c.Assert(chg.IsReady(), Equals, true) c.Check(s.fakeStore.downloads, HasLen, 1) ts := state.NewTaskSet(chg.Tasks()...) - verifyInstallTasks(c, snap.TypeSnapd, noConfigure, 0, ts) + // task set was reconstituted from change tasks, so edges information is + // lost + verifyInstallTasks(c, snap.TypeSnapd, noConfigure|noLastBeforeModificationsEdge, 0, ts) // ensure preferences from the core snap got transferred over var snapst snapstate.SnapState diff --git a/overlord/snapstate/snapstate_update_test.go b/overlord/snapstate/snapstate_update_test.go index e8bdd74dde..2d4d6c3869 100644 --- a/overlord/snapstate/snapstate_update_test.go +++ b/overlord/snapstate/snapstate_update_test.go @@ -37,6 +37,7 @@ import ( "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" @@ -65,6 +66,10 @@ func verifyUpdateTasks(c *C, typ snap.Type, opts, discards int, ts *state.TaskSe } c.Assert(kinds, DeepEquals, expected) + + te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) + c.Assert(te, NotNil) + c.Assert(te.Kind(), Equals, "validate-snap") } func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) { @@ -815,6 +820,11 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) { c.Assert(err, IsNil) chg.AddAll(ts) + // local modifications, edge must be set + te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) + c.Assert(te, NotNil) + c.Assert(te.Kind(), Equals, "validate-snap") + defer s.se.Stop() s.settle(c) @@ -2705,6 +2715,10 @@ func (s *snapmgrTestSuite) TestUpdateSameRevisionSwitchChannelRunThrough(c *C) { chg := s.state.NewChange("refresh", "refresh a snap") chg.AddAll(ts) + // no local modifications, hence no edge + te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) + c.Assert(te, IsNil) + defer s.se.Stop() s.settle(c) @@ -4413,7 +4427,7 @@ func (s *snapmgrTestSuite) testUpdateCreatesGCTasks(c *C, expectedDiscards int) c.Assert(err, IsNil) // ensure edges information is still there - te, err := ts.Edge(snapstate.DownloadAndChecksDoneEdge) + te, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge) c.Assert(te, NotNil) c.Assert(err, IsNil) @@ -4494,7 +4508,7 @@ func (s *snapmgrTestSuite) TestUpdateMany(c *C) { c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())+1) // 1==rerefresh // ensure edges information is still there - te, err := ts.Edge(snapstate.DownloadAndChecksDoneEdge) + te, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge) c.Assert(te, NotNil) c.Assert(err, IsNil) @@ -6844,6 +6858,141 @@ func (s *snapmgrTestSuite) TestUpdatePrerequisiteWithSameDeviceContext(c *C) { }) } +func (s *validationSetsSuite) testUpdateManyValidationSetsPartialFailure(c *C) *state.Change { + logbuf, rest := logger.MockLogger() + defer rest() + + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + vs := snapasserts.NewValidationSets() + snap1 := map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap", + "presence": "required", + } + snap2 := map[string]interface{}{ + "id": "bgtKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-other-snap", + "presence": "required", + } + vsa1 := s.mockValidationSetAssert(c, "bar", "2", snap1, snap2) + vs.Add(vsa1.(*asserts.ValidationSet)) + return vs, nil + }) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + tr := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: 2, + } + assertstate.UpdateValidationSet(s.state, &tr) + + si1 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)} + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si1}, + Current: snap.R(1), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap`, si1) + + si2 := &snap.SideInfo{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)} + snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si2}, + Current: snap.R(1), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-other-snap`, si2) + + s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/some-other-snap/11") + + names, tss, err := snapstate.UpdateMany(context.Background(), s.state, nil, s.user.ID, &snapstate.Flags{}) + c.Assert(err, IsNil) + c.Check(names, DeepEquals, []string{"some-other-snap", "some-snap"}) + c.Check(logbuf.String(), Equals, "") + chg := s.state.NewChange("update", "") + for _, ts := range tss { + chg.AddAll(ts) + } + + s.settle(c) + + return chg +} + +func (s *validationSetsSuite) TestUpdateManyValidationSetsPartialFailureNothingToRestore(c *C) { + var refreshed []string + restoreMaybeRestoreValidationSetsAndRevertSnaps := snapstate.MockMaybeRestoreValidationSetsAndRevertSnaps(func(st *state.State, refreshedSnaps []string) ([]*state.TaskSet, error) { + refreshed = refreshedSnaps + // nothing to restore + return nil, nil + }) + defer restoreMaybeRestoreValidationSetsAndRevertSnaps() + + var addCurrentTrackingToValidationSetsStackCalled int + restoreAddCurrentTrackingToValidationSetsStack := snapstate.MockAddCurrentTrackingToValidationSetsStack(func(st *state.State) error { + addCurrentTrackingToValidationSetsStackCalled++ + return nil + }) + defer restoreAddCurrentTrackingToValidationSetsStack() + + s.testUpdateManyValidationSetsPartialFailure(c) + + // only some-snap was successfully refreshed, this also confirms that + // mockMaybeRestoreValidationSetsAndRevertSnaps was called. + c.Check(refreshed, DeepEquals, []string{"some-snap"}) + + // validation sets history update was attempted (could be a no-op if + // maybeRestoreValidationSetsAndRevertSnaps restored last tracking + // data). + c.Check(addCurrentTrackingToValidationSetsStackCalled, Equals, 1) +} + +func (s *validationSetsSuite) TestUpdateManyValidationSetsPartialFailureRevertTasks(c *C) { + var refreshed []string + restoreMaybeRestoreValidationSetsAndRevertSnaps := snapstate.MockMaybeRestoreValidationSetsAndRevertSnaps(func(st *state.State, refreshedSnaps []string) ([]*state.TaskSet, error) { + refreshed = refreshedSnaps + ts := state.NewTaskSet(st.NewTask("fake-revert-task", "")) + return []*state.TaskSet{ts}, nil + }) + defer restoreMaybeRestoreValidationSetsAndRevertSnaps() + + var addCurrentTrackingToValidationSetsStackCalled int + restoreAddCurrentTrackingToValidationSetsStack := snapstate.MockAddCurrentTrackingToValidationSetsStack(func(st *state.State) error { + addCurrentTrackingToValidationSetsStackCalled++ + return nil + }) + defer restoreAddCurrentTrackingToValidationSetsStack() + + chg := s.testUpdateManyValidationSetsPartialFailure(c) + + // only some-snap was successfully refreshed, this also confirms that + // mockMaybeRestoreValidationSetsAndRevertSnaps was called. + c.Check(refreshed, DeepEquals, []string{"some-snap"}) + + s.state.Lock() + defer s.state.Unlock() + + // check that a fake revert task returned by maybeRestoreValidationSetsAndRevertSnaps + // got injected into the refresh change. + var seen bool + for _, t := range chg.Tasks() { + if t.Kind() == "fake-revert-task" { + seen = true + break + } + } + c.Check(seen, Equals, true) + + // we haven't updated validation sets history + c.Check(addCurrentTrackingToValidationSetsStackCalled, Equals, 0) +} + func (s *snapmgrTestSuite) TestUpdatePrerequisiteBackwardsCompat(c *C) { s.state.Lock() defer s.state.Unlock() diff --git a/overlord/snapstate/storehelpers.go b/overlord/snapstate/storehelpers.go index 0840c1109f..15de87e557 100644 --- a/overlord/snapstate/storehelpers.go +++ b/overlord/snapstate/storehelpers.go @@ -113,6 +113,12 @@ var installSize = func(st *state.State, snaps []minimalInstallInfo, userID int) accountedSnaps[snap.InstanceName] = true } + // if the prerequisites are included in the install, don't query the store + // for info on them + for _, snap := range snaps { + accountedSnaps[snap.InstanceName()] = true + } + var prereqs []string resolveBaseAndContentProviders := func(inst minimalInstallInfo) { diff --git a/overlord/snapstate/storehelpers_test.go b/overlord/snapstate/storehelpers_test.go index f12b38f8ee..223dd0c660 100644 --- a/overlord/snapstate/storehelpers_test.go +++ b/overlord/snapstate/storehelpers_test.go @@ -351,3 +351,135 @@ func (s *snapmgrTestSuite) TestInstallSizeErrorNoDownloadInfo(c *C) { _, err := snapstate.InstallSize(st, []snapstate.MinimalInstallInfo{snapstate.InstallSnapInfo{Info: snap1}}, 0) c.Assert(err, ErrorMatches, `internal error: download info missing.*`) } + +func (s *snapmgrTestSuite) TestInstallSizeWithPrereqNoStore(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + repo := interfaces.NewRepository() + ifacerepo.Replace(st, repo) + + s.setupInstallSizeStore() + + snap1 := snaptest.MockSnap(c, `name: some-snap +version: 1.0 +epoch: 1 +base: core +plugs: + myplug: + interface: content + content: mycontent + content-provider: some-snap2`, &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(1), + }) + snap1.Size = snap1Size + + snap2 := snaptest.MockSnap(c, `name: some-snap2 +version: 1.0 +epoch: 1 +base: core +slots: + myslot: + interface: content + content: mycontent`, &snap.SideInfo{ + RealName: "some-snap2", + Revision: snap.R(1), + }) + snap2.Size = snap2Size + + // core is already installed + s.mockCoreSnap(c) + + sz, err := snapstate.InstallSize(st, []snapstate.MinimalInstallInfo{ + snapstate.InstallSnapInfo{Info: snap1}, snapstate.InstallSnapInfo{Info: snap2}}, 0) + c.Assert(err, IsNil) + c.Check(sz, Equals, uint64(snap1Size+snap2Size)) + + // no call to the store is made + c.Assert(s.fakeStore.fakeBackend.ops, HasLen, 0) +} + +func (s *snapmgrTestSuite) TestInstallSizeWithPrereqAndCoreNoStore(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + repo := interfaces.NewRepository() + ifacerepo.Replace(st, repo) + + s.setupInstallSizeStore() + + snap1 := snaptest.MockSnap(c, `name: some-snap +version: 1.0 +epoch: 1 +base: core +plugs: + myplug: + interface: content + content: mycontent + content-provider: some-snap2`, &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(1), + }) + snap1.Size = snap1Size + + snap2 := snaptest.MockSnap(c, `name: some-snap2 +version: 1.0 +epoch: 1 +base: core +slots: + myslot: + interface: content + content: mycontent`, &snap.SideInfo{ + RealName: "some-snap2", + Revision: snap.R(1), + }) + snap2.Size = snap2Size + + core := snaptest.MockSnap(c, `name: core +version: 1.0 +epoch: 1 +type: os`, &snap.SideInfo{ + RealName: "core", + Revision: snap.R(1), + }) + core.Size = someBaseSize + + sz, err := snapstate.InstallSize(st, []snapstate.MinimalInstallInfo{ + snapstate.InstallSnapInfo{Info: snap1}, snapstate.InstallSnapInfo{Info: snap2}, snapstate.InstallSnapInfo{Info: core}}, 0) + c.Assert(err, IsNil) + c.Check(sz, Equals, uint64(snap1Size+snap2Size+someBaseSize)) + + // no call to the store is made + c.Assert(s.fakeStore.fakeBackend.ops, HasLen, 0) +} + +func (s *snapmgrTestSuite) TestInstallSizeRemotePrereq(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + repo := interfaces.NewRepository() + ifacerepo.Replace(st, repo) + + s.setupInstallSizeStore() + + snap1 := snaptest.MockSnap(c, snapYamlWithContentPlug1, &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(1), + }) + snap1.Size = snap1Size + + s.mockCoreSnap(c) + + sz, err := snapstate.InstallSize(st, []snapstate.MinimalInstallInfo{ + snapstate.InstallSnapInfo{Info: snap1}}, 0) + c.Assert(err, IsNil) + c.Check(sz, Equals, uint64(snap1Size+snapContentSlotSize+someBaseSize)) + + // the prereq's size info is fetched from the store + op := s.fakeStore.fakeBackend.ops.MustFindOp(c, "storesvc-snap-action:action") + c.Assert(op.action.InstanceName, Equals, "snap-content-slot") +} diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 4afc3b7a8e..dd769a9c5c 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -11,7 +11,7 @@ pkgdesc="Service and tools for management of snap packages." depends=('squashfs-tools' 'libseccomp' 'libsystemd' 'apparmor') optdepends=('bash-completion: bash completion support' 'xdg-desktop-portal: desktop integration') -pkgver=2.53.2 +pkgver=2.54.1 pkgrel=1 arch=('x86_64' 'i686' 'armv7h' 'aarch64') url="https://github.com/snapcore/snapd" diff --git a/packaging/debian-sid/changelog b/packaging/debian-sid/changelog index fc1fdefee0..f0f698019f 100644 --- a/packaging/debian-sid/changelog +++ b/packaging/debian-sid/changelog @@ -1,3 +1,383 @@ +snapd (2.54.1-1) unstable; urgency=medium + + * New upstream release, LP: #1955137 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + + -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 20 Dec 2021 10:06:09 +0100 + +snapd (2.54-1) unstable; urgency=medium + + * New upstream release, LP: #1955137 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + + -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 17 Dec 2021 15:49:18 +0100 + +snapd (2.53.4-1) unstable; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + + -- Ian Johnson <ian.johnson@canonical.com> Thu, 02 Dec 2021 17:16:48 -0600 + +snapd (2.53.3-1) unstable; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + + -- Ian Johnson <ian.johnson@canonical.com> Thu, 02 Dec 2021 11:42:15 -0600 + snapd (2.53.2-1) unstable; urgency=medium * New upstream release, LP: #1946127 diff --git a/packaging/debian-sid/snapd.postrm b/packaging/debian-sid/snapd.postrm index ab41eca635..009b4d4add 100644 --- a/packaging/debian-sid/snapd.postrm +++ b/packaging/debian-sid/snapd.postrm @@ -79,6 +79,7 @@ if [ "$1" = "purge" ]; then fi # modules rm -f "/etc/modules-load.d/snap.${snap}.conf" + rm -f "/etc/modprobe.d/snap.${snap}.conf" # timer and socket units find /etc/systemd/system -name "snap.${snap}.*.timer" -o -name "snap.${snap}.*.socket" | while read -r f; do systemctl_stop "$(basename "$f")" diff --git a/packaging/fedora-35 b/packaging/fedora-35 new file mode 120000 index 0000000000..100fe0cd7b --- /dev/null +++ b/packaging/fedora-35 @@ -0,0 +1 @@ +fedora \ No newline at end of file diff --git a/packaging/fedora/snapd.spec b/packaging/fedora/snapd.spec index be2e8db87c..5eb68777b5 100644 --- a/packaging/fedora/snapd.spec +++ b/packaging/fedora/snapd.spec @@ -102,7 +102,7 @@ %endif Name: snapd -Version: 2.53.2 +Version: 2.54.1 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -989,6 +989,374 @@ fi %changelog +* Mon Dec 20 2021 Michael Vogt <michael.vogt@ubuntu.com> +- New upstream release 2.54.1 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + +* Fri Dec 17 2021 Michael Vogt <michael.vogt@ubuntu.com> +- New upstream release 2.54 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + +* Thu Dec 02 2021 Ian Johnson <ian.johnson@canonical.com> +- New upstream release 2.53.4 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + +* Thu Dec 02 2021 Ian Johnson <ian.johnson@canonical.com> +- New upstream release 2.53.3 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + * Mon Nov 15 2021 Ian Johnson <ian.johnson@canonical.com> - New upstream release 2.53.2 - interfaces/builtin/block_devices: allow blkid to print block diff --git a/packaging/opensuse/snapd.changes b/packaging/opensuse/snapd.changes index ddff24a8cf..c047c3c8bd 100644 --- a/packaging/opensuse/snapd.changes +++ b/packaging/opensuse/snapd.changes @@ -1,4 +1,24 @@ ------------------------------------------------------------------- +Mon Dec 20 09:06:09 UTC 2021 - michael.vogt@ubuntu.com + +- Update to upstream release 2.54.1 + +------------------------------------------------------------------- +Fri Dec 17 14:49:18 UTC 2021 - michael.vogt@ubuntu.com + +- Update to upstream release 2.54 + +------------------------------------------------------------------- +Thu Dec 02 23:16:48 UTC 2021 - ian.johnson@canonical.com + +- Update to upstream release 2.53.4 + +------------------------------------------------------------------- +Thu Dec 02 17:42:15 UTC 2021 - ian.johnson@canonical.com + +- Update to upstream release 2.53.3 + +------------------------------------------------------------------- Mon Nov 15 22:09:09 UTC 2021 - ian.johnson@canonical.com - Update to upstream release 2.53.2 diff --git a/packaging/opensuse/snapd.spec b/packaging/opensuse/snapd.spec index 61b2a5af3f..5f3a2a12c6 100644 --- a/packaging/opensuse/snapd.spec +++ b/packaging/opensuse/snapd.spec @@ -81,7 +81,7 @@ Name: snapd -Version: 2.53.2 +Version: 2.54.1 Release: 0 Summary: Tools enabling systems to work with .snap files License: GPL-3.0 @@ -125,6 +125,7 @@ BuildRequires: ca-certificates-mozilla %if %{with apparmor} BuildRequires: libapparmor-devel BuildRequires: apparmor-rpm-macros +BuildRequires: apparmor-parser %endif PreReq: permissions @@ -418,6 +419,7 @@ fi %dir %{_datadir}/zsh %dir %{_datadir}/zsh/site-functions # similar case for fish +%dir %{_datadir}/fish %dir %{_datadir}/fish/vendor_conf.d # Ghost entries for things created at runtime diff --git a/packaging/ubuntu-14.04/changelog b/packaging/ubuntu-14.04/changelog index e6c705b37b..aa0f8635a6 100644 --- a/packaging/ubuntu-14.04/changelog +++ b/packaging/ubuntu-14.04/changelog @@ -1,3 +1,383 @@ +snapd (2.54.1~14.04) trusty; urgency=medium + + * New upstream release, LP: #1955137 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + + -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 20 Dec 2021 10:06:09 +0100 + +snapd (2.54~14.04) trusty; urgency=medium + + * New upstream release, LP: #1955137 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + + -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 17 Dec 2021 15:49:18 +0100 + +snapd (2.53.4~14.04) trusty; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + + -- Ian Johnson <ian.johnson@canonical.com> Thu, 02 Dec 2021 17:16:48 -0600 + +snapd (2.53.3~14.04) trusty; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + + -- Ian Johnson <ian.johnson@canonical.com> Thu, 02 Dec 2021 11:42:15 -0600 + snapd (2.53.2~14.04) trusty; urgency=medium * New upstream release, LP: #1946127 diff --git a/packaging/ubuntu-14.04/snapd.postrm b/packaging/ubuntu-14.04/snapd.postrm index befa073ae4..4626e71f60 100644 --- a/packaging/ubuntu-14.04/snapd.postrm +++ b/packaging/ubuntu-14.04/snapd.postrm @@ -73,6 +73,7 @@ if [ "$1" = "purge" ]; then done # modules rm -f "/etc/modules-load.d/snap.${snap}.conf" + rm -f "/etc/modprobe.d/snap.${snap}.conf" # udev rules find /etc/udev/rules.d -name "*-snap.${snap}.rules" -execdir rm -f "{}" \; # dbus policy files diff --git a/packaging/ubuntu-14.04/source b/packaging/ubuntu-14.04/source index d4219c032f..05552630c6 120000 --- a/packaging/ubuntu-14.04/source +++ b/packaging/ubuntu-14.04/source @@ -1 +1 @@ -../ubuntu-16.04/source/ \ No newline at end of file +../ubuntu-16.04/source \ No newline at end of file diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog index 924a7f1d66..7fe71f481c 100644 --- a/packaging/ubuntu-16.04/changelog +++ b/packaging/ubuntu-16.04/changelog @@ -1,3 +1,383 @@ +snapd (2.54.1) xenial; urgency=medium + + * New upstream release, LP: #1955137 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + + -- Michael Vogt <michael.vogt@ubuntu.com> Mon, 20 Dec 2021 10:06:09 +0100 + +snapd (2.54) xenial; urgency=medium + + * New upstream release, LP: #1955137 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + + -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 17 Dec 2021 15:49:18 +0100 + +snapd (2.53.4) xenial; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + + -- Ian Johnson <ian.johnson@canonical.com> Thu, 02 Dec 2021 17:16:48 -0600 + +snapd (2.53.3) xenial; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + + -- Ian Johnson <ian.johnson@canonical.com> Thu, 02 Dec 2021 11:42:15 -0600 + snapd (2.53.2) xenial; urgency=medium * New upstream release, LP: #1946127 diff --git a/packaging/ubuntu-16.04/rules b/packaging/ubuntu-16.04/rules index 885ae31379..2ba72706b4 100755 --- a/packaging/ubuntu-16.04/rules +++ b/packaging/ubuntu-16.04/rules @@ -164,9 +164,6 @@ override_dh_clean: $(MAKE) -C cmd distclean || true # XXX: hacky^2 (cd c-vendor/squashfuse && rm -f snapfuse && make distclean || true ) - # XXX: drop old mvo5/libseccomp - rm -f ./cmd/snap-seccomp/old_seccomp.go - sed '/mvo5\/libseccomp-golang/d' -i go.mod go.sum vendor/modules.txt || true override_dh_auto_build: # usually done via `go generate` but that is not supported on powerpc diff --git a/packaging/ubuntu-16.04/snapd.postrm b/packaging/ubuntu-16.04/snapd.postrm index 0dba123050..163be54fbb 100644 --- a/packaging/ubuntu-16.04/snapd.postrm +++ b/packaging/ubuntu-16.04/snapd.postrm @@ -85,6 +85,7 @@ if [ "$1" = "purge" ]; then fi # modules rm -f "/etc/modules-load.d/snap.${snap}.conf" + rm -f "/etc/modprobe.d/snap.${snap}.conf" # timer and socket units find /etc/systemd/system -name "snap.${snap}.*.timer" -o -name "snap.${snap}.*.socket" | while read -r f; do systemctl_stop "$(basename "$f")" diff --git a/polkit/pid_start_time.go b/polkit/pid_start_time.go index 93aca0fffb..f1870a233f 100644 --- a/polkit/pid_start_time.go +++ b/polkit/pid_start_time.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build linux // +build linux /* diff --git a/polkit/pid_start_time_test.go b/polkit/pid_start_time_test.go index 7bc1107d1b..dad807bc7a 100644 --- a/polkit/pid_start_time_test.go +++ b/polkit/pid_start_time_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build linux // +build linux /* diff --git a/secboot/encrypt_dummy.go b/secboot/encrypt_dummy.go index 60c9cdd661..6f08781ec4 100644 --- a/secboot/encrypt_dummy.go +++ b/secboot/encrypt_dummy.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build nosecboot // +build nosecboot /* diff --git a/secboot/encrypt_sb.go b/secboot/encrypt_sb.go index 0082b9be67..28136ded12 100644 --- a/secboot/encrypt_sb.go +++ b/secboot/encrypt_sb.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/secboot/encrypt_sb_test.go b/secboot/encrypt_sb_test.go index 105a1da722..3841786736 100644 --- a/secboot/encrypt_sb_test.go +++ b/secboot/encrypt_sb_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/secboot/export_sb_test.go b/secboot/export_sb_test.go index 1d8d8c005a..03a9c4c2e3 100644 --- a/secboot/export_sb_test.go +++ b/secboot/export_sb_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/secboot/secboot_dummy.go b/secboot/secboot_dummy.go index 2036a52d73..013ad6c609 100644 --- a/secboot/secboot_dummy.go +++ b/secboot/secboot_dummy.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build nosecboot // +build nosecboot /* diff --git a/secboot/secboot_hooks.go b/secboot/secboot_hooks.go index 9c7298a8e2..baaa39eee5 100644 --- a/secboot/secboot_hooks.go +++ b/secboot/secboot_hooks.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/secboot/secboot_sb.go b/secboot/secboot_sb.go index 7f68d7a12e..3085275c8f 100644 --- a/secboot/secboot_sb.go +++ b/secboot/secboot_sb.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/secboot/secboot_sb_test.go b/secboot/secboot_sb_test.go index 5e60b6220c..3b0ac83146 100644 --- a/secboot/secboot_sb_test.go +++ b/secboot/secboot_sb_test.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/secboot/secboot_tpm.go b/secboot/secboot_tpm.go index d7fc0c5fee..fb074430bf 100644 --- a/secboot/secboot_tpm.go +++ b/secboot/secboot_tpm.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff --git a/snap/helpers.go b/snap/helpers.go index 0d24abdff4..cacb3074e7 100644 --- a/snap/helpers.go +++ b/snap/helpers.go @@ -20,9 +20,74 @@ package snap import ( + "os/user" + "path/filepath" + "strconv" + "syscall" + + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/snap/naming" ) +var ( + userLookupId = user.LookupId +) + func IsSnapd(snapID string) bool { return snapID == naming.WellKnownSnapID("snapd") } + +// AllUsers returns a list of users, including the root user and all users that +// can be found under /home with a snap directory. +func AllUsers(opts *dirs.SnapDirOptions) ([]*user.User, error) { + ds, err := filepath.Glob(DataHomeGlob(opts)) + if err != nil { + // can't happen? + return nil, err + } + + users := make([]*user.User, 1, len(ds)+1) + root, err := user.LookupId("0") + if err != nil { + return nil, err + } + users[0] = root + seen := make(map[uint32]bool, len(ds)+1) + seen[0] = true + var st syscall.Stat_t + for _, d := range ds { + err := syscall.Stat(d, &st) + if err != nil { + continue + } + if seen[st.Uid] { + continue + } + seen[st.Uid] = true + usr, err := userLookupId(strconv.FormatUint(uint64(st.Uid), 10)) + if err != nil { + // Treat all non-nil errors as user.Unknown{User,Group}Error's, as + // currently Go's handling of returned errno from get{pw,gr}nam_r + // in the cgo implementation of user.Lookup is lacking, and thus + // user.Unknown{User,Group}Error is returned only when errno is 0 + // and the list of users/groups is empty, but as per the man page + // for get{pw,gr}nam_r, there are many other errno's that typical + // systems could return to indicate that the user/group wasn't + // found, however unfortunately the POSIX standard does not actually + // dictate what errno should be used to indicate "user/group not + // found", and so even if Go is more robust, it may not ever be + // fully robust. See from the man page: + // + // > It [POSIX.1-2001] does not call "not found" an error, hence + // > does not specify what value errno might have in this situation. + // > But that makes it impossible to recognize errors. + // + // See upstream Go issue: https://github.com/golang/go/issues/40334 + continue + } else { + users = append(users, usr) + } + } + + return users, nil +} diff --git a/snap/info.go b/snap/info.go index 6bf218ab73..590cd1638f 100644 --- a/snap/info.go +++ b/snap/info.go @@ -303,7 +303,7 @@ type Info struct { Broken string // The information in these fields is ephemeral, available only from the - // store. + // store or when read from a snap file. DownloadInfo Prices map[string]float64 diff --git a/snapdenv/withtestkeys.go b/snapdenv/withtestkeys.go index f02f0a1246..7614ed9efb 100644 --- a/snapdenv/withtestkeys.go +++ b/snapdenv/withtestkeys.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withtestkeys // +build withtestkeys /* diff --git a/snapdtool/info_file.go b/snapdtool/info_file.go index 2780334501..93c5e0730d 100644 --- a/snapdtool/info_file.go +++ b/snapdtool/info_file.go @@ -20,34 +20,55 @@ package snapdtool import ( - "bytes" + "bufio" "fmt" - "io/ioutil" + "os" + "path/filepath" + "strings" ) -// SnapdVersionFromInfoFile returns snapd version read for the -// given info" file, pointed by infoPath. -// The format of the "info" file is a single line with "VERSION=..." -// in it. The file is produced by mkversion.sh and normally installed -// along snapd binary in /usr/lib/snapd. -func SnapdVersionFromInfoFile(infoPath string) (string, error) { - content, err := ioutil.ReadFile(infoPath) +// SnapdVersionFromInfoFile returns the snapd version read from the info file in +// the given dir, as well as any other key/value pairs/flags in the file. +// The format of the "info" file are lines with "KEY=VALUE" with the typical key +// being just VERSION. The file is produced by mkversion.sh and normally +// installed along snapd binary in /usr/lib/snapd. +// Other typical keys in this file include SNAPD_APPARMOR_REEXEC, which +// indicates whether or not the snapd-apparmor binary installed via the +// traditional linux package of snapd supports re-exec into the version in the +// snapd or core snaps. +func SnapdVersionFromInfoFile(dir string) (version string, flags map[string]string, err error) { + infoPath := filepath.Join(dir, "info") + f, err := os.Open(infoPath) if err != nil { - return "", fmt.Errorf("cannot open snapd info file %q: %s", infoPath, err) + return "", nil, fmt.Errorf("cannot open snapd info file %q: %s", infoPath, err) } + defer f.Close() - if !bytes.HasPrefix(content, []byte("VERSION=")) { - idx := bytes.Index(content, []byte("\nVERSION=")) - if idx < 0 { - return "", fmt.Errorf("cannot find snapd version information in %q", content) + flags = map[string]string{} + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "VERSION=") { + version = strings.TrimPrefix(line, "VERSION=") + } else { + keyVal := strings.SplitN(line, "=", 2) + if len(keyVal) != 2 { + // potentially malformed line, just skip it + continue + } + + flags[keyVal[0]] = keyVal[1] } - content = content[idx+1:] } - content = content[8:] - idx := bytes.IndexByte(content, '\n') - if idx > -1 { - content = content[:idx] + + if err := scanner.Err(); err != nil { + return "", nil, fmt.Errorf("error reading snapd info file %q: %v", infoPath, err) + } + + if version == "" { + return "", nil, fmt.Errorf("cannot find snapd version information in file %q", infoPath) } - return string(content), nil + return version, flags, nil } diff --git a/snapdtool/info_file_test.go b/snapdtool/info_file_test.go index 1ce9a5dd23..0f311f79c8 100644 --- a/snapdtool/info_file_test.go +++ b/snapdtool/info_file_test.go @@ -20,6 +20,7 @@ package snapdtool_test import ( + "fmt" "io/ioutil" "path/filepath" @@ -33,8 +34,8 @@ type infoFileSuite struct{} var _ = Suite(&infoFileSuite{}) func (s *infoFileSuite) TestNoVersionFile(c *C) { - _, err := snapdtool.SnapdVersionFromInfoFile("/non-existing-file") - c.Assert(err, ErrorMatches, `cannot open snapd info file "/non-existing-file":.*`) + _, _, err := snapdtool.SnapdVersionFromInfoFile("/non-existing-dir") + c.Assert(err, ErrorMatches, `cannot open snapd info file "/non-existing-dir/info":.*`) } func (s *infoFileSuite) TestNoVersionData(c *C) { @@ -42,8 +43,8 @@ func (s *infoFileSuite) TestNoVersionData(c *C) { infoFile := filepath.Join(top, "info") c.Assert(ioutil.WriteFile(infoFile, []byte("foo"), 0644), IsNil) - _, err := snapdtool.SnapdVersionFromInfoFile(infoFile) - c.Assert(err, ErrorMatches, `cannot find snapd version information in "foo"`) + _, _, err := snapdtool.SnapdVersionFromInfoFile(top) + c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot find snapd version information in file %q`, infoFile)) } func (s *infoFileSuite) TestVersionHappy(c *C) { @@ -51,7 +52,19 @@ func (s *infoFileSuite) TestVersionHappy(c *C) { infoFile := filepath.Join(top, "info") c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=1.2.3"), 0644), IsNil) - ver, err := snapdtool.SnapdVersionFromInfoFile(infoFile) + ver, flags, err := snapdtool.SnapdVersionFromInfoFile(top) c.Assert(err, IsNil) c.Check(ver, Equals, "1.2.3") + c.Assert(flags, HasLen, 0) +} + +func (s *infoFileSuite) TestInfoVersionFlags(c *C) { + top := c.MkDir() + infoFile := filepath.Join(top, "info") + c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=1.2.3\nFOO=BAR"), 0644), IsNil) + + ver, flags, err := snapdtool.SnapdVersionFromInfoFile(top) + c.Assert(err, IsNil) + c.Check(ver, Equals, "1.2.3") + c.Assert(flags, DeepEquals, map[string]string{"FOO": "BAR"}) } diff --git a/snapdtool/tool_linux.go b/snapdtool/tool_linux.go index 986b106bdd..5e8e554d7f 100644 --- a/snapdtool/tool_linux.go +++ b/snapdtool/tool_linux.go @@ -76,8 +76,8 @@ func distroSupportsReExec() bool { // Ensure we do not use older version of snapd, look for info file and ignore // version of core that do not yet have it. func coreSupportsReExec(coreOrSnapdPath string) bool { - infoPath := filepath.Join(coreOrSnapdPath, filepath.Join(dirs.CoreLibExecDir, "info")) - ver, err := SnapdVersionFromInfoFile(infoPath) + infoDir := filepath.Join(coreOrSnapdPath, filepath.Join(dirs.CoreLibExecDir)) + ver, _, err := SnapdVersionFromInfoFile(infoDir) if err != nil { logger.Noticef("%v", err) return false diff --git a/snapdtool/tool_other.go b/snapdtool/tool_other.go index 67c6b0a1b8..454093e380 100644 --- a/snapdtool/tool_other.go +++ b/snapdtool/tool_other.go @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !linux // +build !linux /* diff --git a/spread.yaml b/spread.yaml index daf110af91..944cd29b3d 100644 --- a/spread.yaml +++ b/spread.yaml @@ -128,8 +128,11 @@ backends: - fedora-33-64: workers: 6 + manual: true - fedora-34-64: workers: 6 + - fedora-35-64: + workers: 6 - arch-linux-64: workers: 6 @@ -144,6 +147,7 @@ backends: # unstable systems below - opensuse-15.2-64: workers: 6 + manual: true - opensuse-15.3-64: workers: 6 - opensuse-tumbleweed-64: @@ -785,7 +789,7 @@ suites: _/hosts: _ _/hosts_n_dirs: _ # twisted fails in travis (but not regular spread). - # _/twisted: _ + #_/twisted: _ _/func: _ _/funkyfunc: _ _/funcarg: _ diff --git a/strutil/ctrl16.go b/strutil/ctrl16.go index 4d8a8c1854..425c8913d3 100644 --- a/strutil/ctrl16.go +++ b/strutil/ctrl16.go @@ -1,3 +1,4 @@ +//go:build !go1.7 // +build !go1.7 package strutil diff --git a/strutil/ctrl17.go b/strutil/ctrl17.go index be3c09abcc..e52dec6a4f 100644 --- a/strutil/ctrl17.go +++ b/strutil/ctrl17.go @@ -1,3 +1,4 @@ +//go:build go1.7 // +build go1.7 package strutil diff --git a/systemd/emulation.go b/systemd/emulation.go index 486490f608..9f623a070c 100644 --- a/systemd/emulation.go +++ b/systemd/emulation.go @@ -20,7 +20,6 @@ package systemd import ( - "errors" "fmt" "io" "os" @@ -39,18 +38,24 @@ type emulation struct { rootDir string } -var errNotImplemented = errors.New("not implemented in emulation mode") +type notImplementedError struct { + op string +} + +func (e *notImplementedError) Error() string { + return fmt.Sprintf("%q is not implemented in emulation mode", e.op) +} func (s *emulation) Backend() Backend { return EmulationModeBackend } func (s *emulation) DaemonReload() error { - return errNotImplemented + return ¬ImplementedError{"DaemonReload"} } func (s *emulation) DaemonReexec() error { - return errNotImplemented + return ¬ImplementedError{"DaemonReexec"} } func (s *emulation) Enable(service string) error { @@ -64,59 +69,59 @@ func (s *emulation) Disable(service string) error { } func (s *emulation) Start(service ...string) error { - return errNotImplemented + return ¬ImplementedError{"Start"} } func (s *emulation) StartNoBlock(service ...string) error { - return errNotImplemented + return ¬ImplementedError{"StartNoBlock"} } func (s *emulation) Stop(service string, timeout time.Duration) error { - return errNotImplemented + return ¬ImplementedError{"Stop"} } func (s *emulation) Kill(service, signal, who string) error { - return errNotImplemented + return ¬ImplementedError{"Kill"} } func (s *emulation) Restart(service string, timeout time.Duration) error { - return errNotImplemented + return ¬ImplementedError{"Restart"} } func (s *emulation) ReloadOrRestart(service string) error { - return errNotImplemented + return ¬ImplementedError{"ReloadOrRestart"} } func (s *emulation) RestartAll(service string) error { - return errNotImplemented + return ¬ImplementedError{"RestartAll"} } func (s *emulation) Status(units ...string) ([]*UnitStatus, error) { - return nil, errNotImplemented + return nil, ¬ImplementedError{"Status"} } func (s *emulation) InactiveEnterTimestamp(unit string) (time.Time, error) { - return time.Time{}, errNotImplemented + return time.Time{}, ¬ImplementedError{"InactiveEnterTimestamp"} } func (s *emulation) CurrentMemoryUsage(unit string) (quantity.Size, error) { - return 0, errNotImplemented + return 0, ¬ImplementedError{"CurrentMemoryUsage"} } func (s *emulation) CurrentTasksCount(unit string) (uint64, error) { - return 0, errNotImplemented + return 0, ¬ImplementedError{"CurrentTasksCount"} } func (s *emulation) IsEnabled(service string) (bool, error) { - return false, errNotImplemented + return false, ¬ImplementedError{"IsEnabled"} } func (s *emulation) IsActive(service string) (bool, error) { - return false, errNotImplemented + return false, ¬ImplementedError{"IsActive"} } func (s *emulation) LogReader(services []string, n int, follow bool) (io.ReadCloser, error) { - return nil, errNotImplemented + return nil, fmt.Errorf("LogReader") } func (s *emulation) AddMountUnitFile(snapName, revision, what, where, fstype string) (string, error) { @@ -163,7 +168,7 @@ func (s *emulation) AddMountUnitFile(snapName, revision, what, where, fstype str } func (s *emulation) AddMountUnitFileWithOptions(unitOptions *MountUnitOptions) (string, error) { - return "", errNotImplemented + return "", ¬ImplementedError{"AddMountUnitFileWithOptions"} } func (s *emulation) RemoveMountUnitFile(mountedDir string) error { @@ -197,7 +202,7 @@ func (s *emulation) RemoveMountUnitFile(mountedDir string) error { } func (s *emulation) ListMountUnits(snapName, origin string) ([]string, error) { - return nil, errNotImplemented + return nil, ¬ImplementedError{"ListMountUnits"} } func (s *emulation) Mask(service string) error { @@ -211,9 +216,9 @@ func (s *emulation) Unmask(service string) error { } func (s *emulation) Mount(what, where string, options ...string) error { - return errNotImplemented + return ¬ImplementedError{"Mount"} } func (s *emulation) Umount(whatOrWhere string) error { - return errNotImplemented + return ¬ImplementedError{"Umount"} } diff --git a/tests/completion/data/twisted/this is a file with spaces in it.doc b/tests/completion/data/twisted/this is a file with spaces in it.doc deleted file mode 100644 index e69de29bb2..0000000000 --- a/tests/completion/data/twisted/this is a file with spaces in it.doc +++ /dev/null diff --git a/tests/completion/data/twisted/this isn't.innit b/tests/completion/data/twisted/this isn't.innit deleted file mode 100644 index e69de29bb2..0000000000 --- a/tests/completion/data/twisted/this isn't.innit +++ /dev/null diff --git a/tests/completion/data/twisted/twisted.tar b/tests/completion/data/twisted/twisted.tar Binary files differnew file mode 100644 index 0000000000..62081e2926 --- /dev/null +++ b/tests/completion/data/twisted/twisted.tar diff --git a/tests/completion/twisted.sh b/tests/completion/twisted.sh index 5d52812ce1..4f738f16b9 100644 --- a/tests/completion/twisted.sh +++ b/tests/completion/twisted.sh @@ -1 +1,2 @@ cd "$SPREAD_PATH/$SPREAD_SUITE/data/twisted" +tar -xvf $SPREAD_PATH/$SPREAD_SUITE/data/twisted/twisted.tar diff --git a/tests/core/basic20/task.yaml b/tests/core/basic20/task.yaml index dabbb3e8cd..976946a78b 100644 --- a/tests/core/basic20/task.yaml +++ b/tests/core/basic20/task.yaml @@ -81,3 +81,11 @@ execute: | # check that we have a boot-flags file test -f /run/snapd/boot-flags + + # make sure that loop devices created by snap-bootstrap initramfs-mounts for snaps are readonly + for mount in /run/mnt/base /run/mnt/kernel; do + mountpoint "${mount}" + loop="$(findmnt -o source "${mount}" -n)" + echo "${loop}" | MATCH "/dev/loop[0-9]+" + losetup -O ro -n --raw "${loop}" | MATCH "1" + done diff --git a/tests/core/failover/task.yaml b/tests/core/failover/task.yaml index 13e531cbcb..85af86b635 100644 --- a/tests/core/failover/task.yaml +++ b/tests/core/failover/task.yaml @@ -10,8 +10,8 @@ details: | reboots, one for trying the upgrade and another for rolling back) the installed fundamental snap is the good one and the boot environment variables are correctly set. -# TODO: enable for UC18 ? -systems: [ubuntu-core-16-*] +# TODO: enable for UC20 ? +systems: [ubuntu-core-16-*, ubuntu-core-18-*] # Start early as it takes a long time. priority: 100 @@ -40,7 +40,7 @@ restore: | "$TESTSTOOLS"/boot-state bootenv unset snap_try_kernel debug: | - "$TESTSTOOLS"/boot-state bootenv show + snap debug boot-vars snap list snap changes @@ -61,15 +61,28 @@ execute: | truncate -s 0 "$BUILD_DIR/unpack/initrd.img" } + if os.query is-core18 && [[ "$SPREAD_VARIANT" = "rclocalcrash" ]]; then + # there is no /etc/rc.local on core18 + echo "scenario isn't supported on core18" + exit 0 + fi + #shellcheck source=tests/lib/names.sh . "$TESTSLIB"/names.sh #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB"/snaps.sh + core_name="core" + if os.query is-core18; then + core_name="core18" + elif os.query is-core20; then + core_name="core20" + fi + if [ "$TARGET_SNAP" = kernel ]; then TARGET_SNAP_NAME=$kernel_name else - TARGET_SNAP_NAME=core + TARGET_SNAP_NAME="$core_name" fi if [ "$SPREAD_REBOOT" = 0 ]; then @@ -109,9 +122,14 @@ execute: | fi # check boot env vars - readlink /snap/core/current > failBoot - test "$("$TESTSTOOLS"/boot-state bootenv show snap_"${TARGET_SNAP}")" = "${TARGET_SNAP_NAME}_$(cat prevBoot).snap" - test "$("$TESTSTOOLS"/boot-state bootenv show snap_try_"${TARGET_SNAP}")" = "${TARGET_SNAP_NAME}_$(cat failBoot).snap" + readlink "/snap/$core_name/current" > failBoot + if [ "$TARGET_SNAP" = kernel ]; then + snap debug boot-vars | MATCH "snap_try_kernel=${TARGET_SNAP_NAME}_$(cat failBoot).snap\$" + snap debug boot-vars | MATCH "snap_kernel=${TARGET_SNAP_NAME}_$(cat prevBoot).snap\$" + else + snap debug boot-vars | MATCH "snap_try_core=${TARGET_SNAP_NAME}_$(cat failBoot).snap\$" + snap debug boot-vars | MATCH "snap_core=${TARGET_SNAP_NAME}_$(cat prevBoot).snap\$" + fi REBOOT fi @@ -122,15 +140,19 @@ execute: | retry -n 60 --wait 1 --env TARGET_SNAP_NAME="$TARGET_SNAP_NAME" sh -c 'test $(snap list | awk "/^${TARGET_SNAP_NAME} / {print(\$3)}") = $(cat prevBoot)' # ensure the last install change failed as expected - snap change --last=install | MATCH "cannot finish core installation, there was a rollback across reboot" + snap change --last=install | MATCH "cannot finish $TARGET_SNAP_NAME installation, there was a rollback across reboot" snap change --last=install | MATCH "^Error.*Automatically connect" # and the boot env vars are correctly set echo "Waiting for snapd to clean snap_mode" #shellcheck disable=SC2148 #shellcheck disable=SC2016 - retry -n 200 --wait 1 sh -c 'test -z "$("$TESTSTOOLS"/boot-state bootenv show snap_mode)"' + retry -n 200 --wait 1 sh -c 'snap debug boot-vars | MATCH "snap_mode=\$"' - test "$("$TESTSTOOLS"/boot-state bootenv show snap_"${TARGET_SNAP}")" = "${TARGET_SNAP_NAME}_$(cat prevBoot).snap" - # FIXME: reenable the last check when we reset properly snap_try_{core,kernel} on rollback - # test "$("$TESTSTOOLS"/boot-state bootenv show snap_try_${TARGET_SNAP})" = "" + if [ "$TARGET_SNAP" = kernel ]; then + snap debug boot-vars | MATCH 'snap_try_kernel=$' + snap debug boot-vars | MATCH "snap_kernel=${TARGET_SNAP_NAME}_$(cat prevBoot).snap\$" + else + snap debug boot-vars | MATCH 'snap_try_core=$' + snap debug boot-vars | MATCH "snap_core=${TARGET_SNAP_NAME}_$(cat prevBoot).snap\$" + fi diff --git a/tests/core/fsck-on-boot/task.yaml b/tests/core/fsck-on-boot/task.yaml index 5f0b32fed7..7d351ea7b3 100644 --- a/tests/core/fsck-on-boot/task.yaml +++ b/tests/core/fsck-on-boot/task.yaml @@ -44,7 +44,7 @@ execute: | if mountpoint /run/mnt/snapd >/dev/null; then umount /run/mnt/snapd fi - umount /var/lib/snapd/seed + retry -n 20 --wait 2 sh -c 'umount /var/lib/snapd/seed' umount /run/mnt/ubuntu-seed # Refer to the core 20 gadgets for details: diff --git a/tests/core/kernel-and-base-single-reboot-failover/task.yaml b/tests/core/kernel-and-base-single-reboot-failover/task.yaml new file mode 100644 index 0000000000..13441aaf87 --- /dev/null +++ b/tests/core/kernel-and-base-single-reboot-failover/task.yaml @@ -0,0 +1,117 @@ +summary: Exercises a simultaneous kernel and base refresh with a single reboot + +# TODO make the test work with ubuntu-core-20 +systems: [ubuntu-core-18-*] + +environment: + BLOB_DIR: $(pwd)/fake-store-blobdir + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + + setup_fake_store "$BLOB_DIR" + + readlink /snap/pc-kernel/current > pc-kernel.rev + readlink "/snap/core18/current" > core.rev + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + + if [ "$SPREAD_REBOOT" = 0 ]; then + # break the pc-kernel snap + unsquashfs -d pc-kernel-snap /var/lib/snapd/snaps/pc-kernel_*.snap + truncate -s 0 pc-kernel-snap/initrd.img + + init_fake_refreshes "$BLOB_DIR" pc-kernel --snap-blob "$PWD/pc-kernel-snap" + init_fake_refreshes "$BLOB_DIR" core18 + + # taken from transition_to_recover_mode() + cp /bin/systemctl /tmp/orig-systemctl + mount -o bind "$TESTSLIB/mock-shutdown" /bin/systemctl + tests.cleanup defer umount /bin/systemctl + + snap refresh --no-wait core18 pc-kernel > refresh-change-id + test -n "$(cat refresh-change-id)" + change_id="$(cat refresh-change-id)" + # wait until we observe reboots + # shellcheck disable=SC2016 + retry -n 100 --wait 5 sh -c 'test "$(wc -l < /tmp/mock-shutdown.calls)" -gt "1"' + # stop snapd now to avoid snapd waiting for too long and deciding to + # error out assuming a rollback across reboot + systemctl stop snapd.service snapd.socket + + # both link snaps should be done now, snapd was stopped, so we cannot + # use 'snap change' and we need to inspect the state directly (even if + # snapd was up, it would not respond to API requests as it would be busy + # retrying auto-connect) + snap debug state --change "$change_id" /var/lib/snapd/state.json > tasks.state + # both link snaps are done + MATCH ' Done\s+.*Make snap "pc-kernel" .* available' < tasks.state + MATCH ' Done\s+.*Make snap "core18" .* available' < tasks.state + # auto-connect of the base is in doing and waiting for reboot + MATCH ' Doing\s+.*Automatically connect eligible plugs and slots of snap "core18"' < tasks.state + # auto-connect of the kernel is still queued + MATCH ' Do\s+.*Automatically connect eligible plugs and slots of snap "pc-kernel"' < tasks.state + + snap debug boot-vars > boot-vars.dump + MATCH 'snap_mode=try' < boot-vars.dump + MATCH 'snap_try_core=core18_.*.snap' < boot-vars.dump + MATCH 'snap_try_kernel=pc-kernel_.*.snap' < boot-vars.dump + + # restore shutdown so that spread can reboot the host + tests.cleanup pop + + REBOOT + elif [ "$SPREAD_REBOOT" = 1 ]; then + change_id="$(cat refresh-change-id)" + # we expect the change to have failed due to the kernel not booting + # properly + snap watch "$change_id" || true + snap changes | MATCH "$change_id\s+Error" + snap change "$change_id" > tasks.done + # both link snaps were undone + MATCH 'Undone\s+.*Make snap "pc-kernel" .* available' < tasks.done + MATCH 'Undone\s+.*Make snap "core18" .* available' < tasks.done + + # boot variables should have been cleared + snap debug boot-vars > boot-vars.dump + MATCH 'snap_mode=$' < boot-vars.dump + MATCH 'snap_try_core=$' < boot-vars.dump + MATCH 'snap_try_kernel=$' < boot-vars.dump + + # make sure the system is in stable state, no pending reboots + # XXX systemctl exits with non-0 when in degraded state + (systemctl is-system-running || true) | MATCH '(running|degraded)' + + # we're expecting the old revisions to be back + expecting_kernel="$(cat pc-kernel.rev)" + expecting_core="$(cat core.rev)" + + # verify that current points to old revisions + test "$(readlink /snap/pc-kernel/current)" = "$expecting_kernel" + test "$(readlink /snap/core18/current)" = "$expecting_core" + else + echo "unexpected reboot" + exit 1 + fi diff --git a/tests/core/remodel-base/task.yaml b/tests/core/remodel-base/task.yaml index 9ec5ddafcc..0a4861d727 100644 --- a/tests/core/remodel-base/task.yaml +++ b/tests/core/remodel-base/task.yaml @@ -28,27 +28,35 @@ prepare: | wait_for_device_initialized_change restore: | - if [ "$TRUST_TEST_KEYS" = "false" ]; then - echo "This test needs test keys to be trusted" - exit - fi #shellcheck source=tests/lib/core-config.sh . "$TESTSLIB"/core-config.sh #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh - systemctl stop snapd.service snapd.socket + if [ "$SPREAD_REBOOT" = 0 ]; then + systemctl stop snapd.service snapd.socket - clean_snapd_lib - restore_test_account valid-for-testing - restore_test_model valid-for-testing-pc - restore_core_model + clean_snapd_lib + restore_test_account valid-for-testing + restore_test_model valid-for-testing-pc + restore_core_model - # kick first boot again - systemctl start snapd.service snapd.socket + # kick first boot again + systemctl start snapd.service snapd.socket + + echo "reboot when the system is ready" + for _ in $(seq 30); do + if "$TESTSTOOLS"/journal-state match-log "Waiting for system reboot"; then + break + fi + sleep 1 + done + REBOOT + fi # wait for first boot to be done wait_for_first_boot_change + # extra paranoia because failure to cleanup earlier took us a long time # to find if [ -e /var/snap/$NEW_BASE/current ]; then diff --git a/tests/core/remodel-kernel/task.yaml b/tests/core/remodel-kernel/task.yaml index e40ca24de9..88d50a4b82 100644 --- a/tests/core/remodel-kernel/task.yaml +++ b/tests/core/remodel-kernel/task.yaml @@ -16,7 +16,9 @@ prepare: | . "$TESTSLIB"/systemd.sh # Save the revision of the pc-kernel snap. - readlink /snap/pc-kernel/current > original-revision.txt + readlink /snap/"$OLD_KERNEL"/current > original-revision.txt + # Save the original tracking channel + snap info "$OLD_KERNEL" | awk '/^tracking:/ {print $2}' > original-channel.txt systemctl stop snapd.service snapd.socket @@ -39,16 +41,16 @@ restore: | #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh - # Wait for the final refresh to complete. - snap watch --last=refresh + # Wait for the final refresh to complete (if needed). + snap watch --last=refresh? # Remove all the revisions of pc-kernel that should not be there. - for revno_path in /snap/pc-kernel/*; do + for revno_path in /snap/"$OLD_KERNEL"/*; do revno="$(basename "$revno_path")" if [ "$revno" == current ] || [ "$revno" == "$(cat original-revision.txt)" ]; then continue; fi - snap remove pc-kernel --revision="$revno" + snap remove "$OLD_KERNEL" --revision="$revno" done systemctl stop snapd.service snapd.socket @@ -149,6 +151,6 @@ execute: | snap remove --purge "$NEW_KERNEL" echo "Ensure we are back to the original kernel channel and kernel" - snap refresh --channel="$KERNEL_CHANNEL" "$OLD_KERNEL" + snap refresh --channel="$(cat original-channel.txt)" "$OLD_KERNEL" REBOOT fi diff --git a/tests/core/tmp/task.yaml b/tests/core/tmp/task.yaml new file mode 100644 index 0000000000..72ade5f26b --- /dev/null +++ b/tests/core/tmp/task.yaml @@ -0,0 +1,46 @@ +summary: Check that the tmp.size settings work + +environment: + MOUNTCFG_FILE: /etc/systemd/system/tmp.mount.d/override.conf + +prepare: | + if [ -f "$MOUNTCFG_FILE" ]; then + echo "tmpfs configuration file already present, testbed not clean" + exit 1 + fi + +restore: | + rm -f "$MOUNTCFG_FILE" + +execute: | + echo "Ensure tmp.size is not set initially" + test ! -f "$MOUNTCFG_FILE" + if snap get system tmp.size; then + echo "Error: tmp.size is unexpectedly set" + exit 1 + fi + def_size=$(df --output=size /tmp | tail -1) + + echo "Ensure setting tmp.size works" + for size in 100 200; do + snap set system tmp.size="$size"M + snap get system tmp.size | MATCH "$size"M + df -h --output=size /tmp | MATCH "$size"M + grep '^tmpfs /tmp' /proc/mounts | MATCH nosuid,nodev + MATCH "Options=mode=1777,strictatime,nosuid,nodev,size=${size}M" "$MOUNTCFG_FILE" + # Check that systemd is happy with the generated override.conf + systemctl daemon-reload + done + + echo "Unsetting gets things back to defaults" + snap unset system tmp.size + if snap get system tmp.size; then + echo "Error: tmp.size is unexpectedly set" + exit 1 + fi + test ! -f "$MOUNTCFG_FILE" + cur_size=$(df --output=size /tmp | tail -1) + # For some odd reason, resizing to the default can have a difference with the + # old one of one page (4k), at least in GCE, so we take that into account. + test "$cur_size" -le $((def_size + 4)) && test "$cur_size" -ge $((def_size - 4)) + systemctl daemon-reload diff --git a/tests/lib/fakestore/cmd/fakestore/cmd_make_refreshable.go b/tests/lib/fakestore/cmd/fakestore/cmd_make_refreshable.go index 41a64eee12..97e3deb422 100644 --- a/tests/lib/fakestore/cmd/fakestore/cmd_make_refreshable.go +++ b/tests/lib/fakestore/cmd/fakestore/cmd_make_refreshable.go @@ -20,16 +20,25 @@ package main import ( + "fmt" + "github.com/snapcore/snapd/tests/lib/fakestore/refresh" ) type cmdMakeRefreshable struct { - TopDir string `long:"dir" description:"Directory to be used by the store to keep and serve snaps, <dir>/asserts is used for assertions"` + TopDir string `long:"dir" description:"Directory to be used by the store to keep and serve snaps, <dir>/asserts is used for assertions"` + SnapBlob string `long:"snap-blob" description:"File or directory with new snap revision contents"` + Positional struct { + SnapName string `description:"snap name" positional-arg-name:"snap-name"` + } `positional-args:"yes" required:"1"` } func (x *cmdMakeRefreshable) Execute(args []string) error { + if len(args) > 0 { + return fmt.Errorf("unexpected additional arguments %v", args) + } // setup fake new revisions of snaps for refresh - return refresh.MakeFakeRefreshForSnaps(args, x.TopDir) + return refresh.MakeFakeRefreshForSnaps(x.Positional.SnapName, x.TopDir, x.SnapBlob) } var shortMakeRefreshableHelp = "Makes new versions of the given snaps" diff --git a/tests/lib/fakestore/cmd/fakestore/main.go b/tests/lib/fakestore/cmd/fakestore/main.go index 4e73ea8379..b0b604d526 100644 --- a/tests/lib/fakestore/cmd/fakestore/main.go +++ b/tests/lib/fakestore/cmd/fakestore/main.go @@ -30,7 +30,7 @@ import ( type Options struct{} -var parser = flags.NewParser(&Options{}, flags.Default) +var parser = flags.NewParser(&Options{}, flags.HelpFlag|flags.PassDoubleDash) func main() { if err := logger.SimpleSetup(); err != nil { diff --git a/tests/lib/fakestore/refresh/refresh.go b/tests/lib/fakestore/refresh/refresh.go index e6ee51053d..f4d08e93a9 100644 --- a/tests/lib/fakestore/refresh/refresh.go +++ b/tests/lib/fakestore/refresh/refresh.go @@ -54,7 +54,7 @@ func newAssertsDB(signingPrivKey string) (*asserts.Database, error) { return db, nil } -func MakeFakeRefreshForSnaps(snaps []string, blobDir string) error { +func MakeFakeRefreshForSnaps(snap string, blobDir string, snapBlob string) error { db, err := newAssertsDB(systestkeys.TestStorePrivKey) if err != nil { return err @@ -94,10 +94,8 @@ func MakeFakeRefreshForSnaps(snaps []string, blobDir string) error { f := asserts.NewFetcher(db, retrieve, save) - for _, snap := range snaps { - if err := makeFakeRefreshForSnap(snap, blobDir, db, f); err != nil { - return err - } + if err := makeFakeRefreshForSnap(snap, blobDir, snapBlob, db, f); err != nil { + return err } return nil } @@ -113,7 +111,7 @@ func writeAssert(a asserts.Assertion, targetDir string) (string, error) { return p, err } -func makeFakeRefreshForSnap(snap, targetDir string, db *asserts.Database, f asserts.Fetcher) error { +func makeFakeRefreshForSnap(snap, targetDir, snapBlob string, db *asserts.Database, f asserts.Fetcher) error { // make a fake update snap in /var/tmp (which is not a tempfs) fakeUpdateDir, err := ioutil.TempDir("/var/tmp", "snap-build-") if err != nil { @@ -130,9 +128,28 @@ func makeFakeRefreshForSnap(snap, targetDir string, db *asserts.Database, f asse } defer exec.Command("sudo", "rm", "-rf", fakeUpdateDir) - origInfo, err := copySnap(snap, fakeUpdateDir) + origInfo, err := getOrigInfo(snap) if err != nil { - return fmt.Errorf("copying snap: %v", err) + return err + } + if snapBlob != "" { + fi, err := os.Stat(snapBlob) + if err != nil { + return err + } + if fi.IsDir() { + if err := copyDir(snapBlob, fakeUpdateDir); err != nil { + return fmt.Errorf("copying snap blob dir: %v", err) + } + } else { + if err := unpackSnap(snapBlob, fakeUpdateDir); err != nil { + return fmt.Errorf("unpacking snap blob: %v", err) + } + } + } else { + if err := copySnap(snap, fakeUpdateDir); err != nil { + return fmt.Errorf("copying snap: %v", err) + } } err = copySnapAsserts(origInfo, f) @@ -172,42 +189,65 @@ type info struct { size uint64 } -func copySnap(snapName, targetDir string) (*info, error) { - baseDir := filepath.Join(dirs.SnapMountDir, snapName) - if _, err := os.Stat(baseDir); err != nil { +func getOrigInfo(snapName string) (*info, error) { + origRevision, err := currentRevision(snapName) + if err != nil { return nil, err } - sourceDir := filepath.Join(baseDir, "current") - files, err := filepath.Glob(filepath.Join(sourceDir, "*")) + rev, err := snap.ParseRevision(origRevision) if err != nil { return nil, err } - revnoDir, err := filepath.EvalSymlinks(sourceDir) + place := snap.MinimalPlaceInfo(snapName, rev) + origDigest, origSize, err := asserts.SnapFileSHA3_384(place.MountFile()) if err != nil { return nil, err } + + return &info{revision: origRevision, size: origSize, digest: origDigest}, nil +} + +func currentRevision(snapName string) (string, error) { + baseDir := filepath.Join(dirs.SnapMountDir, snapName) + if _, err := os.Stat(baseDir); err != nil { + return "", err + } + sourceDir := filepath.Join(baseDir, "current") + revnoDir, err := filepath.EvalSymlinks(sourceDir) + if err != nil { + return "", err + } origRevision := filepath.Base(revnoDir) + return origRevision, nil +} + +func copyDir(sourceDir, targetDir string) error { + files, err := filepath.Glob(filepath.Join(sourceDir, "*")) + if err != nil { + return err + } for _, m := range files { if err = exec.Command("sudo", "cp", "-a", m, targetDir).Run(); err != nil { - return nil, err + return err } } + return nil +} - rev, err := snap.ParseRevision(origRevision) - if err != nil { - return nil, err - } - - place := snap.MinimalPlaceInfo(snapName, rev) - origDigest, origSize, err := asserts.SnapFileSHA3_384(place.MountFile()) - if err != nil { - return nil, err +func copySnap(snapName, targetDir string) error { + baseDir := filepath.Join(dirs.SnapMountDir, snapName) + if _, err := os.Stat(baseDir); err != nil { + return err } + sourceDir := filepath.Join(baseDir, "current") + return copyDir(sourceDir, targetDir) +} - return &info{revision: origRevision, size: origSize, digest: origDigest}, nil +func unpackSnap(snapBlob, targetDir string) error { + return exec.Command("sudo", "unsquashfs", "-d", targetDir, "-f", snapBlob).Run() } func buildSnap(snapDir, targetDir string) (*info, error) { diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh index 3a6a81cd7d..db8faeea75 100644 --- a/tests/lib/nested.sh +++ b/tests/lib/nested.sh @@ -1437,7 +1437,7 @@ nested_fetch_spread() { mkdir -p "$NESTED_WORK_DIR" curl -s https://storage.googleapis.com/snapd-spread-tests/spread/spread-amd64.tar.gz | tar -xz -C "$NESTED_WORK_DIR" # make sure spread really exists - test -x "$NESTED_WORK_DIR/spread" + test -x "$NESTED_WORK_DIR/spread" fi echo "$NESTED_WORK_DIR/spread" } @@ -1451,7 +1451,21 @@ nested_build_seed_cdrom() { local ORIG_DIR=$PWD - pushd "$SEED_DIR" || return 1 + pushd "$SEED_DIR" || return 1 genisoimage -output "$ORIG_DIR/$SEED_NAME" -volid "$LABEL" -joliet -rock "$@" - popd || return 1 + popd || return 1 +} + +nested_wait_for_device_initialized_change() { + local retry=60 + local wait=1 + + while ! nested_exec "snap changes" | MATCH "Done.*Initialize device"; do + retry=$(( retry - 1 )) + if [ $retry -le 0 ]; then + echo "Timed out waiting for device to be fully initialized. Aborting!" + return 1 + fi + sleep "$wait" + done } diff --git a/tests/lib/pkgdb.sh b/tests/lib/pkgdb.sh index 44ed797b45..b7048ab509 100755 --- a/tests/lib/pkgdb.sh +++ b/tests/lib/pkgdb.sh @@ -530,7 +530,7 @@ pkg_dependencies_ubuntu_classic(){ echo " avahi-daemon cups - dbus-x11 + dbus-user-session fontconfig gnome-keyring jq @@ -553,7 +553,6 @@ pkg_dependencies_ubuntu_classic(){ ;; ubuntu-16.04-64) echo " - dbus-user-session evolution-data-server fwupd gccgo-6 diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh index 37ea84bb7b..1f5ae355c0 100755 --- a/tests/lib/prepare.sh +++ b/tests/lib/prepare.sh @@ -579,18 +579,18 @@ uc20_build_initramfs_kernel_snap() { sed -i -e 's/set -e/set -ex/' "$skeletondir/main/usr/lib/the-tool" # also save the time before snap-bootstrap runs sed -i -e "s@/usr/lib/snapd/snap-bootstrap@beforeDate=\$(date --utc \'+%s\'); /usr/lib/snapd/snap-bootstrap@" "$skeletondir/main/usr/lib/the-tool" - { - echo "" - echo "if test -d /run/mnt/data/system-data; then touch /run/mnt/data/system-data/the-tool-ran; fi" - # also copy the time for the clock-epoch to system-data, this is - # used by a specific test but doesn't hurt anything to do this for - # all tests - echo "mode=\$(grep -Eo 'snapd_recovery_mode=([a-z]+)' /proc/cmdline)" - echo "mode=\${mode##snapd_recovery_mode=}" - echo "stat -c '%Y' /usr/lib/clock-epoch >> /run/mnt/ubuntu-seed/\${mode}-clock-epoch" - echo "echo \"\$beforeDate\" > /run/mnt/ubuntu-seed/\${mode}-before-snap-bootstrap-date" - echo "date --utc '+%s' > /run/mnt/ubuntu-seed/\${mode}-after-snap-bootstrap-date" - } >> "$skeletondir/main/usr/lib/the-tool" + cat >> "$skeletondir/main/usr/lib/the-tool" <<'EOF' + if test -d /run/mnt/data/system-data; then touch /run/mnt/data/system-data/the-tool-ran; fi + # also copy the time for the clock-epoch to system-data, this is + # used by a specific test but doesn't hurt anything to do this for + # all tests + mode=$(grep -Eo 'snapd_recovery_mode=([a-z]+)' /proc/cmdline) + mode=${mode##snapd_recovery_mode=} + mkdir -p /run/mnt/ubuntu-seed/test + stat -c '%Y' /usr/lib/clock-epoch >> /run/mnt/ubuntu-seed/test/${mode}-clock-epoch + echo "$beforeDate" > /run/mnt/ubuntu-seed/test/${mode}-before-snap-bootstrap-date + date --utc '+%s' > /run/mnt/ubuntu-seed/test/${mode}-after-snap-bootstrap-date +EOF if [ "$injectKernelPanic" = "true" ]; then # add a kernel panic to the end of the-tool execution @@ -1022,13 +1022,13 @@ EOF # mount it so we can use it now mount "/dev/mapper/${dev}p${LOOP_PARTITION}" /mnt - mkdir -p /mnt/user-data/ # copy over everything from gopath to user-data, exclude: # - VCS files # - built debs # - golang archive files and built packages dir # - govendor .cache directory and the binary, if os.query is-core16 || os.query is-core18; then + mkdir -p /mnt/user-data/ # we need to include "core" here because -C option says to ignore # files the way CVS(?!) does, so it ignores files named "core" which # are core dumps, but we have a test suite named "core", so including diff --git a/tests/lib/snaps/store/test-snapd-daemon-user/src/setregid32.c b/tests/lib/snaps/store/test-snapd-daemon-user/src/setregid32.c index d6bcd517c5..da2cc09bae 120000 --- a/tests/lib/snaps/store/test-snapd-daemon-user/src/setregid32.c +++ b/tests/lib/snaps/store/test-snapd-daemon-user/src/setregid32.c @@ -1 +1 @@ -./setregid.c \ No newline at end of file +setregid.c \ No newline at end of file diff --git a/tests/lib/snaps/test-snapd-mount-control/bin/cmd b/tests/lib/snaps/test-snapd-mount-control/bin/cmd new file mode 100755 index 0000000000..e55f49a5a0 --- /dev/null +++ b/tests/lib/snaps/test-snapd-mount-control/bin/cmd @@ -0,0 +1,6 @@ +#!/bin/sh +PS1='$ ' +command="$1" +shift + +exec "$command" "$@" diff --git a/tests/lib/snaps/test-snapd-mount-control/meta/snap.yaml b/tests/lib/snaps/test-snapd-mount-control/meta/snap.yaml new file mode 100644 index 0000000000..554ce36cd5 --- /dev/null +++ b/tests/lib/snaps/test-snapd-mount-control/meta/snap.yaml @@ -0,0 +1,23 @@ +name: test-snapd-mount-control +version: 1.0 +apps: + cmd: + command: bin/cmd +plugs: + mntctl: + interface: mount-control + mount: + - what: /usr/** + where: $SNAP_COMMON/** + options: [rw, bind] + - what: /var/tmp/** + where: $SNAP_COMMON/** + options: [rw, bind] + - what: /dev/sd* + where: /media/** + type: [ext2, ext3, ext4] + options: [rw, sync] + - what: none + where: $SNAP_COMMON/** + type: [tmpfs] + options: [rw] diff --git a/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml b/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml index 073394df15..7a087a8207 100644 --- a/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml +++ b/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml @@ -212,6 +212,9 @@ apps: kernel-module-control: command: bin/run plugs: [ kernel-module-control ] + kernel-module-load: + command: bin/run + plugs: [ kernel-module-load ] kernel-module-observe: command: bin/run plugs: [ kernel-module-observe ] @@ -266,6 +269,9 @@ apps: modem-manager: command: bin/run plugs: [ modem-manager ] + mount-control: + command: bin/run + plugs: [ mount-control ] mount-observe: command: bin/run plugs: [ mount-observe ] @@ -398,6 +404,9 @@ apps: sd-control: command: bin/run plugs: [ sd-control ] + shared-memory: + command: bin/run + plugs: [ shared-memory ] ssh-keys: command: bin/run plugs: [ ssh-keys ] @@ -496,6 +505,17 @@ plugs: interface: dbus bus: system name: test.system + kernel-module-load: + interface: kernel-module-load + modules: + - name: mymodule + load: denied + mount-control: + interface: mount-control + mount: + - what: /dev/sda1 + where: /media/myfiles + options: [rw] system-files: interface: system-files read: [/file1] diff --git a/tests/lib/tools/tests.invariant b/tests/lib/tools/tests.invariant index 56fa1945f3..846e1b5e6e 100755 --- a/tests/lib/tools/tests.invariant +++ b/tests/lib/tools/tests.invariant @@ -9,6 +9,7 @@ show_help() { echo " lxcfs-mounted: /var/lib/lxcfs is a mount point" echo " stray-dbus-daemon: at most one dbus-daemon is running" echo " leftover-defer-sh: defer.sh must not be left over by tests" + echo " broken-snaps: snaps must not be left around that are in a broken state" } if [ $# -eq 0 ]; then @@ -81,7 +82,7 @@ check_stray_dbus_daemon() { ( skipped_system=0 skipped_root_session=0 - for pid in $(pgrep dbus-daemon); do + for pid in $(pgrep -x dbus-daemon); do cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline")" # Ignore one dbus-daemon responsible for the system bus. if echo "$cmdline" | grep -q 'dbus-daemon --system' && [ "$skipped_system" -eq 0 ]; then @@ -122,6 +123,21 @@ check_leftover_defer_sh() { fi } +check_broken_snaps() { + n="$1" # invariant name + ( + # fist column is the snap name, revision is 3rd + snap list --all | awk '/,?broken,?/ {print $1,$3}' | while read -r name rev; do + echo "snap $name ($rev) is broken" + done + ) > "$TESTSTMP/tests.invariant.$n" + if [ -s "$TESTSTMP/tests.invariant.$n" ]; then + echo "tests.invariant: broken snaps" >&2 + cat "$TESTSTMP/tests.invariant.$n" >&2 + return 1 + fi +} + check_invariant() { case "$1" in root-files-in-home) @@ -139,6 +155,9 @@ check_invariant() { leftover-defer-sh) check_leftover_defer_sh "$1" ;; + broken-snaps) + check_broken_snaps "$1" + ;; *) echo "tests.invariant: unknown invariant $1" >&2 exit 1 @@ -147,7 +166,7 @@ check_invariant() { } main() { - ALL_INVARIANTS="root-files-in-home crashed-snap-confine lxcfs-mounted stray-dbus-daemon leftover-defer-sh" + ALL_INVARIANTS="root-files-in-home crashed-snap-confine lxcfs-mounted stray-dbus-daemon leftover-defer-sh broken-snaps" case "$action" in check) diff --git a/tests/main/cgroup-devices-v1/task.yaml b/tests/main/cgroup-devices-v1/task.yaml index 88976c9824..e316ea56dc 100644 --- a/tests/main/cgroup-devices-v1/task.yaml +++ b/tests/main/cgroup-devices-v1/task.yaml @@ -1,6 +1,6 @@ summary: measuring basic properties of device cgroup # Disable the test on all systems that boot with cgroup v2 -systems: [ -fedora-33-*, -fedora-34-*, -debian-11-*, -debian-sid-*, -arch-*, -opensuse-tumbleweed-*, -ubuntu-21.10-*] +systems: [ -fedora-33-*, -fedora-34-*, -fedora-35-*, -debian-11-*, -debian-sid-*, -arch-*, -opensuse-tumbleweed-*, -ubuntu-21.10-*] execute: ./task.sh diff --git a/tests/main/cgroup-freezer/task.yaml b/tests/main/cgroup-freezer/task.yaml index 904c702dd2..7d1c88fee3 100644 --- a/tests/main/cgroup-freezer/task.yaml +++ b/tests/main/cgroup-freezer/task.yaml @@ -5,7 +5,7 @@ details: | placed into the appropriate hierarchy under the freezer cgroup. # Disable the test on all systems that boot with cgroup v2 -systems: [ -fedora-33-*, -fedora-34-*, -debian-11-*, -debian-sid-*, -arch-*, -opensuse-tumbleweed-*, -ubuntu-21.10-*] +systems: [ -fedora-33-*, -fedora-34-*, -fedora-35-*, -debian-11-*, -debian-sid-*, -arch-*, -opensuse-tumbleweed-*, -ubuntu-21.10-*] prepare: | "$TESTSTOOLS"/snaps-state install-local test-snapd-sh diff --git a/tests/main/generic-unregister/task.yaml b/tests/main/generic-unregister/task.yaml index 4b44bf394c..25913dfc62 100644 --- a/tests/main/generic-unregister/task.yaml +++ b/tests/main/generic-unregister/task.yaml @@ -4,6 +4,10 @@ summary: | # ubuntu-14.04: curl does not have --unix-socket option systems: [-ubuntu-core-*, -ubuntu-14.04-*] +environment: + UNTIL_REBOOT/rereg: false + UNTIL_REBOOT/until_reboot: true + prepare: | systemctl stop snapd.service snapd.socket cp /var/lib/snapd/state.json state.json.bak @@ -13,6 +17,7 @@ prepare: | restore: | systemctl stop snapd.service snapd.socket + rm -f /var/lib/snapd/device/private-keys-v1/* cp key/* /var/lib/snapd/device/private-keys-v1/ cp state.json.bak /var/lib/snapd/state.json rm -f /run/snapd/noregister @@ -34,16 +39,24 @@ execute: | keyfile=(/var/lib/snapd/device/private-keys-v1/*) test -f "${keyfile[0]}" - curl --data '{"action":"forget","no-registration-until-reboot":true}' --unix-socket /run/snapd.socket http://localhost/v2/model/serial - - test -f /run/snapd/noregister + curl --data '{"action":"forget","no-registration-until-reboot":'${UNTIL_REBOOT}'}' --unix-socket /run/snapd.socket http://localhost/v2/model/serial snap model --serial 2>&1|MATCH "error: device not registered yet" - not test -e "${keyfile[0]}" - systemctl restart snapd.service + if [ "${UNTIL_REBOOT}" = "true" ] ; then + test -f /run/snapd/noregister + systemctl restart snapd.service + snap model --serial 2>&1|MATCH "error: device not registered yet" + else + not test -e /run/snapd/noregister + snap debug ensure-state-soon + retry --wait 2 -n 120 sh -c 'snap model --serial 2>&1|NOMATCH "error: device not registered yet"' + fi - snap model --serial 2>&1|MATCH "error: device not registered yet" snap find pc - NOMATCH '"session-macaroon":"[^"]' < /var/lib/snapd/state.json + if [ "${UNTIL_REBOOT}" = "true" ] ; then + NOMATCH '"session-macaroon":"[^"]' < /var/lib/snapd/state.json + else + MATCH '"session-macaroon":"[^"]' < /var/lib/snapd/state.json + fi diff --git a/tests/main/interfaces-calendar-service/task.yaml b/tests/main/interfaces-calendar-service/task.yaml index 252a12d988..c2907dc569 100644 --- a/tests/main/interfaces-calendar-service/task.yaml +++ b/tests/main/interfaces-calendar-service/task.yaml @@ -25,6 +25,7 @@ systems: - -debian-sid-* - -fedora-33-* # test-snapd-eds is incompatible with eds version shipped with the distro - -fedora-34-* # test-snapd-eds is incompatible with eds version shipped with the distro + - -fedora-35-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-15.2-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-15.3-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-tumbleweed-* # test-snapd-eds is incompatible with eds version shipped with the distro diff --git a/tests/main/interfaces-contacts-service/task.yaml b/tests/main/interfaces-contacts-service/task.yaml index 3482985236..b65c876164 100644 --- a/tests/main/interfaces-contacts-service/task.yaml +++ b/tests/main/interfaces-contacts-service/task.yaml @@ -19,6 +19,7 @@ systems: - -debian-sid-* - -fedora-33-* # test-snapd-eds is incompatible with eds version shipped with the distro - -fedora-34-* # test-snapd-eds is incompatible with eds version shipped with the distro + - -fedora-35-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-15.2-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-15.3-* # test-snapd-eds is incompatible with eds version shipped with the distro - -opensuse-tumbleweed-* # test-snapd-eds is incompatible with eds version shipped with the distro diff --git a/tests/main/interfaces-kernel-module-load/task.yaml b/tests/main/interfaces-kernel-module-load/task.yaml new file mode 100644 index 0000000000..5c83e773c6 --- /dev/null +++ b/tests/main/interfaces-kernel-module-load/task.yaml @@ -0,0 +1,58 @@ +summary: Ensure that the kernel-module-load interface works. + +details: | + The kernel-module-load interface allows to statically control kernel module + loading in a way that can be constrained via snap-declaration. + +systems: + - ubuntu-core-*-arm-* # XXX: fails with a kill-timeout + +environment: + SNAP_NAME: test-snapd-kernel-module-load + +prepare: | + "$TESTSTOOLS"/snaps-state install-local $SNAP_NAME + +restore: | + echo "Ensure snap is removed even if something goes wrong" + snap remove "$SNAP_NAME" + +execute: | + echo "When the interface is connected" + snap connect "$SNAP_NAME:kernel-module-load" + + echo "Then the kernel modules are configured" + MODPROBE_CONF="/etc/modprobe.d/snap.$SNAP_NAME.conf" + MATCH "blacklist mymodule" < "$MODPROBE_CONF" + MATCH "blacklist other_module" < "$MODPROBE_CONF" + MATCH "options parport_pc io=0x3bc,0x278 irq=none" < "$MODPROBE_CONF" + NOMATCH "blacklist parport_pc" < "$MODPROBE_CONF" + NOMATCH "pcspkr" < "$MODPROBE_CONF" + + echo "And modules are configured to be auto-loaded" + MODULES_LOAD_CONF="/etc/modules-load.d/snap.$SNAP_NAME.conf" + MATCH "parport_pc" < "$MODULES_LOAD_CONF" + MATCH "pcspkr" < "$MODULES_LOAD_CONF" + NOMATCH "mymodule" < "$MODULES_LOAD_CONF" + + echo "Disconnect the interface" + snap disconnect "$SNAP_NAME:kernel-module-load" + + echo "and verify that module configuration files are gone" + test ! -f "$MODPROBE_CONF" + test ! -f "$MODULES_LOAD_CONF" + + # Now we want to verify that removing the snap does not leave any leftovers + echo "Reconnect the interface" + snap connect "$SNAP_NAME:kernel-module-load" + + echo "Configuration files have been recreated" + test -f "$MODPROBE_CONF" + test -f "$MODULES_LOAD_CONF" + + echo "Uninstall the snap" + snap remove "$SNAP_NAME" + + echo "verify that module configuration files are gone" + test ! -f "$MODPROBE_CONF" + test ! -f "$MODULES_LOAD_CONF" diff --git a/tests/main/interfaces-kernel-module-load/test-snapd-kernel-module-load/meta/snap.yaml b/tests/main/interfaces-kernel-module-load/test-snapd-kernel-module-load/meta/snap.yaml new file mode 100644 index 0000000000..fea755ad61 --- /dev/null +++ b/tests/main/interfaces-kernel-module-load/test-snapd-kernel-module-load/meta/snap.yaml @@ -0,0 +1,17 @@ +name: test-snapd-kernel-module-load +summary: A no-strings-attached, no-fuss shell for writing tests +version: 1.0 + +plugs: + kernel-module-load: + interface: kernel-module-load + modules: + - name: mymodule + load: denied + - name: parport_pc + load: on-boot + options: io=0x3bc,0x278 irq=none + - name: other_module + load: denied + - name: pcspkr + load: on-boot diff --git a/tests/main/interfaces-many-core-provided/task.yaml b/tests/main/interfaces-many-core-provided/task.yaml index cf5a26d586..42588055e9 100644 --- a/tests/main/interfaces-many-core-provided/task.yaml +++ b/tests/main/interfaces-many-core-provided/task.yaml @@ -90,6 +90,12 @@ execute: | continue fi + if [ "$plug_iface" = "$CONSUMER_SNAP:mount-control" ] && os.query is-trusty ; then + # systemd version is too old, skipping + snap connect "$plug_iface" "$slot_iface" 2>&1 | MATCH "systemd version 204 is too old \\(expected at least 209\\)" + continue + fi + # The netlink-audit interface adds the `audit_read` capability to the # AppArmor profile, but that's not supported on some older systems if [ "$plug_iface" = "$CONSUMER_SNAP:netlink-audit" ] && os.query is-trusty; then diff --git a/tests/main/interfaces-mount-control/task.yaml b/tests/main/interfaces-mount-control/task.yaml new file mode 100644 index 0000000000..9f831bc70e --- /dev/null +++ b/tests/main/interfaces-mount-control/task.yaml @@ -0,0 +1,97 @@ +summary: Test for the mount-control interface + +environment: + MOUNT_SRC: /var/tmp/test-snapd-mount-control + SNAP_COMMON: /var/snap/test-snapd-mount-control/common + SNAP_NAME: test-snapd-mount-control + MOUNT_DEST: $SNAP_COMMON/target + +prepare: | + mkdir -p "$MOUNT_SRC/dir1" + echo "Something" > "$MOUNT_SRC/file1" + +restore: | + rm connect_error.log + rm -rf "$MOUNT_SRC" + +execute: | + echo "First verify that a snap with a malicious manifest cannot be connected" + "$TESTSTOOLS"/snaps-state install-local test-mount-control-invalid + snap connect test-mount-control-invalid:mntctl 2> connect_error.log || true + if os.query is-trusty; then + echo "On Trusty, we should fail anyway due to systemd being too old" + MATCH "systemd version 204 is too old" < connect_error.log + exit 0 + fi + + MATCH 'mount-control "where" pattern is not clean' < connect_error.log + + echo "Installing the test snap" + + "$TESTSTOOLS"/snaps-state install-local "${SNAP_NAME}" + + echo "Connecting the mount-control interface" + snap connect "${SNAP_NAME}":mntctl + + echo "Verify that the snap can perform a mount" + mkdir -p "$MOUNT_DEST" + "${SNAP_NAME}".cmd mount -o bind,rw "$MOUNT_SRC" "$MOUNT_DEST" + + echo "Verify that the mount has been performed" + "${SNAP_NAME}".cmd grep "$MOUNT_DEST" /proc/self/mountinfo + + echo "and that it's only in the snap's namespace" + NOMATCH "$MOUNT_DEST" < /proc/self/mountinfo + + echo "Ensure that the mounted files are visible" + "${SNAP_NAME}".cmd test -e "$MOUNT_DEST/file1" + + echo "Unmount via the system command umount(8)" + "${SNAP_NAME}".cmd umount "$MOUNT_DEST" + if "${SNAP_NAME}".cmd grep "$MOUNT_DEST" /proc/self/mountinfo; then + echo "Unmount failed" + exit 1 + fi + "${SNAP_NAME}".cmd test "!" -e "$MOUNT_DEST/file1" + + echo "Verify that a mount with a specific FS type can be created" + "${SNAP_NAME}".cmd mount -o rw -t tmpfs none "$MOUNT_DEST" + "${SNAP_NAME}".cmd grep "$MOUNT_DEST.*tmpfs" /proc/self/mountinfo + "${SNAP_NAME}".cmd umount "$MOUNT_DEST" + + if [ "$(snap debug confinement)" = partial ] ; then + echo "Early exit on systems where strict confinement does not work" + exit 0 + fi + + if os.query is-opensuse && ! os.query is-opensuse-tumbleweed; then + echo "Early exit in OpenSUSE as confinement is disabled" + exit 0 + fi + + echo "Verify that a mount not matching the allowed pattern will fail" + if "${SNAP_NAME}".cmd mount -o bind,rw "$MOUNT_SRC" "/tmp/"; then + echo "Mount succeeded despite not matching the allowed pattern" + exit 1 + fi + + echo "Verify that a mount not matching the allowed options will fail" + if "${SNAP_NAME}".cmd mount -o sync "$MOUNT_SRC" "$MOUNT_DEST"; then + echo "Mount succeeded despite not matching the allowed options" + exit 1 + fi + + echo "Verify that a mount not matching the allowed FS type will fail" + mkdir -p /media/somedir + if "${SNAP_NAME}".cmd mount -t debugfs "/dev/sda" "/media/somedir"; then + echo "Mount succeeded despite not matching the allowed FS type" + exit 1 + fi + journalctl -t audit | grep 'fstype="debugfs"' | MATCH 'info="failed type match"' + rmdir /media/somedir + + echo "Verify that a maliciously crafted path cannot bypass the allowed pattern" + if "${SNAP_NAME}".cmd mount -o bind,rw "$MOUNT_SRC" "$SNAP_COMMON/.."; then + echo "Malicious pattern was not blocked" + exit 1 + fi diff --git a/tests/main/interfaces-mount-control/test-mount-control-invalid/bin/cmd b/tests/main/interfaces-mount-control/test-mount-control-invalid/bin/cmd new file mode 100755 index 0000000000..214eb4c775 --- /dev/null +++ b/tests/main/interfaces-mount-control/test-mount-control-invalid/bin/cmd @@ -0,0 +1,3 @@ +#!/bin/sh + +exec "$@" diff --git a/tests/main/interfaces-mount-control/test-mount-control-invalid/meta/snap.yaml b/tests/main/interfaces-mount-control/test-mount-control-invalid/meta/snap.yaml new file mode 100644 index 0000000000..7f7da300fe --- /dev/null +++ b/tests/main/interfaces-mount-control/test-mount-control-invalid/meta/snap.yaml @@ -0,0 +1,16 @@ +name: test-mount-control-invalid +version: 1.0 +apps: + cmd: + command: bin/cmd +plugs: + mntctl: + interface: mount-control + mount: + - what: /usr/** + where: $SNAP_COMMON/** + options: [rw, bind] + - what: /var/tmp/** + where: /media/../** + options: [rw, bind] + diff --git a/tests/main/interfaces-shared-memory/shm-plug/bin/cmd b/tests/main/interfaces-shared-memory/shm-plug/bin/cmd new file mode 100755 index 0000000000..ee708187ee --- /dev/null +++ b/tests/main/interfaces-shared-memory/shm-plug/bin/cmd @@ -0,0 +1,2 @@ +#! /bin/sh +exec "$@" diff --git a/tests/main/interfaces-shared-memory/shm-plug/meta/snap.yaml b/tests/main/interfaces-shared-memory/shm-plug/meta/snap.yaml new file mode 100644 index 0000000000..f648e7db77 --- /dev/null +++ b/tests/main/interfaces-shared-memory/shm-plug/meta/snap.yaml @@ -0,0 +1,10 @@ +name: shm-plug +version: 1.0 +apps: + cmd: + command: bin/cmd + plugs: [shmem] +plugs: + shmem: + interface: shared-memory + shared-memory: super-foo diff --git a/tests/main/interfaces-shared-memory/shm-slot/bin/cmd b/tests/main/interfaces-shared-memory/shm-slot/bin/cmd new file mode 100755 index 0000000000..ee708187ee --- /dev/null +++ b/tests/main/interfaces-shared-memory/shm-slot/bin/cmd @@ -0,0 +1,2 @@ +#! /bin/sh +exec "$@" diff --git a/tests/main/interfaces-shared-memory/shm-slot/meta/snap.yaml b/tests/main/interfaces-shared-memory/shm-slot/meta/snap.yaml new file mode 100644 index 0000000000..60d740122e --- /dev/null +++ b/tests/main/interfaces-shared-memory/shm-slot/meta/snap.yaml @@ -0,0 +1,12 @@ +name: shm-slot +version: 1.0 +apps: + cmd: + command: bin/cmd + slots: [shmem] +slots: + shmem: + interface: shared-memory + shared-memory: super-foo + write: [writable-bar] + read: [readable-foo] diff --git a/tests/main/interfaces-shared-memory/task.yaml b/tests/main/interfaces-shared-memory/task.yaml new file mode 100644 index 0000000000..7a833c8e9e --- /dev/null +++ b/tests/main/interfaces-shared-memory/task.yaml @@ -0,0 +1,74 @@ +summary: Ensure that the shared-memory interface works. + +details: | + The shared-memory interface allows two snaps to share a POSIX shared memory + object declared in the slot of the provider snap. + +prepare: | + "$TESTSTOOLS"/snaps-state install-local shm-slot + "$TESTSTOOLS"/snaps-state install-local shm-plug + +execute: | + echo "When the interface is connected" + snap connect shm-plug:shmem shm-slot:shmem + + # Test writable SHM areas + + echo "Verify that the slot snap can create a writable SHM, and plug can read it" + shm-slot.cmd sh -c 'echo "writable area" > /dev/shm/writable-bar' + shm-plug.cmd cat /dev/shm/writable-bar | MATCH "writable area" + + echo "Plug can also write to it" + shm-plug.cmd sh -c 'echo "client can also write" > /dev/shm/writable-bar' + shm-slot.cmd cat /dev/shm/writable-bar | MATCH "client can also write" + + echo "And vice-versa: plug creates, slot reads" + shm-slot.cmd rm /dev/shm/writable-bar + shm-plug.cmd sh -c 'echo "another test" > /dev/shm/writable-bar' + shm-slot.cmd cat /dev/shm/writable-bar | MATCH "another test" + + # Test read-only SHM areas + + echo "Verify that the slot snap can create a readable SHM, and plug can read it" + shm-slot.cmd sh -c 'echo "read-only area" > /dev/shm/readable-foo' + shm-plug.cmd cat /dev/shm/readable-foo | MATCH "read-only area" + + if [ "$(snap debug confinement)" = strict ] ; then + echo "Plug cannot write to it" + if shm-plug.cmd sh -c 'echo "I cannot write this" > /dev/shm/readable-foo'; then + echo "Plug snap should not be able to write to read-only SHM area" + exit 1 + fi + echo "Double-check that the data was not changed" + shm-slot.cmd cat /dev/shm/readable-foo | MATCH "read-only area" + else + echo "Skipping check on disallowed write, because of partial confinement" + fi + + # cleanup + shm-slot.cmd rm /dev/shm/writable-bar /dev/shm/readable-foo + + echo "Disconnect the interface" + snap disconnect shm-plug:shmem + + if [ "$(snap debug confinement)" = partial ] ; then + echo "Do not execute checks with disconnected plug on systems where confinement doesn't work" + exit 0 + fi + + echo "Neither snap should be able to access the SHM now" + if shm-slot.cmd sh -c 'echo "test1" > /dev/shm/writable-bar'; then + exit 1 + fi + if shm-plug.cmd sh -c 'echo "test2" > /dev/shm/writable-bar'; then + exit 1 + fi + if shm-plug.cmd cat /dev/shm/writable-bar; then + exit 1 + fi + if shm-slot.cmd sh -c 'echo "test3" > /dev/shm/readable-bar'; then + exit 1 + fi + if shm-plug.cmd cat /dev/shm/readable-bar; then + exit 1 + fi diff --git a/tests/main/microk8s-smoke/task.yaml b/tests/main/microk8s-smoke/task.yaml index c6afbe046b..764e36e3b8 100644 --- a/tests/main/microk8s-smoke/task.yaml +++ b/tests/main/microk8s-smoke/task.yaml @@ -6,10 +6,12 @@ systems: - -centos-8-* # fails to start service daemon-containerd - -fedora-33-* # fails to start service daemon-containerd - -fedora-34-* # fails to start service daemon-containerd + - -fedora-35-* # fails to start service daemon-containerd - -debian-10-* # doesn't have libseccomp >= 2.4 - -ubuntu-14.04-* # doesn't have libseccomp >= 2.4 - -ubuntu-18.04-32 # no microk8s snap for i386 pc systems - -arch-linux-* # XXX: no curl to the pod for unknown reasons + - -ubuntu-*-arm* # not available on arm environment: CHANNEL/edge: latest/edge/strict diff --git a/tests/main/security-device-cgroups-helper/task.yaml b/tests/main/security-device-cgroups-helper/task.yaml index 3d8caaf031..88272c00ae 100644 --- a/tests/main/security-device-cgroups-helper/task.yaml +++ b/tests/main/security-device-cgroups-helper/task.yaml @@ -8,6 +8,13 @@ environment: DEVICES_PATH_MEM_FULL: /devices/virtual/mem/full # and /dev/kmsg has 1:11 DEVICES_PATH_MEM_KMSG: /devices/virtual/mem/kmsg + # enable debugs from s-c + SNAPD_DEBUG: "1" + +debug: | + udevadm info /dev/full || true + udevadm info /dev/kmsg || true + tests.device-cgroup test-strict-cgroup-helper.sh dump || true execute: | #shellcheck source=tests/lib/systems.sh @@ -88,6 +95,13 @@ execute: | NOMATCH 'Operation not permitted' < run.log test -n "$(cat run.log)" + # remove action removes the device from the cgroup + "$libexecdir"/snapd/snap-device-helper remove snap_test-strict-cgroup-helper_sh "$DEVICES_PATH_MEM_KMSG" 1:11 + # /dev/kmsg is not present anymore + tests.device-cgroup test-strict-cgroup-helper.sh dump | NOMATCH 'c 1:11 rwm' + # and it's not possible to read /dev/kmsg again + snap run test-strict-cgroup-helper.sh -c 'head -1 /dev/kmsg' 2>&1 | MATCH "Operation not permitted" + # now remove the cgroup if is_cgroupv2; then rm /sys/fs/bpf/snap/snap_test-strict-cgroup-helper_sh diff --git a/tests/main/security-udev-input-subsystem/task.yaml b/tests/main/security-udev-input-subsystem/task.yaml index 1cd3b09c86..f8b1931d33 100644 --- a/tests/main/security-udev-input-subsystem/task.yaml +++ b/tests/main/security-udev-input-subsystem/task.yaml @@ -14,6 +14,10 @@ prepare: | echo "Given the test-snapd-udev-input-subsystem is installed" "$TESTSTOOLS"/snaps-state install-local test-snapd-udev-input-subsystem +debug: | + # shellcheck disable=SC2046 + udevadm info $(find /dev/input/ -type c) | grep -e N: -e MAJOR= -e MINOR= -e TAGS= || true + execute: | if [ -z "$(find /dev/input/by-path -name '*-event-kbd')" ]; then if [ "$SPREAD_SYSTEM" = "ubuntu-16.04-64" ]; then diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar deleted file mode 120000 index 5c68478d09..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar +++ /dev/null @@ -1 +0,0 @@ -foo -> baz -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz deleted file mode 120000 index 5a883c8397..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz +++ /dev/null @@ -1 +0,0 @@ -foo -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz -> qux deleted file mode 120000 index 1910281566..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz -> qux +++ /dev/null @@ -1 +0,0 @@ -foo \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> qux deleted file mode 120000 index 365c3e79f6..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> qux +++ /dev/null @@ -1 +0,0 @@ -foo -> baz \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz deleted file mode 120000 index 6ea8eef97f..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz +++ /dev/null @@ -1 +0,0 @@ -foo -> bar -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz -> qux deleted file mode 120000 index 14b8163083..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz -> qux +++ /dev/null @@ -1 +0,0 @@ -foo -> bar \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo deleted file mode 120000 index 35bb28b82d..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo +++ /dev/null @@ -1 +0,0 @@ -bar -> baz -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar deleted file mode 120000 index ec193c70de..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar +++ /dev/null @@ -1 +0,0 @@ -baz -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> baz b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> baz deleted file mode 120000 index 78df5b06bd..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> baz +++ /dev/null @@ -1 +0,0 @@ -qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> qux deleted file mode 120000 index 3f95386662..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> qux +++ /dev/null @@ -1 +0,0 @@ -baz \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz deleted file mode 120000 index 21f05bdff4..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz +++ /dev/null @@ -1 +0,0 @@ -bar -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz -> qux deleted file mode 120000 index ba0e162e1c..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz -> qux +++ /dev/null @@ -1 +0,0 @@ -bar \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> qux deleted file mode 120000 index 522e3d9299..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> qux +++ /dev/null @@ -1 +0,0 @@ -bar -> baz \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/qux deleted file mode 120000 index af0f493f56..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/qux +++ /dev/null @@ -1 +0,0 @@ -foo -> bar -> baz \ No newline at end of file diff --git a/tests/main/validate-container-happy/task.yaml b/tests/main/validate-container-happy/task.yaml new file mode 100644 index 0000000000..b33faa0112 --- /dev/null +++ b/tests/main/validate-container-happy/task.yaml @@ -0,0 +1,38 @@ +summary: check the symlinks following the right track + +environment: + SNAP: test-snapd-validate-container-happy + +prepare: | + +execute: | + + SNAP_MOUNT_DIR="$(os.paths snap-mount-dir)" + + # We shouldn't use relative symlinks in Github as they cannot be packed correctly. + # So here let's test whether we can still pack such symlinks within a snap and use if needed. + # First we "try" to unpack the snap structure and untar the symlinks + # Then we pack the snap with these symlinks and then install + # Finally we check to see if the symlinks actually support the intervined symlinks + + # Untar the symlinks + tar -xvf "$SNAP"/hell/hell.tar -C "$SNAP"/hell + + snap try "$SNAP" + # Check to see if the symlinks point to the right paths + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/bar | MATCH "foo -> baz -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/baz | MATCH "foo -> bar -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/foo | MATCH "bar -> baz -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/qux | MATCH "foo -> bar -> baz" + snap remove "$SNAP" + + # Create a new snap structure that includes the unpacked symlinks + snap pack "$SNAP" + snap install --dangerous test-snapd-validate-container-happy_1.0_all.snap + tests.cleanup defer snap remove --purge test-snapd-validate-container-happy + + # Check to see if the symlinks retain their existing paths + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/bar | MATCH "foo -> baz -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/baz | MATCH "foo -> bar -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/foo | MATCH "bar -> baz -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/qux | MATCH "foo -> bar -> baz" diff --git a/tests/completion/data/twisted/.just a hidden file b/tests/main/validate-container-happy/test-snapd-validate-container-happy/bin/validate-container index e69de29bb2..e69de29bb2 100644..100755 --- a/tests/completion/data/twisted/.just a hidden file +++ b/tests/main/validate-container-happy/test-snapd-validate-container-happy/bin/validate-container diff --git a/tests/main/validate-container-happy/test-snapd-validate-container-happy/hell/hell.tar b/tests/main/validate-container-happy/test-snapd-validate-container-happy/hell/hell.tar Binary files differnew file mode 100644 index 0000000000..17c8f0c271 --- /dev/null +++ b/tests/main/validate-container-happy/test-snapd-validate-container-happy/hell/hell.tar diff --git a/tests/main/validate-container-happy/test-snapd-validate-container-happy/meta/snap.yaml b/tests/main/validate-container-happy/test-snapd-validate-container-happy/meta/snap.yaml new file mode 100644 index 0000000000..3f660211c7 --- /dev/null +++ b/tests/main/validate-container-happy/test-snapd-validate-container-happy/meta/snap.yaml @@ -0,0 +1,5 @@ +name: test-snapd-validate-container-happy +version: 1.0 +apps: + validate-container: + command: bin/validate-container diff --git a/tests/nested/core/core20-reinstall-partitions/task.yaml b/tests/nested/core/core20-reinstall-partitions/task.yaml new file mode 100644 index 0000000000..b9a7297e7c --- /dev/null +++ b/tests/nested/core/core20-reinstall-partitions/task.yaml @@ -0,0 +1,43 @@ +summary: Run a smoke test on UC20 with encryption enabled + +details: | + This test checks that UC20 can be reinstalled + +systems: [ubuntu-20.04-64] + +environment: + # TODO: figure out a way to do this test where we reset the swtpm after the + # shutdown to go into install mode, but before we actually reboot into the + # install mode + NESTED_ENABLE_SECURE_BOOT: false + NESTED_ENABLE_TPM: false + +execute: | + echo "Wait for the system to be seeded first" + tests.nested exec "sudo snap wait system seed.loaded" + + INITIAL_SERIAL=$(tests.nested exec snap model --serial | grep -Po 'serial:\s+\K.*') + + echo "Reinstall the system" + boot_id=$(tests.nested boot-id) + # add || true in case the SSH connection is broken while executing this + # since this command causes an immediate reboot + tests.nested exec "sudo snap reboot --install" || true + + tests.nested wait-for reboot "${boot_id}" + + # check that we are back in run mode + tests.nested exec cat /proc/cmdline | MATCH 'snapd_recovery_mode=run' + + # wait for the system to get setup and finish seeding + tests.nested wait-for snap-command + tests.nested exec "sudo snap wait system seed.loaded" + + # wait up to two minutes for serial registration + retry -n 60 --wait 2 tests.nested exec snap model --serial + + END_SERIAL=$(tests.nested exec snap model --serial | grep -Po 'serial:\s+\K.*') + if [ "$INITIAL_SERIAL" = "$END_SERIAL" ]; then + echo "test failed, same serial assertion after reinstallation" + exit 1 + fi diff --git a/tests/nested/manual/core20-initramfs-time-moves-forward/task.yaml b/tests/nested/manual/core20-initramfs-time-moves-forward/task.yaml index 32d0d51731..bf07472f4f 100644 --- a/tests/nested/manual/core20-initramfs-time-moves-forward/task.yaml +++ b/tests/nested/manual/core20-initramfs-time-moves-forward/task.yaml @@ -30,4 +30,4 @@ execute: | test "$(tests.nested exec date --utc '+%s')" -ge "$MODEL_ASSERTION_SIGN_TIME" echo "Verify that the timestamp from after snap-bootstrap ran is greater than the time from the model assertion" - test "$(tests.nested exec "cat /run/mnt/ubuntu-seed/install-after-snap-bootstrap-date")" -ge "$MODEL_ASSERTION_SIGN_TIME" + test "$(tests.nested exec "cat /run/mnt/ubuntu-seed/test/install-after-snap-bootstrap-date")" -ge "$MODEL_ASSERTION_SIGN_TIME" diff --git a/tests/nested/manual/core20-remodel/task.yaml b/tests/nested/manual/core20-remodel/task.yaml index bbf9e91191..1a16c6dca2 100644 --- a/tests/nested/manual/core20-remodel/task.yaml +++ b/tests/nested/manual/core20-remodel/task.yaml @@ -23,6 +23,9 @@ execute: | # conflict with an existing system label label_base=$(tests.nested exec "date '+%Y%m%d'") + # wait until device is initialized and has a serial + nested_wait_for_device_initialized_change + echo "Refresh model assertion to revision 2" nested_copy "$TESTSLIB/assertions/valid-for-testing-pc-revno-2-20.model" REMOTE_CHG_ID="$(tests.nested exec sudo snap remodel --no-wait valid-for-testing-pc-revno-2-20.model)" diff --git a/tests/nested/manual/core20-to-core22/task.yaml b/tests/nested/manual/core20-to-core22/task.yaml index 5a66f65141..8250f9509a 100644 --- a/tests/nested/manual/core20-to-core22/task.yaml +++ b/tests/nested/manual/core20-to-core22/task.yaml @@ -33,6 +33,9 @@ execute: | label_base=$(tests.nested exec "date '+%Y%m%d'") label="${label_base}-1" + # wait until device is initialized and has a serial + nested_wait_for_device_initialized_change + echo "Remodel to UC22" nested_copy "$TESTSLIB/assertions/valid-for-testing-pc-22-from-20.model" REMOTE_CHG_ID="$(tests.nested exec sudo snap remodel --no-wait valid-for-testing-pc-22-from-20.model)" diff --git a/timeutil/synchronized.go b/timeutil/synchronized.go index aef2784ca2..27cd2cdaff 100644 --- a/timeutil/synchronized.go +++ b/timeutil/synchronized.go @@ -25,7 +25,6 @@ import ( "github.com/godbus/dbus" "github.com/snapcore/snapd/dbusutil" - "github.com/snapcore/snapd/logger" ) func isNoServiceOrUnknownPropertyDbusErr(err error) bool { @@ -69,7 +68,6 @@ func IsNTPSynchronized() (bool, error) { if !ok { return false, fmt.Errorf("timedate1 returned invalid value for NTPSynchronized property: %s", dbusV) } - logger.Debugf("NTPSynchronized state returned by timedate1: %s", dbusV) return v, nil } |
