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 .*"