diff --git a/client/errors.go b/client/errors.go index cbed8c089a7..53c39de0cbf 100644 --- a/client/errors.go +++ b/client/errors.go @@ -25,16 +25,17 @@ type ErrorKind string // error kind const value doc comments here have a non-default, // specialized style (to help docs/error-kind.go): // -// // ErrorKind...: DESCRIPTION [no final dot] +// // ErrorKind...: DESCRIPTION . // +// Note the mandatory dot at the end. // `code-like` quoting should be used when meaningful. -// Error kinds. Keep in sync with: https://forum.snapcraft.io/t/using-the-rest-api/18603#heading--errors +// Error kinds. Keep https://forum.snapcraft.io/t/using-the-rest-api/18603#heading--errors in sync using doc/error-kinds.go. const ( // ErrorKindTwoFactorRequired: the client needs to retry the - // `login` command including an OTP + // `login` command including an OTP. ErrorKindTwoFactorRequired ErrorKind = "two-factor-required" - // ErrorKindTwoFactorFailed: the OTP provided wasn't recognised + // ErrorKindTwoFactorFailed: the OTP provided wasn't recognised. ErrorKindTwoFactorFailed ErrorKind = "two-factor-failed" // ErrorKindLoginRequired: the requested operation cannot be // performed without an authenticated user. This is the kind @@ -46,46 +47,45 @@ const ( // and a list of the failures on each field. ErrorKindInvalidAuthData ErrorKind = "invalid-auth-data" // ErrorKindPasswordPolicy: provided password doesn't meet - // system policy + // system policy. ErrorKindPasswordPolicy ErrorKind = "password-policy" - // ErrorKindAuthCancelled: authentication was cancelled by the user + // ErrorKindAuthCancelled: authentication was cancelled by the user. ErrorKindAuthCancelled ErrorKind = "auth-cancelled" - // ErrorKindTermsNotAccepted: deprecated, do not document + // ErrorKindTermsNotAccepted: deprecated, do not document. ErrorKindTermsNotAccepted ErrorKind = "terms-not-accepted" - // ErrorKindNoPaymentMethods: deprecated, do not document + // ErrorKindNoPaymentMethods: deprecated, do not document. ErrorKindNoPaymentMethods ErrorKind = "no-payment-methods" - // ErrorKindPaymentDeclined: deprecated, do not document + // ErrorKindPaymentDeclined: deprecated, do not document. ErrorKindPaymentDeclined ErrorKind = "payment-declined" // ErrorKindSnapAlreadyInstalled: the requested snap is - // already installed + // already installed. ErrorKindSnapAlreadyInstalled ErrorKind = "snap-already-installed" - // ErrorKindSnapNotInstalled: the requested snap is not installed + // ErrorKindSnapNotInstalled: the requested snap is not installed. ErrorKindSnapNotInstalled ErrorKind = "snap-not-installed" - // ErrorKindSnapNotFound: the requested snap couldn't be found + // ErrorKindSnapNotFound: the requested snap couldn't be found. ErrorKindSnapNotFound ErrorKind = "snap-not-found" - // ErrorKindAppNotFound: the requested app couldn't be found + // ErrorKindAppNotFound: the requested app couldn't be found. ErrorKindAppNotFound ErrorKind = "app-not-found" - // ErrorKindSnapLocal: cannot perform operation on local snap + // ErrorKindSnapLocal: cannot perform operation on local snap. ErrorKindSnapLocal ErrorKind = "snap-local" // ErrorKindSnapNeedsDevMode: the requested snap needs devmode - // to be installed + // to be installed. ErrorKindSnapNeedsDevMode ErrorKind = "snap-needs-devmode" // ErrorKindSnapNeedsClassic: the requested snap needs classic - // confinement to be installed + // confinement to be installed. ErrorKindSnapNeedsClassic ErrorKind = "snap-needs-classic" // ErrorKindSnapNeedsClassicSystem: the requested snap can't - // be installed on the current non-classic system + // be installed on the current non-classic system. ErrorKindSnapNeedsClassicSystem ErrorKind = "snap-needs-classic-system" - // ErrorKindSnapNotClassic: snap not compatible with classic mode + // ErrorKindSnapNotClassic: snap not compatible with classic mode. ErrorKindSnapNotClassic ErrorKind = "snap-not-classic" // ErrorKindSnapNoUpdateAvailable: the requested snap does not - // have an update available + // have an update available. ErrorKindSnapNoUpdateAvailable ErrorKind = "snap-no-update-available" - // ErrorKindSnapRevisionNotAvailable: no snap revision available - // as specified + // as specified. ErrorKindSnapRevisionNotAvailable ErrorKind = "snap-revision-not-available" // ErrorKindSnapChannelNotAvailable: no snap revision on specified // channel. The `value` of the error is a rich object with @@ -105,38 +105,38 @@ const ( ErrorKindSnapChangeConflict ErrorKind = "snap-change-conflict" // ErrorKindNotSnap: the given snap or directory does not - // look like a snap + // look like a snap. ErrorKindNotSnap ErrorKind = "snap-not-a-snap" // ErrorKindInterfacesUnchanged: the requested interfaces' - // operation would have no effect + // operation would have no effect. ErrorKindInterfacesUnchanged ErrorKind = "interfaces-unchanged" - // ErrorKindBadQuery: a bad query was provided + // ErrorKindBadQuery: a bad query was provided. ErrorKindBadQuery ErrorKind = "bad-query" // ErrorKindConfigNoSuchOption: the given configuration option - // does not exist + // does not exist. ErrorKindConfigNoSuchOption ErrorKind = "option-not-found" - // ErrorKindAssertionNotFound: assertion can not be found + // ErrorKindAssertionNotFound: assertion can not be found. ErrorKindAssertionNotFound ErrorKind = "assertion-not-found" - // ErrorKindUnsuccessful: snapctl command was unsuccessful + // ErrorKindUnsuccessful: snapctl command was unsuccessful. ErrorKindUnsuccessful ErrorKind = "unsuccessful" - // ErrorKindNetworkTimeout: a timeout occurred during the request + // ErrorKindNetworkTimeout: a timeout occurred during the request. ErrorKindNetworkTimeout ErrorKind = "network-timeout" - // ErrorKindDNSFailure: DNS not responding + // ErrorKindDNSFailure: DNS not responding. ErrorKindDNSFailure ErrorKind = "dns-failure" ) // Maintenance error kinds. // These are used only inside the maintenance field of responses. -// Keep in sync with: https://forum.snapcraft.io/t/using-the-rest-api/18603#heading--maint-errors +// Keep https://forum.snapcraft.io/t/using-the-rest-api/18603#heading--maint-errors in sync using doc/error-kinds.go. const ( - // ErrorKindDaemonRestart: daemon is restarting + // ErrorKindDaemonRestart: daemon is restarting. ErrorKindDaemonRestart ErrorKind = "daemon-restart" - // ErrorKindSystemRestart: system is restarting + // ErrorKindSystemRestart: system is restarting. ErrorKindSystemRestart ErrorKind = "system-restart" ) diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go index 46465313be5..35c2d345591 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go @@ -1183,6 +1183,14 @@ func (s *initramfsMountsSuite) testInitramfsMountsInstallRecoverModeStep2Measure } func (s *initramfsMountsSuite) TestInitramfsMountsInstallModeStep2Measure(c *C) { + s.testInitramfsMountsInstallRecoverModeStep2Measure(c, "install") +} + +func (s *initramfsMountsSuite) TestInitramfsMountsInstallModeUnsetStep2Measure(c *C) { + // TODO:UC20: eventually we should require snapd_recovery_mode to be set to + // explicitly "install" for install mode, but we originally allowed + // snapd_recovery_mode="" and interpreted it as install mode, so test that + // case too s.testInitramfsMountsInstallRecoverModeStep2Measure(c, "") } diff --git a/cmd/snap/cmd_debug_seeding.go b/cmd/snap/cmd_debug_seeding.go index 4f23487bf0f..81b72191625 100644 --- a/cmd/snap/cmd_debug_seeding.go +++ b/cmd/snap/cmd_debug_seeding.go @@ -32,6 +32,7 @@ import ( type cmdSeeding struct { clientMixin + unicodeMixin } func init() { @@ -45,6 +46,8 @@ func init() { } func (x *cmdSeeding) Execute(args []string) error { + esc := x.getEscapes() + if len(args) > 0 { return ErrExtraArgs } @@ -59,6 +62,8 @@ func (x *cmdSeeding) Execute(args []string) error { // use json.RawMessage to delay unmarshal'ing to the interfaces pkg PreseedSystemKey *json.RawMessage `json:"preseed-system-key,omitempty"` SeedRestartSystemKey *json.RawMessage `json:"seed-restart-system-key,omitempty"` + + SeedError string `json:"seed-error,omitempty"` } if err := x.client.DebugGet("seeding", &resp, nil); err != nil { return err @@ -68,6 +73,19 @@ func (x *cmdSeeding) Execute(args []string) error { // show seeded and preseeded keys fmt.Fprintf(w, "seeded:\t%v\n", resp.Seeded) + if resp.SeedError != "" { + // print seed-error + termWidth, _ := termSize() + termWidth -= 3 + if termWidth > 100 { + // any wider than this and it gets hard to read + termWidth = 100 + } + fmt.Fprintln(w, "seed-error: |") + // XXX: reuse/abuse + printDescr(w, resp.SeedError, termWidth) + } + fmt.Fprintf(w, "preseeded:\t%v\n", resp.Preseeded) // calculate the time spent preseeding (if preseeded) and seeding @@ -76,13 +94,13 @@ func (x *cmdSeeding) Execute(args []string) error { // if we are missing time values, we will default to showing "-" for the // duration - seedDuration := "-" + seedDuration := esc.dash if resp.Preseeded { if resp.PreseedTime != nil && resp.PreseedStartTime != nil { preseedDuration := resp.PreseedTime.Sub(*resp.PreseedStartTime).Round(time.Millisecond) fmt.Fprintf(w, "image-preseeding:\t%v\n", preseedDuration) } else { - fmt.Fprintf(w, "image-preseeding:\t-\n") + fmt.Fprintf(w, "image-preseeding:\t%s\n", esc.dash) } if resp.SeedTime != nil && resp.SeedRestartTime != nil { diff --git a/cmd/snap/cmd_debug_seeding_test.go b/cmd/snap/cmd_debug_seeding_test.go index e696feccc11..d2658e96c41 100644 --- a/cmd/snap/cmd_debug_seeding_test.go +++ b/cmd/snap/cmd_debug_seeding_test.go @@ -219,6 +219,18 @@ var noPreseedingJSON = ` "type": "sync" }` +var seedingError = `{ + "result": { + "preseed-start-time": "2020-07-24T21:41:33.838194712Z", + "preseed-time": "2020-07-24T21:41:43.156401424Z", + "preseeded": true, + "seed-error": "cannot perform the following tasks:\n- xxx" + }, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + // a system that was preseeded, but didn't record the new keys // this is the case for a system that was preseeded and then seeded with an old // snapd, but then is refreshed to a version of snapd that supports snap debug @@ -258,11 +270,12 @@ var stillSeedingNoPreseed = `{ func (s *SnapSuite) TestDebugSeeding(c *C) { tt := []struct { - jsonResp string - expStdout string - expStderr string - expErr string - comment string + jsonResp string + expStdout string + expStderr string + expErr string + comment string + hasUnicode bool }{ { jsonResp: newPreseedNewSnapdSameSysKey, @@ -361,9 +374,19 @@ seed-restart-system-key: { expStdout: ` seeded: true preseeded: false -seed-completion: - +seed-completion: -- `[1:], - comment: "not preseeded", + comment: "not preseeded no unicode", + }, + { + jsonResp: noPreseedingJSON, + expStdout: ` +seeded: true +preseeded: false +seed-completion: – +`[1:], + comment: "not preseeded", + hasUnicode: true, }, { jsonResp: oldPreseedingJSON, @@ -381,18 +404,52 @@ seed-completion: 2m0s seeded: false preseeded: true image-preseeding: 9.318s -seed-completion: - +seed-completion: -- `[1:], - comment: "preseeded, still seeding", + comment: "preseeded, still seeding no unicode", + }, + { + jsonResp: stillSeeding, + expStdout: ` +seeded: false +preseeded: true +image-preseeding: 9.318s +seed-completion: – +`[1:], + hasUnicode: true, + comment: "preseeded, still seeding", }, { jsonResp: stillSeedingNoPreseed, expStdout: ` seeded: false preseeded: false -seed-completion: - +seed-completion: -- `[1:], - comment: "not preseeded, still seeding", + comment: "not preseeded, still seeding no unicode", + }, + { + jsonResp: stillSeedingNoPreseed, + expStdout: ` +seeded: false +preseeded: false +seed-completion: – +`[1:], + hasUnicode: true, + comment: "not preseeded, still seeding", + }, + { + jsonResp: seedingError, + expStdout: ` +seeded: false +seed-error: | + cannot perform the following tasks: + - xxx +preseeded: true +image-preseeding: 9.318s +seed-completion: -- +`[1:], + comment: "preseeded, error during seeding", }, } @@ -414,7 +471,11 @@ seed-completion: - c.Fatalf("expected to get 1 request, now on %d", n) } }) - rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "seeding"}) + args := []string{"debug", "seeding"} + if t.hasUnicode { + args = append(args, "--unicode=always") + } + rest, err := snap.Parser(snap.Client()).ParseArgs(args) if t.expErr != "" { c.Assert(err, ErrorMatches, t.expErr, comment) c.Assert(s.Stdout(), Equals, "", comment) diff --git a/daemon/api_debug_seeding.go b/daemon/api_debug_seeding.go index 2d93e6b393d..27678f842b2 100644 --- a/daemon/api_debug_seeding.go +++ b/daemon/api_debug_seeding.go @@ -56,6 +56,11 @@ type seedingInfo struct { // SeedRestartSystemKey is the system-key that was created on first boot of the // preseeded image. SeedRestartSystemKey interface{} `json:"seed-restart-system-key,omitempty"` + + // SeedError is set if no seed change succeeded yet and at + // least one was in error. It is set to the error of the + // oldest known in error one. + SeedError string `json:"seed-error,omitempty"` } func getSeedingInfo(st *state.State) Response { @@ -76,8 +81,25 @@ func getSeedingInfo(st *state.State) Response { return InternalError(err.Error()) } + var seedError string + var seedErrorChangeTime time.Time + if !seeded { + for _, chg := range st.Changes() { + if chg.Kind() != "seed" && !chg.IsReady() { + continue + } + if err := chg.Err(); err != nil { + if seedErrorChangeTime.IsZero() || chg.SpawnTime().Before(seedErrorChangeTime) { + seedError = chg.Err().Error() + seedErrorChangeTime = chg.SpawnTime() + } + } + } + } + data := &seedingInfo{ Seeded: seeded, + SeedError: seedError, Preseeded: preseeded, PreseedSystemKey: preseedSysKey, SeedRestartSystemKey: seedRestartSysKey, diff --git a/daemon/api_debug_seeding_test.go b/daemon/api_debug_seeding_test.go index 53b559e9374..1a16b06f6c6 100644 --- a/daemon/api_debug_seeding_test.go +++ b/daemon/api_debug_seeding_test.go @@ -24,6 +24,8 @@ import ( "time" . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/overlord/state" ) var _ = Suite(&seedingDebugSuite{}) @@ -152,3 +154,64 @@ func (s *seedingDebugSuite) TestSeedingDebugPreseededStillSeeding(c *C) { SeedRestartTime: &seedRestartTime, }) } + +func (s *seedingDebugSuite) TestSeedingDebugPreseededSeedError(c *C) { + preseedStartTime, err := time.Parse(time.RFC3339, "2020-01-01T10:00:00Z") + c.Assert(err, IsNil) + preseedTime, err := time.Parse(time.RFC3339, "2020-01-01T10:00:01Z") + c.Assert(err, IsNil) + seedRestartTime, err := time.Parse(time.RFC3339, "2020-01-01T10:00:03Z") + c.Assert(err, IsNil) + + st := s.d.overlord.State() + st.Lock() + + st.Set("preseeded", true) + st.Set("seeded", false) + + st.Set("preseed-system-key", "foo") + st.Set("seed-restart-system-key", "bar") + + st.Set("preseed-start-time", preseedStartTime) + st.Set("seed-restart-time", seedRestartTime) + + st.Set("preseed-time", preseedTime) + + chg1 := st.NewChange("seed", "tentative 1") + t11 := st.NewTask("seed task", "t11") + t12 := st.NewTask("seed task", "t12") + chg1.AddTask(t11) + chg1.AddTask(t12) + t11.SetStatus(state.UndoneStatus) + t11.Errorf("t11: undone") + t12.SetStatus(state.ErrorStatus) + t12.Errorf("t12: fail") + + // ensure different spawn time + time.Sleep(50 * time.Millisecond) + chg2 := st.NewChange("seed", "tentative 2") + t21 := st.NewTask("seed task", "t21") + chg2.AddTask(t21) + t21.SetStatus(state.ErrorStatus) + t21.Errorf("t21: error") + + chg3 := st.NewChange("seed", "tentative 3") + t31 := st.NewTask("seed task", "t31") + chg3.AddTask(t31) + t31.SetStatus(state.DoingStatus) + + st.Unlock() + + data := s.getSeedingDebug(c) + c.Check(data, DeepEquals, &seedingInfo{ + Seeded: false, + Preseeded: true, + PreseedSystemKey: "foo", + SeedRestartSystemKey: "bar", + PreseedStartTime: &preseedStartTime, + PreseedTime: &preseedTime, + SeedRestartTime: &seedRestartTime, + SeedError: `cannot perform the following tasks: +- t12 (t12: fail)`, + }) +} diff --git a/docs/error-kinds.go b/docs/error-kinds.go index aacf5a5894d..9105a60cbbb 100644 --- a/docs/error-kinds.go +++ b/docs/error-kinds.go @@ -78,12 +78,8 @@ func main() { doc = strings.Replace(doc, "\n", " ", -1) doc = strings.Replace(doc, " ", " ", -1) doc = strings.TrimSpace(doc) - doc1 := strings.TrimSuffix(doc, ".") - if doc1 != doc { - if strings.Index(doc1, ". ") == -1 { - fmt.Fprintf(os.Stderr, "unexpected dot at the end %q for %s\n", doc, name) - } - doc = doc1 + if !strings.HasSuffix(doc, ".") { + fmt.Fprintf(os.Stderr, "expected dot at the end %q for %s\n", doc, name) } if strings.HasPrefix(doc, "deprecated") { // skip diff --git a/osutil/disks/mockdisk.go b/osutil/disks/mockdisk.go new file mode 100644 index 00000000000..3579a677743 --- /dev/null +++ b/osutil/disks/mockdisk.go @@ -0,0 +1,131 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package disks + +import ( + "fmt" + + "github.com/snapcore/snapd/osutil" +) + +// MockDiskMapping is an implementation of Disk for mocking purposes, it is +// exported so that other packages can easily mock a specific disk layout +// without needing to mock the mount setup, sysfs, or udev commands just to test +// high level logic. +// DevNum must be a unique string per unique mocked disk, if only one disk is +// being mocked it can be left empty. +type MockDiskMapping struct { + FilesystemLabelToPartUUID map[string]string + DiskHasPartitions bool + DevNum string +} + +// FindMatchingPartitionUUID returns a matching PartitionUUID for the specified +// label if it exists. Part of the Disk interface. +func (d *MockDiskMapping) FindMatchingPartitionUUID(label string) (string, error) { + osutil.MustBeTestBinary("mock disks only to be used in tests") + if partuuid, ok := d.FilesystemLabelToPartUUID[label]; ok { + return partuuid, nil + } + return "", FilesystemLabelNotFoundError{Label: label} +} + +// HasPartitions returns if the mock disk has partitions or not. Part of the +// Disk interface. +func (d *MockDiskMapping) HasPartitions() bool { + return d.DiskHasPartitions +} + +// MountPointIsFromDisk returns if the disk that the specified mount point comes +// from is the same disk as the object. Part of the Disk interface. +func (d *MockDiskMapping) MountPointIsFromDisk(mountpoint string, opts *Options) (bool, error) { + osutil.MustBeTestBinary("mock disks only to be used in tests") + + // this is relying on the fact that DiskFromMountPoint should have been + // mocked for us to be using this mockDisk method anyways + otherDisk, err := DiskFromMountPoint(mountpoint, opts) + if err != nil { + return false, err + } + + if otherDisk.Dev() == d.Dev() && otherDisk.HasPartitions() == d.HasPartitions() { + return true, nil + } + + return false, nil +} + +// Dev returns a unique representation of the mock disk that is suitable for +// comparing two mock disks to see if they are the same. Part of the Disk +// interface. +func (d *MockDiskMapping) Dev() string { + return d.DevNum +} + +// Mountpoint is a combination of a mountpoint location and whether that +// mountpoint is a decrypted device. It is only used in identifying mount points +// with MountPointIsFromDisk and DiskFromMountPoint with +// MockMountPointDisksToPartionMapping. +type Mountpoint struct { + Mountpoint string + IsDecryptedDevice bool +} + +// MockMountPointDisksToPartionMapping will mock DiskFromMountPoint such that +// the specified mapping is returned/used. Specifically, keys in the provided +// map are mountpoints, and the values for those keys are the disks that will +// be returned from DiskFromMountPoint or used internally in +// MountPointIsFromDisk. +func MockMountPointDisksToPartionMapping(mockedMountPoints map[Mountpoint]*MockDiskMapping) (restore func()) { + osutil.MustBeTestBinary("mock disks only to be used in tests") + + // verify that all unique MockDiskMapping's have unique DevNum's + alreadySeen := make(map[string]*MockDiskMapping, len(mockedMountPoints)) + for _, mockDisk := range mockedMountPoints { + if old, ok := alreadySeen[mockDisk.DevNum]; ok { + if mockDisk != old { + // we already saw a disk with this DevNum as a different pointer + // so just assume it's different + msg := fmt.Sprintf("mocked disks %+v and %+v have the same DevNum (%s) but are not the same object", old, mockDisk, mockDisk.DevNum) + panic(msg) + } + // otherwise same ptr, no point in comparing them + } else { + // didn't see it before, save it now + alreadySeen[mockDisk.DevNum] = mockDisk + } + } + + old := diskFromMountPoint + + diskFromMountPoint = func(mountpoint string, opts *Options) (Disk, error) { + if opts == nil { + opts = &Options{} + } + m := Mountpoint{mountpoint, opts.IsDecryptedDevice} + if mockedDisk, ok := mockedMountPoints[m]; ok { + return mockedDisk, nil + } + return nil, fmt.Errorf("mountpoint %s not mocked", mountpoint) + } + return func() { + diskFromMountPoint = old + } +} diff --git a/osutil/disks/mockdisk_test.go b/osutil/disks/mockdisk_test.go new file mode 100644 index 00000000000..da115533f12 --- /dev/null +++ b/osutil/disks/mockdisk_test.go @@ -0,0 +1,208 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package disks_test + +import ( + . "gopkg.in/check.v1" + + "golang.org/x/xerrors" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil/disks" + "github.com/snapcore/snapd/testutil" +) + +type mockDiskSuite struct { + testutil.BaseTest +} + +var _ = Suite(&mockDiskSuite{}) + +func (s *mockDiskSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) +} + +func (s *mockDiskSuite) TestMockMountPointDisksToPartionMappingVerifiesUniqueness(c *C) { + // two different disks with different DevNum's + d1 := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "label1": "part1", + }, + DiskHasPartitions: true, + DevNum: "d1", + } + + d2 := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "label1": "part1", + }, + DiskHasPartitions: false, + DevNum: "d2", + } + + // the pointers are different, and they are not the same + c.Assert(d1, Not(Equals), d2) + c.Assert(d1, Not(DeepEquals), d2) + + m := map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: "mount1"}: d1, + {Mountpoint: "mount2"}: d1, + {Mountpoint: "mount3"}: d2, + } + + // mocking works + r := disks.MockMountPointDisksToPartionMapping(m) + defer r() + + // changing so they have the same DevNum doesn't work though + d2.DevNum = "d1" + c.Assert( + func() { disks.MockMountPointDisksToPartionMapping(m) }, + PanicMatches, + `mocked disks .* and .* have the same DevNum \(d1\) but are not the same object`, + ) + + // mocking with just one disk at multiple mount points works too + m2 := map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: "mount1"}: d1, + {Mountpoint: "mount2"}: d1, + } + r = disks.MockMountPointDisksToPartionMapping(m2) + defer r() +} + +func (s *mockDiskSuite) TestMockMountPointDisksToPartionMapping(c *C) { + d1 := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "label1": "part1", + }, + DiskHasPartitions: true, + DevNum: "d1", + } + + d2 := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "label2": "part2", + }, + DiskHasPartitions: true, + DevNum: "d2", + } + + r := disks.MockMountPointDisksToPartionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: "mount1"}: d1, + {Mountpoint: "mount2"}: d1, + {Mountpoint: "mount3"}: d2, + }, + ) + defer r() + + // we can find the mock disk + foundDisk, err := disks.DiskFromMountPoint("mount1", nil) + c.Assert(err, IsNil) + + // and it has labels + label, err := foundDisk.FindMatchingPartitionUUID("label1") + c.Assert(err, IsNil) + c.Assert(label, Equals, "part1") + + // the same mount point is always from the same disk + matches, err := foundDisk.MountPointIsFromDisk("mount1", nil) + c.Assert(err, IsNil) + c.Assert(matches, Equals, true) + + // mount2 goes to the same disk, as per the mapping above + matches, err = foundDisk.MountPointIsFromDisk("mount2", nil) + c.Assert(err, IsNil) + c.Assert(matches, Equals, true) + + // mount3 does not however + matches, err = foundDisk.MountPointIsFromDisk("mount3", nil) + c.Assert(err, IsNil) + c.Assert(matches, Equals, false) + + // a disk from mount3 is also able to be found + foundDisk2, err := disks.DiskFromMountPoint("mount3", nil) + c.Assert(err, IsNil) + + // we can find label2 from mount3's disk + label, err = foundDisk2.FindMatchingPartitionUUID("label2") + c.Assert(err, IsNil) + c.Assert(label, Equals, "part2") + + // we can't find label1 from mount1's or mount2's disk + _, err = foundDisk2.FindMatchingPartitionUUID("label1") + c.Assert(err, ErrorMatches, "filesystem label \"label1\" not found") + var errNotFound disks.FilesystemLabelNotFoundError + c.Assert(xerrors.As(err, &errNotFound), Equals, true) + + // mount1 and mount2 do not match mount3 disk + matches, err = foundDisk2.MountPointIsFromDisk("mount1", nil) + c.Assert(err, IsNil) + c.Assert(matches, Equals, false) + matches, err = foundDisk2.MountPointIsFromDisk("mount2", nil) + c.Assert(err, IsNil) + c.Assert(matches, Equals, false) +} + +func (s *mockDiskSuite) TestMockMountPointDisksToPartionMappingDecryptedDevices(c *C) { + d1 := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-seed": "ubuntu-seed-part", + "ubuntu-boot": "ubuntu-boot-part", + "ubuntu-data-enc": "ubuntu-data-enc-part", + }, + DiskHasPartitions: true, + DevNum: "d1", + } + + r := disks.MockMountPointDisksToPartionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: "/run/mnt/ubuntu-boot"}: d1, + {Mountpoint: "/run/mnt/ubuntu-seed"}: d1, + { + Mountpoint: "/run/mnt/ubuntu-data", + IsDecryptedDevice: true, + }: d1, + }, + ) + defer r() + + // first we get ubuntu-boot (which is not a decrypted device) + d, err := disks.DiskFromMountPoint("/run/mnt/ubuntu-boot", nil) + c.Assert(err, IsNil) + + // next we find ubuntu-seed (also not decrypted) + label, err := d.FindMatchingPartitionUUID("ubuntu-seed") + c.Assert(err, IsNil) + c.Assert(label, Equals, "ubuntu-seed-part") + + // then we find ubuntu-data-enc, which is not a decrypted device + label, err = d.FindMatchingPartitionUUID("ubuntu-data-enc") + c.Assert(err, IsNil) + c.Assert(label, Equals, "ubuntu-data-enc-part") + + // and then finally ubuntu-data enc is from the same disk as ubuntu-boot + // with IsDecryptedDevice = true + opts := &disks.Options{IsDecryptedDevice: true} + matches, err := d.MountPointIsFromDisk("/run/mnt/ubuntu-data", opts) + c.Assert(err, IsNil) + c.Assert(matches, Equals, true) +} diff --git a/packaging/debian-sid/rules b/packaging/debian-sid/rules index 677f03b3bb5..a7b8a48cc66 100755 --- a/packaging/debian-sid/rules +++ b/packaging/debian-sid/rules @@ -243,6 +243,8 @@ override_dh_install-arch: rm -f ${CURDIR}/debian/tmp/usr/bin/chrorder # bootloader assets generator rm -f ${CURDIR}/debian/tmp/usr/bin/genasset + # docs generator + rm -f ${CURDIR}/debian/tmp/usr/bin/docs # Install snapd's systemd units / upstart jobs, done # here instead of debian/snapd.install because the diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh index d0d144ef135..5473aa9ecaf 100644 --- a/tests/lib/nested.sh +++ b/tests/lib/nested.sh @@ -154,7 +154,7 @@ get_nested_snap_rev(){ get_snap_rev_for_channel(){ SNAP=$1 CHANNEL=$2 - execute_remote "snap info $SNAP" | grep "$CHANNEL" | awk '{ print $4 }' | sed 's/.*(\(.*\))/\1/' | tr -d '\n' + snap info "$SNAP" | grep "$CHANNEL" | awk '{ print $4 }' | sed 's/.*(\(.*\))/\1/' | tr -d '\n' } get_nested_snap_channel(){ diff --git a/tests/nested/manual/refresh-revert-fundamentals/task.yaml b/tests/nested/manual/refresh-revert-fundamentals/task.yaml new file mode 100644 index 00000000000..82dec6655c9 --- /dev/null +++ b/tests/nested/manual/refresh-revert-fundamentals/task.yaml @@ -0,0 +1,137 @@ +summary: Refresh and revert the fundamental snaps for uc20 + +description: | + This test validates the fundamental snaps can be refreshed + and reverted to the new snaps published to edge channel. + +systems: [ubuntu-20.04-*] + +environment: + CORE_CHANNEL: beta + CORE_REFRESH_CHANNEL: edge + BUILD_SNAPD_FROM_CURRENT: false + USE_CLOUD_INIT: true + ENABLE_SECURE_BOOT: true + ENABLE_TPM: true + + SNAP/kernel: pc-kernel + TRACK/kernel: 20 + + SNAP/gadget: pc + TRACK/gadget: 20 + + SNAP/snapd: snapd + TRACK/snapd: latest + + SNAP/base: core20 + TRACK/base: latest + +prepare: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + FROM_REV="$(get_snap_rev_for_channel "$SNAP" $TRACK/$CORE_CHANNEL)" + TO_REV="$(get_snap_rev_for_channel "$SNAP" $TRACK/$CORE_REFRESH_CHANNEL)" + + if [ "$FROM_REV" = "$TO_REV" ]; then + echo "Initial and target revisions are the same, skipping..." + touch skip.test + exit + fi + + create_nested_core_vm + start_nested_core_vm + +restore: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + if [ -f skip.test ]; then + rm -f skip.test + exit + fi + + destroy_nested_vm + cleanup_nested_env + + rm -f "$WORK_DIR/image/ubuntu-core.img" + +execute: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + if [ -f skip.test ]; then + exit + fi + + FROM_REV="$(get_snap_rev_for_channel "$SNAP" $TRACK/$CORE_CHANNEL)" + TO_REV="$(get_snap_rev_for_channel "$SNAP" $TRACK/$CORE_REFRESH_CHANNEL)" + + execute_remote "snap list $SNAP" | MATCH "^${SNAP}.*${FROM_REV}.*${TRACK}/${CORE_CHANNEL}.*" + # The snap is refreshed + REFRESH_ID=$(execute_remote "sudo snap refresh --no-wait --channel $CORE_REFRESH_CHANNEL $SNAP") + + case "$SNAP" in + snapd|pc) + # we manually reboot even after snapd refresh to ensure that if + # resealing took place we are still able to boot + execute_remote "snap watch $REFRESH_ID" + execute_remote "snap changes" | MATCH "$REFRESH_ID\s+Done\s+.*" + execute_remote "sudo reboot" + ;; + pc-kernel|core20) + # don't manually reboot, wait for automatic snapd reboot + ;; + esac + wait_for_no_ssh + wait_for_ssh + + # Check the new version of the snaps is correct after the system reboot + execute_remote "snap list $SNAP" | MATCH "^${SNAP}.*${TO_REV}.*${TRACK}/${CORE_REFRESH_CHANNEL}.*" + + # We check the change is completed + case "$SNAP" in + pc-kernel|core20) + for _ in $(seq 10); do + if execute_remote "snap changes" | MATCH "$REFRESH_ID\s+Done\s+.*"; then + break + fi + sleep 1 + done + execute_remote "snap changes" | MATCH "$REFRESH_ID\s+Done\s+.*" + ;; + esac + + # The snap is reverted + REVERT_ID=$(execute_remote "sudo snap revert --no-wait $SNAP") + + case "$SNAP" in + snapd|pc) + # we manually reboot even after snapd refresh to ensure that if + # resealing took place we are still able to boot + execute_remote "snap watch $REVERT_ID" + execute_remote "snap changes" | MATCH "$REVERT_ID\s+Done\s+.*" + execute_remote "sudo reboot" + ;; + pc-kernel|core20) + # don't manually reboot, wait for automatic snapd reboot + ;; + esac + wait_for_no_ssh + wait_for_ssh + + # Check the version of the snaps after the revert is correct + execute_remote "snap list $SNAP" | MATCH "^${SNAP}.*${FROM_REV}.*${TRACK}/${CORE_REFRESH_CHANNEL}.*" + + # We check the change is completed + case "$SNAP" in + pc-kernel|core20) + for _ in $(seq 10); do + if execute_remote "snap changes" | MATCH "$REVERT_ID\s+Done\s+.*"; then + break + fi + sleep 1 + done + execute_remote "snap changes" | MATCH "$REVERT_ID\s+Done\s+.*" + ;; + esac diff --git a/tests/nested/manual/refresh-revert-gadget-and-kernel/task.yaml b/tests/nested/manual/refresh-revert-gadget-and-kernel/task.yaml deleted file mode 100644 index cf53da50cbf..00000000000 --- a/tests/nested/manual/refresh-revert-gadget-and-kernel/task.yaml +++ /dev/null @@ -1,83 +0,0 @@ -summary: Refresh and revert the gadget and kernel snap - -description: | - This test validates the kernel and gadget snaps can be refreshed - and reverted to the new snaps published to edge channel. - -systems: [ubuntu-20.04-*] - -environment: - CORE_CHANNEL: beta - CORE_REFRESH_CHANNEL: edge - BUILD_SNAPD_FROM_CURRENT: false - USE_CLOUD_INIT: true - ENABLE_SECURE_BOOT: true - ENABLE_TPM: true - SNAP/kernel: pc-kernel - SNAP/gadget: pc - -prepare: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - - FROM_REV="$(get_snap_rev_for_channel "$SNAP" 20/$CORE_CHANNEL)" - TO_REV="$(get_snap_rev_for_channel "$SNAP" 20/$CORE_REFRESH_CHANNEL)" - - if [ "$FROM_REV" = "$TO_REV" ]; then - echo "Initial and target revisions are the same, skipping..." - touch skip.test - exit - fi - - mkdir -p "$WORK_DIR/image" - rm -f "$WORK_DIR/image/ubuntu-core.img" - - URL="$(get_cdimage_current_image_url 20 dangerous-$CORE_CHANNEL amd64)" - wget -O "$WORK_DIR/image/ubuntu-core.img.xz" "$URL" - unxz "$WORK_DIR/image/ubuntu-core.img.xz" - - configure_cloud_init_nested_core_vm_uc20 - start_nested_core_vm - -restore: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - - if [ -f skip.test ]; then - rm -f skip.test - exit - fi - - destroy_nested_vm - cleanup_nested_env - - rm -f "$WORK_DIR/image/ubuntu-core.img" - -execute: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - - if [ -f skip.test ]; then - exit - fi - - execute_remote "snap list $SNAP" | MATCH "^$SNAP .* 20/$CORE_CHANNEL .*" - # The snap is refreshed - execute_remote "sudo snap refresh --channel $CORE_REFRESH_CHANNEL $SNAP" || true - - # Wait for system reboot - wait_for_no_ssh - wait_for_ssh - - # Check the new version of the snaps is correct after the system reboot - execute_remote "snap list $SNAP" | MATCH "^$SNAP .* 20/$CORE_REFRESH_CHANNEL .*" - - # The snap is reverted - execute_remote "sudo snap revert $SNAP" || true - - # Wait for system reboot - wait_for_no_ssh - wait_for_ssh - - # Check the version of the snaps after the revert is correct - execute_remote "snap list $SNAP" | MATCH "^$SNAP .* 20/$CORE_CHANNEL .*"