summaryrefslogtreecommitdiff
diff options
-rw-r--r--asserts/asserts.go17
-rw-r--r--client/login.go2
-rw-r--r--cmd/snap-confine/mount-support.c14
-rw-r--r--cmd/snap/cmd_download.go28
-rw-r--r--cmd/snap/main_test.go2
-rw-r--r--cmd/snapctl/main_test.go4
-rw-r--r--daemon/api_test.go18
-rw-r--r--daemon/daemon_test.go12
-rw-r--r--image/export_test.go14
-rw-r--r--image/helpers.go91
-rw-r--r--image/image.go38
-rw-r--r--image/image_test.go35
-rw-r--r--interfaces/builtin/all.go2
-rw-r--r--interfaces/builtin/all_test.go2
-rw-r--r--interfaces/builtin/basedeclaration.go13
-rw-r--r--interfaces/builtin/basedeclaration_test.go6
-rw-r--r--interfaces/builtin/screen_inhibit_control.go10
-rw-r--r--interfaces/builtin/thumbnailer.go142
-rw-r--r--interfaces/builtin/thumbnailer_test.go84
-rw-r--r--interfaces/builtin/unity8.go71
-rw-r--r--interfaces/builtin/unity8_test.go56
-rwxr-xr-xmkversion.sh15
-rw-r--r--overlord/devicestate/devicemgr.go151
-rw-r--r--overlord/devicestate/devicemgr_test.go518
-rw-r--r--overlord/devicestate/export_test.go3
-rw-r--r--overlord/devicestate/firstboot.go36
-rw-r--r--overlord/devicestate/firstboot_test.go132
-rw-r--r--overlord/managers_test.go1
-rw-r--r--overlord/overlord_test.go20
-rw-r--r--overlord/snapstate/check_snap.go7
-rw-r--r--overlord/snapstate/check_snap_test.go28
-rw-r--r--overlord/snapstate/export_test.go6
-rw-r--r--overlord/snapstate/snapmgr.go26
-rw-r--r--overlord/snapstate/snapmgr_test.go69
-rw-r--r--overlord/snapstate/snapstate.go3
-rw-r--r--packaging/ubuntu-14.04/changelog8
-rw-r--r--packaging/ubuntu-16.04/changelog8
-rw-r--r--snap/gadget.go23
-rw-r--r--snap/gadget_test.go55
-rw-r--r--store/store.go17
-rw-r--r--tests/lib/assertions/developer1-my-classic-w-gadget.model20
-rw-r--r--tests/lib/assertions/developer1-my-classic.model19
-rwxr-xr-xtests/lib/changes.sh8
-rwxr-xr-xtests/lib/reset.sh13
-rw-r--r--tests/lib/snaps/classic-gadget/meta/gadget.yaml1
-rwxr-xr-xtests/lib/snaps/classic-gadget/meta/hooks/prepare-device2
-rw-r--r--tests/lib/snaps/classic-gadget/meta/icon.pngbin0 -> 3371 bytes
-rw-r--r--tests/lib/snaps/classic-gadget/meta/snap.yaml4
-rwxr-xr-xtests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-down3
-rwxr-xr-xtests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-up3
-rw-r--r--tests/lib/snaps/test-snapd-libvirt-consumer/snapcraft.yaml49
-rw-r--r--tests/lib/snaps/test-snapd-libvirt-consumer/vm/ping-unikernel.xml22
-rw-r--r--tests/main/classic-custom-device-reg/task.yaml75
-rw-r--r--tests/main/classic-firstboot/task.yaml72
-rw-r--r--tests/main/classic-ubuntu-core-transition-two-cores/task.yaml3
-rw-r--r--tests/main/classic-ubuntu-core-transition/task.yaml3
-rw-r--r--tests/main/interfaces-libvirt/task.yaml84
-rw-r--r--tests/main/refresh-all-undo/task.yaml9
-rw-r--r--tests/regression/lp-1665004/task.yaml13
59 files changed, 1712 insertions, 478 deletions
diff --git a/asserts/asserts.go b/asserts/asserts.go
index 32d7ca9b78..7616429623 100644
--- a/asserts/asserts.go
+++ b/asserts/asserts.go
@@ -331,15 +331,17 @@ var _ Assertion = (*assertionBase)(nil)
// where:
//
// HEADER is a set of header entries separated by "\n"
-// BODY can be arbitrary,
+// BODY can be arbitrary text,
// SIGNATURE is the signature
//
+// Both BODY and HEADER must be UTF8.
+//
// A header entry for a single line value (no '\n' in it) looks like:
//
// NAME ": " SIMPLEVALUE
//
// The format supports multiline text values (with '\n's in them) and
-// lists possibly nested with string scalars in them.
+// lists or maps, possibly nested, with string scalars in them.
//
// For those a header entry looks like:
//
@@ -348,12 +350,17 @@ var _ Assertion = (*assertionBase)(nil)
// where MULTI can be
//
// * (baseindent + 4)-space indented value (multiline text)
+//
// * entries of a list each of the form:
//
-// " "*baseindent " -" ( " " SIMPLEVALUE | "\n" MULTI )
+// " "*baseindent " -" ( " " SIMPLEVALUE | "\n" MULTI )
+//
+// * entries of map each of the form:
+//
+// " "*baseindent " " NAME ":" ( " " SIMPLEVALUE | "\n" MULTI )
//
// baseindent starts at 0 and then grows with nesting matching the
-// previous level introduction (the " "*baseindent " -" bit)
+// previous level introduction (e.g. the " "*baseindent " -" bit)
// length minus 1.
//
// In general the following headers are mandatory:
@@ -369,8 +376,10 @@ var _ Assertion = (*assertionBase)(nil)
//
// revision (a positive int)
// body-length (expected to be equal to the length of BODY)
+// format (a positive int for the format iteration of the type used)
//
// Times are expected to be in the RFC3339 format: "2006-01-02T15:04:05Z07:00".
+//
func Decode(serializedAssertion []byte) (Assertion, error) {
// copy to get an independent backstorage that can't be mutated later
assertionSnapshot := make([]byte, len(serializedAssertion))
diff --git a/client/login.go b/client/login.go
index d6dc4edf81..722ccf5d9b 100644
--- a/client/login.go
+++ b/client/login.go
@@ -87,7 +87,7 @@ func (client *Client) LoggedInUser() *User {
return u
}
-const authFileEnvKey = "SNAPPY_STORE_AUTH_DATA_FILENAME"
+const authFileEnvKey = "SNAPD_AUTH_DATA_FILENAME"
func storeAuthDataFilename(homeDir string) string {
if fn := os.Getenv(authFileEnvKey); fn != "" {
diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c
index a652731276..1d5361ccc0 100644
--- a/cmd/snap-confine/mount-support.c
+++ b/cmd/snap-confine/mount-support.c
@@ -429,6 +429,20 @@ static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config)
SC_HOSTFS_DIR);
}
}
+ // Ensure that hostfs isgroup owned by root. We may have (now or earlier)
+ // created the directory as the user who first ran a snap on a given
+ // system and the group identity of that user is visilbe on disk.
+ // This was LP:#1665004
+ struct stat sb;
+ if (stat(SC_HOSTFS_DIR, &sb) < 0) {
+ die("cannot stat %s", SC_HOSTFS_DIR);
+ }
+ if (sb.st_uid != 0 || sb.st_gid != 0) {
+ if (chown(SC_HOSTFS_DIR, 0, 0) < 0) {
+ die("cannot change user/group owner of %s to root",
+ SC_HOSTFS_DIR);
+ }
+ }
// Make the upcoming "put_old" directory for pivot_root private so that
// mount events don't propagate to any peer group. In practice pivot root
// has a number of undocumented requirements and one of them is that the
diff --git a/cmd/snap/cmd_download.go b/cmd/snap/cmd_download.go
index 93529d649f..324a90e7b0 100644
--- a/cmd/snap/cmd_download.go
+++ b/cmd/snap/cmd_download.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2017 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
@@ -31,9 +31,7 @@ import (
"github.com/snapcore/snapd/asserts/sysdb"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/image"
- "github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/snap"
- "github.com/snapcore/snapd/store"
)
type cmdDownload struct {
@@ -62,7 +60,7 @@ func init() {
}})
}
-func fetchSnapAssertions(sto *store.Store, snapPath string, snapInfo *snap.Info, dlOpts *image.DownloadOptions) error {
+func fetchSnapAssertions(tsto *image.ToolingStore, snapPath string, snapInfo *snap.Info) error {
db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
Backstore: asserts.NewMemoryBackstore(),
Trusted: sysdb.Trusted(),
@@ -82,7 +80,7 @@ func fetchSnapAssertions(sto *store.Store, snapPath string, snapInfo *snap.Info,
save := func(a asserts.Assertion) error {
return encoder.Encode(a)
}
- f := image.StoreAssertionFetcher(sto, dlOpts, db, save)
+ f := tsto.AssertionFetcher(db, save)
_, err = image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db)
return err
@@ -110,29 +108,23 @@ func (x *cmdDownload) Execute(args []string) error {
snapName := string(x.Positional.Snap)
- // FIXME: set auth context
- var authContext auth.AuthContext
- var user *auth.UserState
-
- sto := store.New(nil, authContext)
- // we always allow devmode for downloads
- devMode := true
+ tsto, err := image.NewToolingStore()
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(Stderr, i18n.G("Fetching snap %q\n"), snapName)
dlOpts := image.DownloadOptions{
TargetDir: "", // cwd
- DevMode: devMode,
Channel: x.Channel,
- User: user,
}
-
- fmt.Fprintf(Stderr, i18n.G("Fetching snap %q\n"), snapName)
- snapPath, snapInfo, err := image.DownloadSnap(sto, snapName, revision, &dlOpts)
+ snapPath, snapInfo, err := tsto.DownloadSnap(snapName, revision, &dlOpts)
if err != nil {
return err
}
fmt.Fprintf(Stderr, i18n.G("Fetching assertions for %q\n"), snapName)
- err = fetchSnapAssertions(sto, snapPath, snapInfo, &dlOpts)
+ err = fetchSnapAssertions(tsto, snapPath, snapInfo)
if err != nil {
return err
}
diff --git a/cmd/snap/main_test.go b/cmd/snap/main_test.go
index 888c36b4ef..a5e9c0ea6a 100644
--- a/cmd/snap/main_test.go
+++ b/cmd/snap/main_test.go
@@ -151,7 +151,7 @@ func mockVersion(v string) (restore func()) {
return func() { cmd.Version = old }
}
-const TestAuthFileEnvKey = "SNAPPY_STORE_AUTH_DATA_FILENAME"
+const TestAuthFileEnvKey = "SNAPD_AUTH_DATA_FILENAME"
const TestAuthFileContents = `{"id":123,"email":"hello@mail.com","macaroon":"MDAxM2xvY2F0aW9uIHNuYXBkCjAwMTJpZGVudGlmaWVyIDQzCjAwMmZzaWduYXR1cmUg5RfMua72uYop4t3cPOBmGUuaoRmoDH1HV62nMJq7eqAK"}`
func (s *SnapSuite) TestErrorResult(c *C) {
diff --git a/cmd/snapctl/main_test.go b/cmd/snapctl/main_test.go
index f50174872a..d41339057a 100644
--- a/cmd/snapctl/main_test.go
+++ b/cmd/snapctl/main_test.go
@@ -75,14 +75,14 @@ func (s *snapctlSuite) SetUpTest(c *C) {
s.expectedArgs = []string{}
fakeAuthPath := filepath.Join(c.MkDir(), "auth.json")
- os.Setenv("SNAPPY_STORE_AUTH_DATA_FILENAME", fakeAuthPath)
+ os.Setenv("SNAPD_AUTH_DATA_FILENAME", fakeAuthPath)
err := ioutil.WriteFile(fakeAuthPath, []byte(`{"macaroon":"user-macaroon"}`), 0644)
c.Assert(err, IsNil)
}
func (s *snapctlSuite) TearDownTest(c *C) {
os.Unsetenv("SNAP_CONTEXT")
- os.Unsetenv("SNAPPY_STORE_AUTH_DATA_FILENAME")
+ os.Unsetenv("SNAPD_AUTH_DATA_FILENAME")
clientConfig.BaseURL = ""
s.server.Close()
os.Args = s.oldArgs
diff --git a/daemon/api_test.go b/daemon/api_test.go
index d3daa16445..643d9120b4 100644
--- a/daemon/api_test.go
+++ b/daemon/api_test.go
@@ -143,8 +143,8 @@ func (s *apiBaseSuite) SetUpSuite(c *check.C) {
VersionID: "mocked",
})
- snapstate.CanAutoRefresh = func(*state.State) bool {
- return false
+ snapstate.CanAutoRefresh = func(*state.State) (bool, error) {
+ return false, nil
}
}
@@ -229,6 +229,8 @@ func (s *apiBaseSuite) daemon(c *check.C) *Daemon {
st.Lock()
defer st.Unlock()
snapstate.ReplaceStore(st, s)
+ // mark as already seeded
+ st.Set("seeded", true)
s.d = d
return d
@@ -1793,7 +1795,7 @@ func (s *apiSuite) TestSideloadSnapJailModeAndDevmode(c *check.C) {
"\r\n" +
"true\r\n" +
"----hello--\r\n"
- d := newTestDaemon(c)
+ d := s.daemon(c)
d.overlord.Loop()
defer d.overlord.Stop()
@@ -1817,7 +1819,7 @@ func (s *apiSuite) TestSideloadSnapJailModeInDevModeOS(c *check.C) {
"\r\n" +
"true\r\n" +
"----hello--\r\n"
- d := newTestDaemon(c)
+ d := s.daemon(c)
d.overlord.Loop()
defer d.overlord.Stop()
@@ -1834,7 +1836,7 @@ func (s *apiSuite) TestSideloadSnapJailModeInDevModeOS(c *check.C) {
}
func (s *apiSuite) TestLocalInstallSnapDeriveSideInfo(c *check.C) {
- d := newTestDaemon(c)
+ d := s.daemon(c)
d.overlord.Loop()
defer d.overlord.Stop()
// add the assertions first
@@ -1916,7 +1918,7 @@ func (s *apiSuite) TestSideloadSnapNoSignaturesDangerOff(c *check.C) {
"\r\n" +
"xyzzy\r\n" +
"----hello--\r\n"
- d := newTestDaemon(c)
+ d := s.daemon(c)
d.overlord.Loop()
defer d.overlord.Stop()
@@ -1959,7 +1961,7 @@ func (s *apiSuite) TestSideloadSnapNotValidFormFile(c *check.C) {
}
func (s *apiSuite) TestTrySnap(c *check.C) {
- d := newTestDaemon(c)
+ d := s.daemon(c)
d.overlord.Loop()
defer d.overlord.Stop()
@@ -2074,7 +2076,7 @@ func (s *apiSuite) TestTrySnapNotDir(c *check.C) {
}
func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedFlags snapstate.Flags, hasCoreSnap bool) string {
- d := newTestDaemon(c)
+ d := s.daemon(c)
d.overlord.Loop()
defer d.overlord.Stop()
diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go
index 897ab874f8..4543c7463b 100644
--- a/daemon/daemon_test.go
+++ b/daemon/daemon_test.go
@@ -225,6 +225,12 @@ func (l *witnessAcceptListener) Accept() (net.Conn, error) {
func (s *daemonSuite) TestStartStop(c *check.C) {
d := newTestDaemon(c)
+ st := d.overlord.State()
+ // mark as already seeded
+ st.Lock()
+ st.Set("seeded", true)
+ st.Unlock()
+
l, err := net.Listen("tcp", "127.0.0.1:0")
c.Assert(err, check.IsNil)
@@ -265,6 +271,12 @@ func (s *daemonSuite) TestStartStop(c *check.C) {
func (s *daemonSuite) TestRestartWiring(c *check.C) {
d := newTestDaemon(c)
+ // mark as already seeded
+ st := d.overlord.State()
+ st.Lock()
+ st.Set("seeded", true)
+ st.Unlock()
+
l, err := net.Listen("tcp", "127.0.0.1:0")
c.Assert(err, check.IsNil)
diff --git a/image/export_test.go b/image/export_test.go
index d7452ba569..021c55f9a3 100644
--- a/image/export_test.go
+++ b/image/export_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2017 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
@@ -19,6 +19,14 @@
package image
+import (
+ "github.com/snapcore/snapd/overlord/auth"
+)
+
+func MockToolingStore(sto Store) *ToolingStore {
+ return &ToolingStore{sto: sto}
+}
+
var (
LocalSnaps = localSnaps
DecodeModelAssertion = decodeModelAssertion
@@ -26,3 +34,7 @@ var (
BootstrapToRootDir = bootstrapToRootDir
InstallCloudConfig = installCloudConfig
)
+
+func (tsto *ToolingStore) User() *auth.UserState {
+ return tsto.user
+}
diff --git a/image/helpers.go b/image/helpers.go
index e9f630cbf2..2e645c9746 100644
--- a/image/helpers.go
+++ b/image/helpers.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2016 Canonical Ltd
+ * Copyright (C) 2014-2017 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
@@ -22,6 +22,7 @@ package image
// TODO: put these in appropriate package(s) once they are clarified a bit more
import (
+ "encoding/json"
"fmt"
"os"
"path/filepath"
@@ -37,14 +38,6 @@ import (
"golang.org/x/net/context"
)
-// DownloadOptions carries options for downloading snaps plus assertions.
-type DownloadOptions struct {
- TargetDir string
- Channel string
- DevMode bool
- User *auth.UserState
-}
-
// A Store can find metadata on snaps, download snaps and fetch assertions.
type Store interface {
SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error)
@@ -53,11 +46,79 @@ type Store interface {
Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error)
}
+// ToolingStore wraps access to the store for tools.
+type ToolingStore struct {
+ sto Store
+ user *auth.UserState
+}
+
+func newToolingStore(arch, storeID string) (*ToolingStore, error) {
+ cfg := store.DefaultConfig()
+ cfg.Architecture = arch
+ cfg.StoreID = storeID
+ var user *auth.UserState
+ if authFn := os.Getenv("UBUNTU_STORE_AUTH_DATA_FILENAME"); authFn != "" {
+ var err error
+ user, err = readAuthFile(authFn)
+ if err != nil {
+ return nil, err
+ }
+ }
+ sto := store.New(cfg, nil)
+ return &ToolingStore{
+ sto: sto,
+ user: user,
+ }, nil
+}
+
+type authData struct {
+ Macaroon string `json:"macaroon"`
+ Discharges []string `json:"discharges"`
+}
+
+func readAuthFile(authFn string) (*auth.UserState, error) {
+ f, err := os.Open(authFn)
+ if err != nil {
+ return nil, fmt.Errorf("cannot open auth file %q: %v", authFn, err)
+ }
+ defer f.Close()
+
+ var creds authData
+ dec := json.NewDecoder(f)
+ if err := dec.Decode(&creds); err != nil {
+ return nil, fmt.Errorf("cannot decode auth file %q: %v", authFn, err)
+ }
+ if creds.Macaroon == "" || len(creds.Discharges) == 0 {
+ return nil, fmt.Errorf("invalid auth file %q: missing fields", authFn)
+ }
+ return &auth.UserState{
+ StoreMacaroon: creds.Macaroon,
+ StoreDischarges: creds.Discharges,
+ }, nil
+}
+
+func NewToolingStoreFromModel(model *asserts.Model) (*ToolingStore, error) {
+ return newToolingStore(model.Architecture(), model.Store())
+}
+
+func NewToolingStore() (*ToolingStore, error) {
+ arch := os.Getenv("UBUNTU_STORE_ARCH")
+ storeID := os.Getenv("UBUNTU_STORE_ID")
+ return newToolingStore(arch, storeID)
+}
+
+// DownloadOptions carries options for downloading snaps plus assertions.
+type DownloadOptions struct {
+ TargetDir string
+ Channel string
+}
+
// DownloadSnap downloads the snap with the given name and optionally revision using the provided store and options. It returns the final full path of the snap inside the opts.TargetDir and a snap.Info for the snap.
-func DownloadSnap(sto Store, name string, revision snap.Revision, opts *DownloadOptions) (targetFn string, info *snap.Info, err error) {
+func (tsto *ToolingStore) DownloadSnap(name string, revision snap.Revision, opts *DownloadOptions) (targetFn string, info *snap.Info, err error) {
if opts == nil {
opts = &DownloadOptions{}
}
+ sto := tsto.sto
targetDir := opts.TargetDir
if targetDir == "" {
@@ -73,7 +134,7 @@ func DownloadSnap(sto Store, name string, revision snap.Revision, opts *Download
Channel: opts.Channel,
Revision: revision,
}
- snap, err := sto.SnapInfo(spec, opts.User)
+ snap, err := sto.SnapInfo(spec, tsto.user)
if err != nil {
return "", nil, fmt.Errorf("cannot find snap %q: %v", name, err)
}
@@ -82,17 +143,17 @@ func DownloadSnap(sto Store, name string, revision snap.Revision, opts *Download
targetFn = filepath.Join(targetDir, baseName)
pb := progress.NewTextProgress()
- if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, opts.User); err != nil {
+ if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, tsto.user); err != nil {
return "", nil, err
}
return targetFn, snap, nil
}
-// StoreAssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them.
-func StoreAssertionFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher {
+// AssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them.
+func (tsto *ToolingStore) AssertionFetcher(db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher {
retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
- return sto.Assertion(ref.Type, ref.PrimaryKey, dlOpts.User)
+ return tsto.sto.Assertion(ref.Type, ref.PrimaryKey, tsto.user)
}
save2 := func(a asserts.Assertion) error {
// for checking
diff --git a/image/image.go b/image/image.go
index 85bb90ad8b..723002f45e 100644
--- a/image/image.go
+++ b/image/image.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2016 Canonical Ltd
+ * Copyright (C) 2014-2017 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
@@ -37,7 +37,6 @@ import (
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/squashfs"
- "github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/strutil"
)
@@ -131,13 +130,16 @@ func Prepare(opts *Options) error {
if model.Series() != release.Series {
return fmt.Errorf("model with series %q != %q unsupported", model.Series(), release.Series)
}
- sto := makeStore(model)
+ tsto, err := NewToolingStoreFromModel(model)
+ if err != nil {
+ return err
+ }
- if err := downloadUnpackGadget(sto, model, opts, local); err != nil {
+ if err := downloadUnpackGadget(tsto, model, opts, local); err != nil {
return err
}
- return bootstrapToRootDir(sto, model, opts, local)
+ return bootstrapToRootDir(tsto, model, opts, local)
}
// these are postponed, not implemented or abandoned, not finalized,
@@ -170,7 +172,7 @@ func decodeModelAssertion(opts *Options) (*asserts.Model, error) {
return modela, nil
}
-func downloadUnpackGadget(sto Store, model *asserts.Model, opts *Options, local *localInfos) error {
+func downloadUnpackGadget(tsto *ToolingStore, model *asserts.Model, opts *Options, local *localInfos) error {
if err := os.MkdirAll(opts.GadgetUnpackDir, 0755); err != nil {
return fmt.Errorf("cannot create gadget unpack dir %q: %s", opts.GadgetUnpackDir, err)
}
@@ -179,7 +181,7 @@ func downloadUnpackGadget(sto Store, model *asserts.Model, opts *Options, local
TargetDir: opts.GadgetUnpackDir,
Channel: opts.Channel,
}
- snapFn, _, err := acquireSnap(sto, model.Gadget(), dlOpts, local)
+ snapFn, _, err := acquireSnap(tsto, model.Gadget(), dlOpts, local)
if err != nil {
return err
}
@@ -189,7 +191,7 @@ func downloadUnpackGadget(sto Store, model *asserts.Model, opts *Options, local
return snap.Unpack("*", opts.GadgetUnpackDir)
}
-func acquireSnap(sto Store, name string, dlOpts *DownloadOptions, local *localInfos) (downloadedSnap string, info *snap.Info, err error) {
+func acquireSnap(tsto *ToolingStore, name string, dlOpts *DownloadOptions, local *localInfos) (downloadedSnap string, info *snap.Info, err error) {
if info := local.Info(name); info != nil {
// local snap to install (unasserted only for now)
p := local.Path(name)
@@ -199,7 +201,7 @@ func acquireSnap(sto Store, name string, dlOpts *DownloadOptions, local *localIn
}
return dst, info, nil
}
- return DownloadSnap(sto, name, snap.R(0), dlOpts)
+ return tsto.DownloadSnap(name, snap.R(0), dlOpts)
}
type addingFetcher struct {
@@ -207,13 +209,13 @@ type addingFetcher struct {
addedRefs []*asserts.Ref
}
-func makeFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database) *addingFetcher {
+func makeFetcher(tsto *ToolingStore, dlOpts *DownloadOptions, db *asserts.Database) *addingFetcher {
var f addingFetcher
save := func(a asserts.Assertion) error {
f.addedRefs = append(f.addedRefs, a.Ref())
return nil
}
- f.Fetcher = StoreAssertionFetcher(sto, dlOpts, db, save)
+ f.Fetcher = tsto.AssertionFetcher(db, save)
return &f
}
@@ -249,7 +251,7 @@ func MockTrusted(mockTrusted []asserts.Assertion) (restore func()) {
}
}
-func bootstrapToRootDir(sto Store, model *asserts.Model, opts *Options, local *localInfos) error {
+func bootstrapToRootDir(tsto *ToolingStore, model *asserts.Model, opts *Options, local *localInfos) error {
// FIXME: try to avoid doing this
if opts.RootDir != "" {
dirs.SetRootDir(opts.RootDir)
@@ -270,7 +272,7 @@ func bootstrapToRootDir(sto Store, model *asserts.Model, opts *Options, local *l
if err != nil {
return err
}
- f := makeFetcher(sto, &DownloadOptions{}, db)
+ f := makeFetcher(tsto, &DownloadOptions{}, db)
if err := f.Save(model); err != nil {
if !osutil.GetenvBool("UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_MODEL") {
@@ -291,7 +293,6 @@ func bootstrapToRootDir(sto Store, model *asserts.Model, opts *Options, local *l
dlOpts := &DownloadOptions{
TargetDir: snapSeedDir,
Channel: opts.Channel,
- DevMode: false, // XXX: should this be true?
}
for _, d := range []string{snapSeedDir, assertSeedDir} {
@@ -328,7 +329,7 @@ func bootstrapToRootDir(sto Store, model *asserts.Model, opts *Options, local *l
fmt.Fprintf(Stdout, "Fetching %s\n", snapName)
}
- fn, info, err := acquireSnap(sto, name, dlOpts, local)
+ fn, info, err := acquireSnap(tsto, name, dlOpts, local)
if err != nil {
return err
}
@@ -500,10 +501,3 @@ func copyLocalSnapFile(snapPath, targetDir string, info *snap.Info) (dstPath str
dst := filepath.Join(targetDir, filepath.Base(info.MountFile()))
return dst, osutil.CopyFile(snapPath, dst, 0)
}
-
-func makeStore(model *asserts.Model) Store {
- cfg := store.DefaultConfig()
- cfg.Architecture = model.Architecture()
- cfg.StoreID = model.Store()
- return store.New(cfg, nil)
-}
diff --git a/image/image_test.go b/image/image_test.go
index 654ad65c8e..843842c235 100644
--- a/image/image_test.go
+++ b/image/image_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2016 Canonical Ltd
+ * Copyright (C) 2014-2017 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
@@ -58,6 +58,7 @@ type imageSuite struct {
downloadedSnaps map[string]string
storeSnapInfo map[string]*snap.Info
+ tsto *image.ToolingStore
storeSigning *assertstest.StoreStack
brandSigning *assertstest.SigningDB
@@ -78,6 +79,7 @@ func (s *imageSuite) SetUpTest(c *C) {
image.Stderr = s.stderr
s.downloadedSnaps = make(map[string]string)
s.storeSnapInfo = make(map[string]*snap.Info)
+ s.tsto = image.MockToolingStore(s)
rootPrivKey, _ := assertstest.GenerateKey(1024)
storePrivKey, _ := assertstest.GenerateKey(752)
@@ -279,7 +281,7 @@ func (s *imageSuite) TestHappyDecodeModelAssertion(c *C) {
}
func (s *imageSuite) TestMissingGadgetUnpackDir(c *C) {
- err := image.DownloadUnpackGadget(s, s.model, &image.Options{}, nil)
+ err := image.DownloadUnpackGadget(s.tsto, s.model, &image.Options{}, nil)
c.Assert(err, ErrorMatches, `cannot create gadget unpack dir "": mkdir : no such file or directory`)
}
@@ -308,7 +310,7 @@ func (s *imageSuite) TestDownloadUnpackGadget(c *C) {
local, err := image.LocalSnaps(opts)
c.Assert(err, IsNil)
- err = image.DownloadUnpackGadget(s, s.model, opts, local)
+ err = image.DownloadUnpackGadget(s.tsto, s.model, opts, local)
c.Assert(err, IsNil)
// verify the right data got unpacked
@@ -374,7 +376,7 @@ func (s *imageSuite) TestBootstrapToRootDir(c *C) {
local, err := image.LocalSnaps(opts)
c.Assert(err, IsNil)
- err = image.BootstrapToRootDir(s, s.model, opts, local)
+ err = image.BootstrapToRootDir(s.tsto, s.model, opts, local)
c.Assert(err, IsNil)
// check seed yaml
@@ -466,7 +468,7 @@ func (s *imageSuite) TestBootstrapToRootDirLocalCoreBrandKernel(c *C) {
local, err := image.LocalSnaps(opts)
c.Assert(err, IsNil)
- err = image.BootstrapToRootDir(s, s.model, opts, local)
+ err = image.BootstrapToRootDir(s.tsto, s.model, opts, local)
c.Assert(err, IsNil)
// check seed yaml
@@ -592,7 +594,7 @@ func (s *imageSuite) TestBootstrapToRootDirDevmodeSnap(c *C) {
local, err := image.LocalSnaps(opts)
c.Assert(err, IsNil)
- err = image.BootstrapToRootDir(s, s.model, opts, local)
+ err = image.BootstrapToRootDir(s.tsto, s.model, opts, local)
c.Assert(err, IsNil)
// check seed yaml
@@ -649,7 +651,7 @@ func (s *imageSuite) TestBootstrapToRootDirKernelPublisherMismatch(c *C) {
local, err := image.LocalSnaps(opts)
c.Assert(err, IsNil)
- err = image.BootstrapToRootDir(s, s.model, opts, local)
+ err = image.BootstrapToRootDir(s.tsto, s.model, opts, local)
c.Assert(err, ErrorMatches, `cannot use kernel "pc-kernel" published by "other" for model by "my-brand"`)
}
@@ -678,3 +680,22 @@ func (s *imageSuite) TestInstallCloudConfigWithCloudConfig(c *C) {
c.Assert(err, IsNil)
c.Check(content, DeepEquals, canary)
}
+
+func (s *imageSuite) TestNewToolingStoreWithAuth(c *C) {
+ tmpdir := c.MkDir()
+ authFn := filepath.Join(tmpdir, "auth.json")
+ err := ioutil.WriteFile(authFn, []byte(`{
+"macaroon": "MACAROON",
+"discharges": ["DISCHARGE"]
+}`), 0600)
+ c.Assert(err, IsNil)
+
+ os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn)
+ defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME")
+
+ tsto, err := image.NewToolingStore()
+ c.Assert(err, IsNil)
+ user := tsto.User()
+ c.Check(user.StoreMacaroon, Equals, "MACAROON")
+ c.Check(user.StoreDischarges, DeepEquals, []string{"DISCHARGE"})
+}
diff --git a/interfaces/builtin/all.go b/interfaces/builtin/all.go
index 264002d8ca..917d71f926 100644
--- a/interfaces/builtin/all.go
+++ b/interfaces/builtin/all.go
@@ -52,9 +52,11 @@ var allInterfaces = []interfaces.Interface{
&PppInterface{},
&PulseAudioInterface{},
&SerialPortInterface{},
+ &ThumbnailerInterface{},
&TimeControlInterface{},
&UDisks2Interface{},
&UbuntuDownloadManagerInterface{},
+ &Unity8Interface{},
&UpowerObserveInterface{},
&UhidInterface{},
NewAccountControlInterface(),
diff --git a/interfaces/builtin/all_test.go b/interfaces/builtin/all_test.go
index 6f2fb7ce35..df53945079 100644
--- a/interfaces/builtin/all_test.go
+++ b/interfaces/builtin/all_test.go
@@ -54,9 +54,11 @@ func (s *AllSuite) TestInterfaces(c *C) {
c.Check(all, Contains, &builtin.PhysicalMemoryObserveInterface{})
c.Check(all, Contains, &builtin.PulseAudioInterface{})
c.Check(all, Contains, &builtin.SerialPortInterface{})
+ c.Check(all, Contains, &builtin.ThumbnailerInterface{})
c.Check(all, Contains, &builtin.TimeControlInterface{})
c.Check(all, Contains, &builtin.UDisks2Interface{})
c.Check(all, Contains, &builtin.UbuntuDownloadManagerInterface{})
+ c.Check(all, Contains, &builtin.Unity8Interface{})
c.Check(all, Contains, &builtin.UpowerObserveInterface{})
c.Check(all, Contains, &builtin.UhidInterface{})
c.Check(all, DeepContains, builtin.NewAccountControlInterface())
diff --git a/interfaces/builtin/basedeclaration.go b/interfaces/builtin/basedeclaration.go
index ae11488e67..c2cf118ac4 100644
--- a/interfaces/builtin/basedeclaration.go
+++ b/interfaces/builtin/basedeclaration.go
@@ -157,6 +157,8 @@ plugs:
snapd-control:
allow-installation: false
deny-auto-connection: true
+ unity8:
+ allow-installation: false
slots:
account-control:
allow-installation:
@@ -508,6 +510,12 @@ slots:
slot-snap-type:
- core
deny-auto-connection: true
+ thumbnailer:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-auto-connection: true
+ deny-connection: true
time-control:
allow-installation:
slot-snap-type:
@@ -543,6 +551,11 @@ slots:
allow-installation:
slot-snap-type:
- core
+ unity8:
+ allow-installation:
+ slot-snap-type:
+ - app
+ deny-connection: true
unity8-calendar:
allow-installation:
slot-snap-type:
diff --git a/interfaces/builtin/basedeclaration_test.go b/interfaces/builtin/basedeclaration_test.go
index ee61d1d9ac..1b96ff2e3e 100644
--- a/interfaces/builtin/basedeclaration_test.go
+++ b/interfaces/builtin/basedeclaration_test.go
@@ -147,6 +147,7 @@ func (s *baseDeclSuite) TestAutoConnection(c *C) {
"pulseaudio": true,
"screen-inhibit-control": true,
"unity7": true,
+ "unity8": true,
"ubuntu-download-manager": true,
"upower-observe": true,
"x11": true,
@@ -436,8 +437,10 @@ var (
"ppp": {"core"},
"pulseaudio": {"app", "core"},
"serial-port": {"core", "gadget"},
+ "thumbnailer": {"app"},
"udisks2": {"app"},
"uhid": {"core"},
+ "unity8": {"app"},
"unity8-calendar": {"app"},
"unity8-contacts": {"app"},
"ubuntu-download-manager": {"app"},
@@ -528,6 +531,7 @@ func (s *baseDeclSuite) TestPlugInstallation(c *C) {
"kernel-module-control": true,
"lxd-support": true,
"snapd-control": true,
+ "unity8": true,
}
for _, iface := range all {
@@ -574,6 +578,7 @@ func (s *baseDeclSuite) TestConnection(c *C) {
"location-observe": true,
"lxd": true,
"mir": true,
+ "thumbnailer": true,
"udisks2": true,
"unity8-calendar": true,
"unity8-contacts": true,
@@ -647,6 +652,7 @@ func (s *baseDeclSuite) TestSanity(c *C) {
"kernel-module-control": true,
"lxd-support": true,
"snapd-control": true,
+ "unity8": true,
}
for _, iface := range all {
diff --git a/interfaces/builtin/screen_inhibit_control.go b/interfaces/builtin/screen_inhibit_control.go
index 929f6d40c2..c4145a540b 100644
--- a/interfaces/builtin/screen_inhibit_control.go
+++ b/interfaces/builtin/screen_inhibit_control.go
@@ -55,7 +55,15 @@ dbus (send)
bus=session
path=/Screensaver
interface=org.freedesktop.ScreenSaver
- member=org.freedesktop.ScreenSaver.{Inhibit,UnInhibit}
+ member=org.freedesktop.ScreenSaver.{Inhibit,UnInhibit,SimulateUserActivity}
+ peer=(label=unconfined),
+
+# gnome, kde and cinnamon screensaver
+dbus (send)
+ bus=session
+ path=/{,ScreenSaver}
+ interface=org.{gnome.ScreenSaver,kde.screensaver,cinnamon.ScreenSaver}
+ member=SimulateUserActivity
peer=(label=unconfined),
`
diff --git a/interfaces/builtin/thumbnailer.go b/interfaces/builtin/thumbnailer.go
new file mode 100644
index 0000000000..d4fea8bfda
--- /dev/null
+++ b/interfaces/builtin/thumbnailer.go
@@ -0,0 +1,142 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2017 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"
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+const thumbnailerPermanentSlotAppArmor = `
+# Description: Allow use of aa_query_label API. This
+# discloses the AppArmor policy for all processes.
+
+/sys/module/apparmor/parameters/enabled r,
+@{PROC}/@{pid}/mounts r,
+/sys/kernel/security/apparmor/.access rw,
+
+# Description: Allow owning the Thumbnailer bus name on the session bus
+
+#include <abstractions/dbus-session-strict>
+
+dbus (send)
+ bus=session
+ path=/org/freedesktop/DBus
+ interface=org.freedesktop.DBus
+ member={RequestName,ReleaseName,GetConnectionCredentials}
+ peer=(name=org.freedesktop.DBus, label=unconfined),
+
+dbus (bind)
+ bus=session
+ name=com.canonical.Thumbnailer,
+`
+
+const thumbnailerConnectedSlotAppArmor = `
+# Description: Allow access to plug's data directory.
+
+@{INSTALL_DIR}/###PLUG_SNAP_NAME###/** r,
+owner @{HOME}/snap/###PLUG_SNAP_NAME###/** r,
+/var/snap/###PLUG_SNAP_NAME###/** r,
+
+# Description: allow client snaps to access the thumbnailer service.
+dbus (receive, send)
+ bus=session
+ interface=com.canonical.Thumbnailer
+ path=/com/canonical/Thumbnailer
+ peer=(label=###PLUG_SECURITY_TAGS###),
+`
+
+const thumbnailerConnectedPlugAppArmor = `
+# Description: allow access to the thumbnailer D-Bus service.
+
+#include <abstractions/dbus-session-strict>
+
+dbus (receive, send)
+ bus=session
+ interface=com.canonical.Thumbnailer
+ path=/com/canonical/Thumbnailer
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`
+
+type ThumbnailerInterface struct{}
+
+func (iface *ThumbnailerInterface) Name() string {
+ return "thumbnailer"
+}
+
+func (iface *ThumbnailerInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *ThumbnailerInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ snippet := []byte(thumbnailerConnectedPlugAppArmor)
+ old := []byte("###SLOT_SECURITY_TAGS###")
+ new := slotAppLabelExpr(slot)
+ snippet = bytes.Replace(snippet, old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *ThumbnailerInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ return []byte(thumbnailerPermanentSlotAppArmor), nil
+ }
+ return nil, nil
+}
+
+func (iface *ThumbnailerInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ switch securitySystem {
+ case interfaces.SecurityAppArmor:
+ snippet := []byte(thumbnailerConnectedSlotAppArmor)
+ old := []byte("###PLUG_SNAP_NAME###")
+ new := []byte(plug.Snap.Name())
+ snippet = bytes.Replace(snippet, old, new, -1)
+
+ old = []byte("###PLUG_SECURITY_TAGS###")
+ new = plugAppLabelExpr(plug)
+ snippet = bytes.Replace(snippet, old, new, -1)
+ return snippet, nil
+ }
+ return nil, nil
+}
+
+func (iface *ThumbnailerInterface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("plug is not of interface %q", iface.Name()))
+ }
+ return nil
+}
+
+func (iface *ThumbnailerInterface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface.Name()))
+ }
+ return nil
+}
+
+func (iface *ThumbnailerInterface) AutoConnect(plug *interfaces.Plug, slot *interfaces.Slot) bool {
+ return true
+}
diff --git a/interfaces/builtin/thumbnailer_test.go b/interfaces/builtin/thumbnailer_test.go
new file mode 100644
index 0000000000..88ad8a32a2
--- /dev/null
+++ b/interfaces/builtin/thumbnailer_test.go
@@ -0,0 +1,84 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2017 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 (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type ThumbnailerInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&ThumbnailerInterfaceSuite{
+ iface: &builtin.ThumbnailerInterface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "server", Type: snap.TypeOS},
+ Name: "thumbnailer",
+ Interface: "thumbnailer",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "client"},
+ Name: "thumbnailer",
+ Interface: "thumbnailer",
+ },
+ },
+})
+
+func (s *ThumbnailerInterfaceSuite) TestName(c *C) {
+ c.Check(s.iface.Name(), Equals, "thumbnailer")
+}
+
+func (s *ThumbnailerInterfaceSuite) TestSanitizeIncorrectInterface(c *C) {
+ c.Check(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) },
+ PanicMatches, `slot is not of interface "thumbnailer"`)
+ c.Check(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) },
+ PanicMatches, `plug is not of interface "thumbnailer"`)
+}
+
+func (s *ThumbnailerInterfaceSuite) TestUsedSecuritySystems(c *C) {
+ // connected slots have a non-nil security snippet for apparmor
+ snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+ // slots have a permanent non-nil security snippet for apparmor
+ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+ c.Assert(snippet, Not(IsNil))
+}
+
+func (s *ThumbnailerInterfaceSuite) TestSlotGrantedAccessToPlugFiles(c *C) {
+ snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor)
+ c.Assert(err, IsNil)
+
+ c.Check(string(snippet), testutil.Contains, `@{INSTALL_DIR}/client/**`)
+ c.Check(string(snippet), testutil.Contains, `@{HOME}/snap/client/**`)
+ c.Check(string(snippet), testutil.Contains, `/var/snap/client/**`)
+}
diff --git a/interfaces/builtin/unity8.go b/interfaces/builtin/unity8.go
new file mode 100644
index 0000000000..a917fc8f7d
--- /dev/null
+++ b/interfaces/builtin/unity8.go
@@ -0,0 +1,71 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2017 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 (
+ "fmt"
+
+ "github.com/snapcore/snapd/interfaces"
+)
+
+type Unity8Interface struct{}
+
+func (iface *Unity8Interface) Name() string {
+ return "unity8"
+}
+
+func (iface *Unity8Interface) String() string {
+ return iface.Name()
+}
+
+func (iface *Unity8Interface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *Unity8Interface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *Unity8Interface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *Unity8Interface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) {
+ return nil, nil
+}
+
+func (iface *Unity8Interface) SanitizePlug(plug *interfaces.Plug) error {
+ if iface.Name() != plug.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+ return nil
+}
+
+func (iface *Unity8Interface) SanitizeSlot(slot *interfaces.Slot) error {
+ if iface.Name() != slot.Interface {
+ panic(fmt.Sprintf("slot is not of interface %q", iface))
+ }
+ return nil
+}
+
+func (iface *Unity8Interface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool {
+ // allow what declarations allowed
+ return true
+}
diff --git a/interfaces/builtin/unity8_test.go b/interfaces/builtin/unity8_test.go
new file mode 100644
index 0000000000..cfe9adad08
--- /dev/null
+++ b/interfaces/builtin/unity8_test.go
@@ -0,0 +1,56 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2017 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 (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+)
+
+type unity8InterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.Slot
+ plug *interfaces.Plug
+}
+
+var _ = Suite(&unity8InterfaceSuite{
+ iface: &builtin.Unity8Interface{},
+ slot: &interfaces.Slot{
+ SlotInfo: &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "unity8-session"},
+ Name: "unity8-session",
+ Interface: "unity8",
+ },
+ },
+ plug: &interfaces.Plug{
+ PlugInfo: &snap.PlugInfo{
+ Snap: &snap.Info{SuggestedName: "unity8-session"},
+ Name: "unity8-app",
+ Interface: "unity8",
+ },
+ },
+})
+
+func (s *unity8InterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "unity8")
+}
diff --git a/mkversion.sh b/mkversion.sh
index 353590ab12..42714b82f5 100755
--- a/mkversion.sh
+++ b/mkversion.sh
@@ -26,9 +26,18 @@ if [ "$GOPACKAGE" = "cmd" ]; then
GO_GENERATE_BUILDDIR="$(pwd)/.."
fi
-if which git >/dev/null; then
- v="$(git describe --dirty --always | sed -e 's/-/+git/;y/-/./' )"
- o=git
+# If the version is passed in as an argument to mkversion.sh, let's use that.
+if [ ! -z "$1" ]; then
+ v="$1"
+ o=shell
+fi
+
+if [ -z "$v" ]; then
+ # Let's try to derive the version from git..
+ if which git >/dev/null; then
+ v="$(git describe --dirty --always | sed -e 's/-/+git/;y/-/./' )"
+ o=git
+ fi
fi
if [ -z "$v" ]; then
diff --git a/overlord/devicestate/devicemgr.go b/overlord/devicestate/devicemgr.go
index c8d8b2ebeb..e96c68fae3 100644
--- a/overlord/devicestate/devicemgr.go
+++ b/overlord/devicestate/devicemgr.go
@@ -30,8 +30,6 @@ import (
"fmt"
"net/http"
"net/url"
- "os"
- "path/filepath"
"regexp"
"strings"
"time"
@@ -116,6 +114,21 @@ func (m *DeviceManager) changeInFlight(kind string) bool {
return false
}
+// helpers to keep count of attempts to get a serial, useful to decide
+// to give up holding off trying to auto-refresh
+
+type ensureOperationalAttemptsKey struct{}
+
+func incEnsureOperationalAttempts(st *state.State) {
+ cur, _ := st.Cached(ensureOperationalAttemptsKey{}).(int)
+ st.Cache(ensureOperationalAttemptsKey{}, cur+1)
+}
+
+func ensureOperationalAttempts(st *state.State) int {
+ cur, _ := st.Cached(ensureOperationalAttemptsKey{}).(int)
+ return cur
+}
+
// ensureOperationalShouldBackoff returns whether we should abstain from
// further become-operational tentatives while its backoff interval is
// not expired.
@@ -151,13 +164,12 @@ func (m *DeviceManager) ensureOperational() error {
}
if device.Brand == "" || device.Model == "" {
- // need first-boot, loading of model assertion info
- if release.OnClassic {
- // TODO: are we going to have model assertions on classic or need will need to cheat here?
- return nil
- }
- // cannot proceed yet, once first boot is done these will be set
- // and we can pick up from there
+ // cannot proceed until seeding has loaded the model
+ // assertion and set the brand and model, that is
+ // optional on classic
+ // TODO: later we can check if
+ // "seeded" was set and we still don't have a brand/model
+ // and use a fallback assertion
return nil
}
@@ -165,6 +177,9 @@ func (m *DeviceManager) ensureOperational() error {
return nil
}
+ // TODO: make presence of gadget optional on classic? that is
+ // sensible only for devices that the store can give directly
+ // serials to and when we will have a general fallback
gadgetInfo, err := snapstate.GadgetInfo(m.state)
if err == state.ErrNoState {
// no gadget installed yet, cannot proceed
@@ -178,6 +193,8 @@ func (m *DeviceManager) ensureOperational() error {
if m.ensureOperationalShouldBackoff(time.Now()) {
return nil
}
+ // increment attempt count
+ incEnsureOperationalAttempts(m.state)
// XXX: some of these will need to be split and use hooks
// retries might need to embrace more than one "task" then,
@@ -219,17 +236,6 @@ func (m *DeviceManager) ensureSeedYaml() error {
m.state.Lock()
defer m.state.Unlock()
- // FIXME: enable on classic?
- //
- // Disable seed.yaml on classic for now. In the long run we want
- // classic to have a seed parsing as well so that we can install
- // snaps in a classic environment (LP: #1609903). However right
- // now it is under heavy development so until the dust
- // settles we disable it.
- if release.OnClassic {
- return nil
- }
-
var seeded bool
err := m.state.Get("seeded", &seeded)
if err != nil && err != state.ErrNoState {
@@ -243,12 +249,6 @@ func (m *DeviceManager) ensureSeedYaml() error {
return nil
}
- coreInfo, err := snapstate.CoreInfo(m.state)
- if err == nil && coreInfo.Name() == "ubuntu-core" {
- // already seeded... recover
- return m.alreadyFirstbooted()
- }
-
tsAll, err := populateStateFromSeed(m.state)
if err != nil {
return err
@@ -267,52 +267,6 @@ func (m *DeviceManager) ensureSeedYaml() error {
return nil
}
-// alreadyFirstbooted recovers already first booted devices with the old method appropriately
-func (m *DeviceManager) alreadyFirstbooted() error {
- device, err := auth.Device(m.state)
- if err != nil {
- return err
- }
- // recover key-id
- if device.Brand != "" && device.Model != "" {
- serials, err := assertstate.DB(m.state).FindMany(asserts.SerialType, map[string]string{
- "brand-id": device.Brand,
- "model": device.Model,
- })
- if err != nil && err != asserts.ErrNotFound {
- return err
- }
-
- if len(serials) == 1 {
- // we can recover the key id from the assertion
- serial := serials[0].(*asserts.Serial)
- keyID := serial.DeviceKey().ID()
- device.KeyID = keyID
- device.Serial = serial.Serial()
- err := auth.SetDevice(m.state, device)
- if err != nil {
- return err
- }
- // best effort to cleanup abandoned keys
- pat := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "*")
- keyFns, err := filepath.Glob(pat)
- if err != nil {
- panic(fmt.Sprintf("invalid glob for device keys: %v", err))
- }
- for _, keyFn := range keyFns {
- if filepath.Base(keyFn) == keyID {
- continue
- }
- os.Remove(keyFn)
- }
- }
-
- }
-
- m.state.Set("seeded", true)
- return nil
-}
-
func (m *DeviceManager) ensureBootOk() error {
m.state.Lock()
defer m.state.Unlock()
@@ -841,28 +795,40 @@ func (m *DeviceManager) doMarkSeeded(t *state.Task, _ *tomb.Tomb) error {
// canAutoRefresh is a helper that checks if the device is able to
// auto-refresh
-func canAutoRefresh(st *state.State) bool {
- // no need to wait for seeding on classic
- if release.OnClassic {
- return true
- }
-
- // on all-snap devices we need to be seeded first
+func canAutoRefresh(st *state.State) (bool, error) {
+ // we need to be seeded first
var seeded bool
st.Get("seeded", &seeded)
if !seeded {
- return false
+ return false, nil
+ }
+
+ _, err := Model(st)
+ if err == state.ErrNoState {
+ // no model, no need to wait for a serial
+ // can happen only on classic
+ return true, nil
+ }
+ if err != nil {
+ return false, err
+ }
+
+ // either we have a serial or we try anyway if we attempted
+ // for a while to get a serial, this would allow us to at
+ // least upgrade core if that can help
+ if ensureOperationalAttempts(st) >= 3 {
+ return true, nil
}
- // FIXME: The serial requirement means that developer images
- // with custom models will not auto-refresh
- // and we also need to have a serial
- _, err := Serial(st)
+ _, err = Serial(st)
+ if err == state.ErrNoState {
+ return false, nil
+ }
if err != nil {
- return false
+ return false, err
}
- return true
+ return true, nil
}
var repeatRequestSerial string
@@ -979,6 +945,10 @@ func checkGadgetOrKernel(st *state.State, snapInfo, curInfo *snap.Info, flags sn
currentInfo = snapstate.GadgetInfo
getName = (*asserts.Model).Gadget
case snap.TypeKernel:
+ if release.OnClassic {
+ return fmt.Errorf("cannot install a kernel snap on classic")
+ }
+
kind = "kernel"
currentInfo = snapstate.KernelInfo
getName = (*asserts.Model).Kernel
@@ -987,11 +957,6 @@ func checkGadgetOrKernel(st *state.State, snapInfo, curInfo *snap.Info, flags sn
return nil
}
- if release.OnClassic {
- // for the time being
- return fmt.Errorf("cannot install a %s snap on classic", kind)
- }
-
model, err := Model(st)
if err == state.ErrNoState {
return fmt.Errorf("cannot install %s without model assertion", kind)
@@ -1024,6 +989,10 @@ func checkGadgetOrKernel(st *state.State, snapInfo, curInfo *snap.Info, flags sn
// first installation of a gadget/kernel
expectedName := getName(model)
+ if expectedName == "" { // can happen only on classic
+ return fmt.Errorf("cannot install %s snap on classic if not requested by the model", kind)
+ }
+
if snapInfo.Name() != expectedName {
return fmt.Errorf("cannot install %s %q, model assertion requests %q", kind, snapInfo.Name(), expectedName)
}
diff --git a/overlord/devicestate/devicemgr_test.go b/overlord/devicestate/devicemgr_test.go
index 01ad926af6..2f22165b3a 100644
--- a/overlord/devicestate/devicemgr_test.go
+++ b/overlord/devicestate/devicemgr_test.go
@@ -21,13 +21,11 @@ package devicestate_test
import (
"encoding/json"
- "errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
- "path/filepath"
"sync"
"testing"
"time"
@@ -69,6 +67,8 @@ type deviceMgrSuite struct {
brandSigning *assertstest.SigningDB
reqID string
+
+ restoreOnClassic func()
}
var _ = Suite(&deviceMgrSuite{})
@@ -130,6 +130,8 @@ func (sto *fakeStore) Sections(*auth.UserState) ([]string, error) {
func (s *deviceMgrSuite) SetUpTest(c *C) {
dirs.SetRootDir(c.MkDir())
+ s.restoreOnClassic = release.MockOnClassic(false)
+
rootPrivKey, _ := assertstest.GenerateKey(1024)
storePrivKey, _ := assertstest.GenerateKey(752)
s.storeSigning = assertstest.NewStoreStack("canonical", rootPrivKey, storePrivKey)
@@ -173,7 +175,7 @@ func (s *deviceMgrSuite) TearDownTest(c *C) {
assertstate.ReplaceDB(s.state, nil)
s.state.Unlock()
dirs.SetRootDir("")
- release.OnClassic = true
+ s.restoreOnClassic()
}
func (s *deviceMgrSuite) settle() {
@@ -185,6 +187,12 @@ func (s *deviceMgrSuite) settle() {
}
}
+// seeding avoids triggering a real full seeding, it simulates having it in process instead
+func (s *deviceMgrSuite) seeding() {
+ chg := s.state.NewChange("seed", "Seed system")
+ chg.SetStatus(state.DoingStatus)
+}
+
func (s *deviceMgrSuite) mockServer(c *C) *httptest.Server {
expectedUserAgent := httputil.UserAgent()
@@ -311,6 +319,9 @@ version: gadget
Model: "pc",
})
+ // avoid full seeding
+ s.seeding()
+
// runs the whole device registration process
s.state.Unlock()
s.settle()
@@ -388,6 +399,9 @@ version: gadget
chg := s.state.NewChange("become-operational", "...")
chg.AddTask(t)
+ // avoid full seeding
+ s.seeding()
+
s.state.Unlock()
s.mgr.Ensure()
s.mgr.Wait()
@@ -454,6 +468,9 @@ version: gadget
chg := s.state.NewChange("become-operational", "...")
chg.AddTask(t)
+ // avoid full seeding
+ s.seeding()
+
s.state.Unlock()
s.mgr.Ensure()
s.mgr.Wait()
@@ -516,6 +533,9 @@ version: gadget
Model: "pc",
})
+ // avoid full seeding
+ s.seeding()
+
// runs the whole device registration process with polling
s.state.Unlock()
s.settle()
@@ -602,12 +622,14 @@ version: gadget
hooks:
prepare-device:
`, "")
-
auth.SetDevice(s.state, &auth.DeviceState{
Brand: "canonical",
Model: "pc",
})
+ // avoid full seeding
+ s.seeding()
+
// runs the whole device registration process
s.state.Unlock()
s.settle()
@@ -674,6 +696,9 @@ func (s *deviceMgrSuite) TestFullDeviceRegistrationErrorBackoff(c *C) {
s.state.Lock()
defer s.state.Unlock()
+ // sanity
+ c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 0)
+
s.setupGadget(c, `
name: gadget
type: gadget
@@ -685,6 +710,9 @@ version: gadget
Model: "pc",
})
+ // avoid full seeding
+ s.seeding()
+
// try the whole device registration process
s.state.Unlock()
s.settle()
@@ -710,6 +738,7 @@ version: gadget
c.Check(s.mgr.EnsureOperationalShouldBackoff(time.Now()), Equals, true)
c.Check(s.mgr.EnsureOperationalShouldBackoff(time.Now().Add(6*time.Minute)), Equals, false)
+ c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 1)
// try again the whole device registration process
s.reqID = "REQID-1"
@@ -730,6 +759,8 @@ version: gadget
c.Check(becomeOperational.Status().Ready(), Equals, true)
c.Check(becomeOperational.Err(), IsNil)
+ c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 2)
+
device, err = auth.Device(s.state)
c.Assert(err, IsNil)
c.Check(device.KeyID, Equals, keyID)
@@ -891,8 +922,6 @@ func (s *deviceMgrSuite) TestDeviceAssertionsDeviceSessionRequest(c *C) {
}
func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlAlreadySeeded(c *C) {
- release.OnClassic = false
-
s.state.Lock()
s.state.Set("seeded", true)
s.state.Unlock()
@@ -910,8 +939,6 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlAlreadySeeded(c *C) {
}
func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlChangeInFlight(c *C) {
- release.OnClassic = false
-
s.state.Lock()
chg := s.state.NewChange("seed", "just for testing")
chg.AddTask(s.state.NewTask("test-task", "the change needs a task"))
@@ -929,7 +956,7 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlChangeInFlight(c *C) {
c.Assert(called, Equals, false)
}
-func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlSkippedOnClassic(c *C) {
+func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlAlsoOnClassic(c *C) {
release.OnClassic = true
called := false
@@ -941,12 +968,10 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlSkippedOnClassic(c *C) {
err := s.mgr.EnsureSeedYaml()
c.Assert(err, IsNil)
- c.Assert(called, Equals, false)
+ c.Assert(called, Equals, true)
}
func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlHappy(c *C) {
- release.OnClassic = false
-
restore := devicestate.MockPopulateStateFromSeed(func(*state.State) (ts []*state.TaskSet, err error) {
t := s.state.NewTask("test-task", "a random task")
ts = append(ts, state.NewTaskSet(t))
@@ -963,104 +988,6 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlHappy(c *C) {
c.Check(s.state.Changes(), HasLen, 1)
}
-func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlRecover(c *C) {
- release.OnClassic = false
-
- restore := devicestate.MockPopulateStateFromSeed(func(*state.State) (ts []*state.TaskSet, err error) {
- return nil, errors.New("should not be called")
- })
- defer restore()
-
- s.state.Lock()
- defer s.state.Unlock()
-
- s.setupCore(c, "ubuntu-core", `
-name: ubuntu-core
-type: os
-version: ubuntu-core
-`, "")
-
- // have a model assertion
- model, err := s.storeSigning.Sign(asserts.ModelType, map[string]interface{}{
- "series": "16",
- "brand-id": "canonical",
- "model": "pc",
- "gadget": "pc",
- "kernel": "kernel",
- "architecture": "amd64",
- "timestamp": time.Now().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
- err = assertstate.Add(s.state, model)
- c.Assert(err, IsNil)
-
- // have a serial assertion
- devKey, _ := assertstest.GenerateKey(752)
- encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey())
- keyID := devKey.PublicKey().ID()
- c.Assert(err, IsNil)
- serial, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{
- "brand-id": "canonical",
- "model": "pc",
- "serial": "8989",
- "device-key": string(encDevKey),
- "device-key-sha3-384": keyID,
- "timestamp": time.Now().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
- err = assertstate.Add(s.state, serial)
- c.Assert(err, IsNil)
-
- // forgotten key id and serial
- auth.SetDevice(s.state, &auth.DeviceState{
- Brand: "canonical",
- Model: "pc",
- })
- // put key on disk
- err = s.mgr.KeypairManager().Put(devKey)
- c.Assert(err, IsNil)
- // extra unused stuff
- junk1 := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "junkjunk1")
- err = ioutil.WriteFile(junk1, nil, 0644)
- c.Assert(err, IsNil)
- junk2 := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "junkjunk2")
- err = ioutil.WriteFile(junk2, nil, 0644)
- c.Assert(err, IsNil)
- // double check
- pat := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "*")
- onDisk, err := filepath.Glob(pat)
- c.Assert(err, IsNil)
- c.Check(onDisk, HasLen, 3)
-
- s.state.Unlock()
- err = s.mgr.EnsureSeedYaml()
- s.state.Lock()
- c.Assert(err, IsNil)
-
- c.Check(s.state.Changes(), HasLen, 0)
-
- var seeded bool
- err = s.state.Get("seeded", &seeded)
- c.Assert(err, IsNil)
- c.Check(seeded, Equals, true)
-
- device, err := auth.Device(s.state)
- c.Assert(err, IsNil)
- c.Check(device, DeepEquals, &auth.DeviceState{
- Brand: "canonical",
- Model: "pc",
- KeyID: keyID,
- Serial: "8989",
- })
- // key is still there
- _, err = s.mgr.KeypairManager().Get(keyID)
- c.Assert(err, IsNil)
- onDisk, err = filepath.Glob(pat)
- c.Assert(err, IsNil)
- // junk was removed
- c.Check(onDisk, HasLen, 1)
-}
-
func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkSkippedOnClassic(c *C) {
release.OnClassic = true
@@ -1069,8 +996,6 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkSkippedOnClassic(c *C) {
}
func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkBootloaderHappy(c *C) {
- release.OnClassic = false
-
bootloader := boottest.NewMockBootloader("mock", c.MkDir())
partition.ForceBootloader(bootloader)
defer partition.ForceBootloader(nil)
@@ -1100,8 +1025,6 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkBootloaderHappy(c *C) {
}
func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkUpdateBootRevisionsHappy(c *C) {
- release.OnClassic = false
-
bootloader := boottest.NewMockBootloader("mock", c.MkDir())
partition.ForceBootloader(bootloader)
defer partition.ForceBootloader(nil)
@@ -1134,8 +1057,6 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkUpdateBootRevisionsHappy(c
}
func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkNotRunAgain(c *C) {
- release.OnClassic = false
-
bootloader := boottest.NewMockBootloader("mock", c.MkDir())
bootloader.SetBootVars(map[string]string{
"snap_mode": "trying",
@@ -1152,8 +1073,6 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkNotRunAgain(c *C) {
}
func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkError(c *C) {
- release.OnClassic = false
-
s.state.Lock()
// seeded
s.state.Set("seeded", true)
@@ -1176,22 +1095,11 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkError(c *C) {
c.Assert(err, ErrorMatches, "devicemgr: bootloader err")
}
-func (s *deviceMgrSuite) TestCheckGadget(c *C) {
- release.OnClassic = false
- s.state.Lock()
- defer s.state.Unlock()
- // nothing is setup
- gadgetInfo := snaptest.MockInfo(c, `type: gadget
-name: other-gadget`, nil)
-
- err := devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{})
- c.Check(err, ErrorMatches, `cannot install gadget without model assertion`)
-
- // setup model assertion
+func (s *deviceMgrSuite) setupBrands(c *C) {
brandAcct := assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{
"account-id": "my-brand",
}, "")
- err = assertstate.Add(s.state, brandAcct)
+ err := assertstate.Add(s.state, brandAcct)
c.Assert(err, IsNil)
otherAcct := assertstest.NewAccount(s.storeSigning, "other-brand", map[string]interface{}{
"account-id": "other-brand",
@@ -1204,6 +1112,33 @@ name: other-gadget`, nil)
brandAccKey := assertstest.NewAccountKey(s.storeSigning, brandAcct, nil, brandPubKey, "")
err = assertstate.Add(s.state, brandAccKey)
c.Assert(err, IsNil)
+}
+
+func (s *deviceMgrSuite) setupSnapDecl(c *C, name, snapID, publisherID string) {
+ brandGadgetDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-name": name,
+ "snap-id": snapID,
+ "publisher-id": publisherID,
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, brandGadgetDecl)
+ c.Assert(err, IsNil)
+}
+
+func (s *deviceMgrSuite) TestCheckGadget(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ // nothing is setup
+ gadgetInfo := snaptest.MockInfo(c, `type: gadget
+name: other-gadget`, nil)
+
+ err := devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, ErrorMatches, `cannot install gadget without model assertion`)
+
+ // setup model assertion
+ s.setupBrands(c)
model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{
"series": "16",
@@ -1227,16 +1162,7 @@ name: other-gadget`, nil)
c.Check(err, ErrorMatches, `cannot install gadget "other-gadget", model assertion requests "gadget"`)
// brand gadget
- brandGadgetDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
- "series": "16",
- "snap-name": "gadget",
- "snap-id": "brand-gadget-id",
- "publisher-id": "my-brand",
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
- err = assertstate.Add(s.state, brandGadgetDecl)
- c.Assert(err, IsNil)
+ s.setupSnapDecl(c, "gadget", "brand-gadget-id", "my-brand")
brandGadgetInfo := snaptest.MockInfo(c, `
type: gadget
name: gadget
@@ -1244,16 +1170,7 @@ name: gadget
brandGadgetInfo.SnapID = "brand-gadget-id"
// canonical gadget
- canonicalGadgetDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
- "series": "16",
- "snap-name": "gadget",
- "snap-id": "canonical-gadget-id",
- "publisher-id": "canonical",
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
- err = assertstate.Add(s.state, canonicalGadgetDecl)
- c.Assert(err, IsNil)
+ s.setupSnapDecl(c, "gadget", "canonical-gadget-id", "canonical")
canonicalGadgetInfo := snaptest.MockInfo(c, `
type: gadget
name: gadget
@@ -1261,16 +1178,81 @@ name: gadget
canonicalGadgetInfo.SnapID = "canonical-gadget-id"
// other gadget
- otherGadgetDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
- "series": "16",
- "snap-name": "gadget",
- "snap-id": "other-gadget-id",
- "publisher-id": "other-brand",
- "timestamp": time.Now().UTC().Format(time.RFC3339),
+ s.setupSnapDecl(c, "gadget", "other-gadget-id", "other-brand")
+ otherGadgetInfo := snaptest.MockInfo(c, `
+type: gadget
+name: gadget
+`, nil)
+ otherGadgetInfo.SnapID = "other-gadget-id"
+
+ // install brand gadget ok
+ err = devicestate.CheckGadgetOrKernel(s.state, brandGadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, IsNil)
+
+ // install canonical gadget ok
+ err = devicestate.CheckGadgetOrKernel(s.state, canonicalGadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, IsNil)
+
+ // install other gadget fails
+ err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, ErrorMatches, `cannot install gadget "gadget" published by "other-brand" for model by "my-brand"`)
+
+ // unasserted installation of other works
+ otherGadgetInfo.SnapID = ""
+ err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, IsNil)
+}
+
+func (s *deviceMgrSuite) TestCheckGadgetOnClassic(c *C) {
+ release.OnClassic = true
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ gadgetInfo := snaptest.MockInfo(c, `type: gadget
+name: other-gadget`, nil)
+
+ // setup model assertion
+ s.setupBrands(c)
+
+ model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "classic": "true",
+ "gadget": "gadget",
+ "timestamp": time.Now().Format(time.RFC3339),
}, nil, "")
c.Assert(err, IsNil)
- err = assertstate.Add(s.state, otherGadgetDecl)
+ err = assertstate.Add(s.state, model)
+ c.Assert(err, IsNil)
+ err = auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-model",
+ })
c.Assert(err, IsNil)
+
+ err = devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, ErrorMatches, `cannot install gadget "other-gadget", model assertion requests "gadget"`)
+
+ // brand gadget
+ s.setupSnapDecl(c, "gadget", "brand-gadget-id", "my-brand")
+ brandGadgetInfo := snaptest.MockInfo(c, `
+type: gadget
+name: gadget
+`, nil)
+ brandGadgetInfo.SnapID = "brand-gadget-id"
+
+ // canonical gadget
+ s.setupSnapDecl(c, "gadget", "canonical-gadget-id", "canonical")
+ canonicalGadgetInfo := snaptest.MockInfo(c, `
+type: gadget
+name: gadget
+`, nil)
+ canonicalGadgetInfo.SnapID = "canonical-gadget-id"
+
+ // other gadget
+ s.setupSnapDecl(c, "gadget", "other-gadget-id", "other-brand")
otherGadgetInfo := snaptest.MockInfo(c, `
type: gadget
name: gadget
@@ -1295,34 +1277,56 @@ name: gadget
c.Check(err, IsNil)
}
+func (s *deviceMgrSuite) TestCheckGadgetOnClassicGadgetNotSpecified(c *C) {
+ release.OnClassic = true
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ gadgetInfo := snaptest.MockInfo(c, `type: gadget
+name: gadget`, nil)
+
+ // setup model assertion
+ s.setupBrands(c)
+
+ model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{
+ "series": "16",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "classic": "true",
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, model)
+ c.Assert(err, IsNil)
+ err = auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-model",
+ })
+ c.Assert(err, IsNil)
+
+ err = devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{})
+ c.Check(err, ErrorMatches, `cannot install gadget snap on classic if not requested by the model`)
+}
+
func (s *deviceMgrSuite) TestCheckKernel(c *C) {
- release.OnClassic = false
s.state.Lock()
defer s.state.Unlock()
- // nothing is setup
kernelInfo := snaptest.MockInfo(c, `type: kernel
name: lnrk`, nil)
+ // not on classic
+ release.OnClassic = true
err := devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, snapstate.Flags{})
+ c.Check(err, ErrorMatches, `cannot install a kernel snap on classic`)
+ release.OnClassic = false
+
+ // nothing is setup
+ err = devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, snapstate.Flags{})
c.Check(err, ErrorMatches, `cannot install kernel without model assertion`)
// setup model assertion
- brandAcct := assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{
- "account-id": "my-brand",
- }, "")
- err = assertstate.Add(s.state, brandAcct)
- c.Assert(err, IsNil)
- otherAcct := assertstest.NewAccount(s.storeSigning, "other-brand", map[string]interface{}{
- "account-id": "other-brand",
- }, "")
- err = assertstate.Add(s.state, otherAcct)
- c.Assert(err, IsNil)
-
- brandPubKey, err := s.brandSigning.PublicKey("")
- c.Assert(err, IsNil)
- brandAccKey := assertstest.NewAccountKey(s.storeSigning, brandAcct, nil, brandPubKey, "")
- err = assertstate.Add(s.state, brandAccKey)
- c.Assert(err, IsNil)
+ s.setupBrands(c)
model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{
"series": "16",
@@ -1346,16 +1350,7 @@ name: lnrk`, nil)
c.Check(err, ErrorMatches, `cannot install kernel "lnrk", model assertion requests "krnl"`)
// brand kernel
- brandKrnlDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
- "series": "16",
- "snap-name": "krnl",
- "snap-id": "brand-krnl-id",
- "publisher-id": "my-brand",
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
- err = assertstate.Add(s.state, brandKrnlDecl)
- c.Assert(err, IsNil)
+ s.setupSnapDecl(c, "krnl", "brand-krnl-id", "my-brand")
brandKrnlInfo := snaptest.MockInfo(c, `
type: kernel
name: krnl
@@ -1363,16 +1358,7 @@ name: krnl
brandKrnlInfo.SnapID = "brand-krnl-id"
// canonical kernel
- canonicalKrnlDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
- "series": "16",
- "snap-name": "krnl",
- "snap-id": "canonical-krnl-id",
- "publisher-id": "canonical",
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
- err = assertstate.Add(s.state, canonicalKrnlDecl)
- c.Assert(err, IsNil)
+ s.setupSnapDecl(c, "krnl", "canonical-krnl-id", "canonical")
canonicalKrnlInfo := snaptest.MockInfo(c, `
type: kernel
name: krnl
@@ -1380,16 +1366,7 @@ name: krnl
canonicalKrnlInfo.SnapID = "canonical-krnl-id"
// other kernel
- otherKrnlDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
- "series": "16",
- "snap-name": "krnl",
- "snap-id": "other-krnl-id",
- "publisher-id": "other-brand",
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
- err = assertstate.Add(s.state, otherKrnlDecl)
- c.Assert(err, IsNil)
+ s.setupSnapDecl(c, "krnl", "other-krnl-id", "other-brand")
otherKrnlInfo := snaptest.MockInfo(c, `
type: kernel
name: krnl
@@ -1414,12 +1391,20 @@ name: krnl
c.Check(err, IsNil)
}
-func (s *deviceMgrSuite) TestCanAutoRefreshOnClassicAlways(c *C) {
- s.state.Lock()
- defer s.state.Unlock()
-
- release.OnClassic = true
- c.Check(devicestate.CanAutoRefresh(s.state), Equals, true)
+func (s *deviceMgrSuite) makeModelAssertionInState(c *C, brandID, model string, extras map[string]string) {
+ headers := map[string]interface{}{
+ "series": "16",
+ "brand-id": brandID,
+ "model": model,
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ for k, v := range extras {
+ headers[k] = v
+ }
+ modelAs, err := s.storeSigning.Sign(asserts.ModelType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, modelAs)
+ c.Assert(err, IsNil)
}
func (s *deviceMgrSuite) makeSerialAssertionInState(c *C, brandID, model, serialN string) {
@@ -1443,26 +1428,117 @@ func (s *deviceMgrSuite) TestCanAutoRefreshOnCore(c *C) {
s.state.Lock()
defer s.state.Unlock()
- release.OnClassic = false
+ canAutoRefresh := func() bool {
+ ok, err := devicestate.CanAutoRefresh(s.state)
+ c.Assert(err, IsNil)
+ return ok
+ }
- // not seeded, no serial -> no auto-refresh
+ // not seeded, no model, no serial -> no auto-refresh
s.state.Set("seeded", false)
- c.Check(devicestate.CanAutoRefresh(s.state), Equals, false)
+ c.Check(canAutoRefresh(), Equals, false)
- // seeded, no serial -> no auto-refresh
+ // seeded, model, no serial -> no auto-refresh
s.state.Set("seeded", true)
- c.Check(devicestate.CanAutoRefresh(s.state), Equals, false)
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+ s.makeModelAssertionInState(c, "canonical", "pc", map[string]string{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ })
+ c.Check(canAutoRefresh(), Equals, false)
+
+ // seeded, model, serial -> auto-refresh
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ Serial: "8989",
+ })
+ s.makeSerialAssertionInState(c, "canonical", "pc", "8989")
+ c.Check(canAutoRefresh(), Equals, true)
+
+ // not seeded, model, serial -> no auto-refresh
+ s.state.Set("seeded", false)
+ c.Check(canAutoRefresh(), Equals, false)
+}
+
+func (s *deviceMgrSuite) TestCanAutoRefreshNoSerialFallback(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ canAutoRefresh := func() bool {
+ ok, err := devicestate.CanAutoRefresh(s.state)
+ c.Assert(err, IsNil)
+ return ok
+ }
+
+ // seeded, model, no serial, two attempts at getting serial
+ // -> no auto-refresh
+ devicestate.IncEnsureOperationalAttempts(s.state)
+ devicestate.IncEnsureOperationalAttempts(s.state)
+ s.state.Set("seeded", true)
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+ s.makeModelAssertionInState(c, "canonical", "pc", map[string]string{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ })
+ c.Check(canAutoRefresh(), Equals, false)
+
+ // third attempt ongoing, or done
+ // fallback, try auto-refresh
+ devicestate.IncEnsureOperationalAttempts(s.state)
+ // sanity
+ c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 3)
+ c.Check(canAutoRefresh(), Equals, true)
+}
+
+func (s *deviceMgrSuite) TestCanAutoRefreshOnClassic(c *C) {
+ release.OnClassic = true
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ canAutoRefresh := func() bool {
+ ok, err := devicestate.CanAutoRefresh(s.state)
+ c.Assert(err, IsNil)
+ return ok
+ }
+
+ // not seeded, no model, no serial -> no auto-refresh
+ s.state.Set("seeded", false)
+ c.Check(canAutoRefresh(), Equals, false)
+
+ // seeded, no model -> auto-refresh
+ s.state.Set("seeded", true)
+ c.Check(canAutoRefresh(), Equals, true)
+
+ // seeded, model, no serial -> no auto-refresh
+ auth.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc",
+ })
+ s.makeModelAssertionInState(c, "canonical", "pc", map[string]string{
+ "classic": "true",
+ })
+ c.Check(canAutoRefresh(), Equals, false)
- // seeded, serial -> auto-refresh
+ // seeded, model, serial -> auto-refresh
auth.SetDevice(s.state, &auth.DeviceState{
Brand: "canonical",
Model: "pc",
Serial: "8989",
})
s.makeSerialAssertionInState(c, "canonical", "pc", "8989")
- c.Check(devicestate.CanAutoRefresh(s.state), Equals, true)
+ c.Check(canAutoRefresh(), Equals, true)
- // not seeded, serial -> no auto-refresh
+ // not seeded, model, serial -> no auto-refresh
s.state.Set("seeded", false)
- c.Check(devicestate.CanAutoRefresh(s.state), Equals, false)
+ c.Check(canAutoRefresh(), Equals, false)
}
diff --git a/overlord/devicestate/export_test.go b/overlord/devicestate/export_test.go
index b2e618e023..9e721ee006 100644
--- a/overlord/devicestate/export_test.go
+++ b/overlord/devicestate/export_test.go
@@ -108,4 +108,7 @@ var (
ImportAssertionsFromSeed = importAssertionsFromSeed
CheckGadgetOrKernel = checkGadgetOrKernel
CanAutoRefresh = canAutoRefresh
+
+ IncEnsureOperationalAttempts = incEnsureOperationalAttempts
+ EnsureOperationalAttempts = ensureOperationalAttempts
)
diff --git a/overlord/devicestate/firstboot.go b/overlord/devicestate/firstboot.go
index b333a0e9c2..6ebefc4845 100644
--- a/overlord/devicestate/firstboot.go
+++ b/overlord/devicestate/firstboot.go
@@ -20,6 +20,7 @@
package devicestate
import (
+ "errors"
"fmt"
"io/ioutil"
"os"
@@ -29,13 +30,17 @@ import (
"github.com/snapcore/snapd/asserts/snapasserts"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n/dumb"
+ "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/release"
"github.com/snapcore/snapd/snap"
)
+var errNothingToDo = errors.New("nothing to do")
+
func populateStateFromSeedImpl(st *state.State) ([]*state.TaskSet, error) {
// check that the state is empty
var seeded bool
@@ -47,13 +52,24 @@ func populateStateFromSeedImpl(st *state.State) ([]*state.TaskSet, error) {
return nil, fmt.Errorf("cannot populate state: already seeded")
}
+ markSeeded := st.NewTask("mark-seeded", i18n.G("Mark system seeded"))
+
// ack all initial assertions
model, err := importAssertionsFromSeed(st)
+ if err == errNothingToDo {
+ return []*state.TaskSet{state.NewTaskSet(markSeeded)}, nil
+ }
if err != nil {
return nil, err
}
- seed, err := snap.ReadSeedYaml(filepath.Join(dirs.SnapSeedDir, "seed.yaml"))
+ seedYamlFile := filepath.Join(dirs.SnapSeedDir, "seed.yaml")
+ if release.OnClassic && !osutil.FileExists(seedYamlFile) {
+ // on classic it is ok to not seed any snaps
+ return []*state.TaskSet{state.NewTaskSet(markSeeded)}, nil
+ }
+
+ seed, err := snap.ReadSeedYaml(seedYamlFile)
if err != nil {
return nil, err
}
@@ -107,11 +123,10 @@ func populateStateFromSeedImpl(st *state.State) ([]*state.TaskSet, error) {
tsAll = append(tsAll, ts)
}
if len(tsAll) == 0 {
- return nil, nil
+ return nil, fmt.Errorf("cannot proceed, no snaps to seed")
}
ts := tsAll[len(tsAll)-1]
- markSeeded := st.NewTask("mark-seeded", i18n.G("Mark system seeded"))
markSeeded.WaitAll(ts)
tsAll = append(tsAll, state.NewTaskSet(markSeeded))
@@ -136,6 +151,10 @@ func importAssertionsFromSeed(st *state.State) (*asserts.Model, error) {
// set device,model from the model assertion
assertSeedDir := filepath.Join(dirs.SnapSeedDir, "assertions")
dc, err := ioutil.ReadDir(assertSeedDir)
+ if release.OnClassic && os.IsNotExist(err) {
+ // on classic seeding is optional
+ return nil, errNothingToDo
+ }
if err != nil {
return nil, fmt.Errorf("cannot read assert seed dir: %s", err)
}
@@ -173,6 +192,17 @@ func importAssertionsFromSeed(st *state.State) (*asserts.Model, error) {
}
modelAssertion := a.(*asserts.Model)
+ classicModel := modelAssertion.Classic()
+ if release.OnClassic != classicModel {
+ var msg string
+ if classicModel {
+ msg = "cannot seed an all-snaps system with a classic model"
+ } else {
+ msg = "cannot seed a classic system with an all-snaps model"
+ }
+ return nil, fmt.Errorf(msg)
+ }
+
// set device,model from the model assertion
device.Brand = modelAssertion.BrandID()
device.Model = modelAssertion.Model()
diff --git a/overlord/devicestate/firstboot_test.go b/overlord/devicestate/firstboot_test.go
index f5d0a9e957..abacace542 100644
--- a/overlord/devicestate/firstboot_test.go
+++ b/overlord/devicestate/firstboot_test.go
@@ -25,6 +25,7 @@ import (
"os"
"path/filepath"
"strconv"
+ "strings"
"time"
. "gopkg.in/check.v1"
@@ -40,6 +41,7 @@ import (
"github.com/snapcore/snapd/overlord/devicestate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/testutil"
@@ -56,6 +58,8 @@ type FirstBootTestSuite struct {
brandSigning *assertstest.SigningDB
overlord *overlord.Overlord
+
+ restoreOnClassic func()
}
var _ = Suite(&FirstBootTestSuite{})
@@ -63,6 +67,7 @@ var _ = Suite(&FirstBootTestSuite{})
func (s *FirstBootTestSuite) SetUpTest(c *C) {
tempdir := c.MkDir()
dirs.SetRootDir(tempdir)
+ s.restoreOnClassic = release.MockOnClassic(false)
// mock the world!
err := os.MkdirAll(filepath.Join(dirs.SnapSeedDir, "snaps"), 0755)
@@ -93,12 +98,67 @@ func (s *FirstBootTestSuite) SetUpTest(c *C) {
}
func (s *FirstBootTestSuite) TearDownTest(c *C) {
- dirs.SetRootDir("/")
os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS")
s.systemctl.Restore()
s.mockUdevAdm.Restore()
s.restore()
+ s.restoreOnClassic()
+ dirs.SetRootDir("/")
+}
+
+func (s *FirstBootTestSuite) TestPopulateFromSeedOnClassicNoop(c *C) {
+ release.OnClassic = true
+
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ err := os.Remove(filepath.Join(dirs.SnapSeedDir, "assertions"))
+ c.Assert(err, IsNil)
+
+ tsAll, err := devicestate.PopulateStateFromSeedImpl(st)
+ c.Assert(err, IsNil)
+ // only mark seeded
+ c.Check(tsAll, HasLen, 1)
+ tasks := tsAll[0].Tasks()
+ c.Check(tasks, HasLen, 1)
+ c.Check(tasks[0].Kind(), Equals, "mark-seeded")
+}
+
+func (s *FirstBootTestSuite) TestPopulateFromSeedOnClassicNoSeedYaml(c *C) {
+ release.OnClassic = true
+
+ ovld, err := overlord.New()
+ c.Assert(err, IsNil)
+ st := ovld.State()
+
+ // add a bunch of assert files
+ assertsChain := s.makeModelAssertionChain(c, "my-model-classic")
+ for i, as := range assertsChain {
+ fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i))
+ err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
+ c.Assert(err, IsNil)
+ }
+
+ err = os.Remove(filepath.Join(dirs.SnapSeedDir, "seed.yaml"))
+ c.Assert(err, IsNil)
+
+ st.Lock()
+ defer st.Unlock()
+
+ tsAll, err := devicestate.PopulateStateFromSeedImpl(st)
+ c.Assert(err, IsNil)
+ // only mark seeded
+ c.Check(tsAll, HasLen, 1)
+ tasks := tsAll[0].Tasks()
+ c.Check(tasks, HasLen, 1)
+ c.Check(tasks[0].Kind(), Equals, "mark-seeded")
+
+ ds, err := auth.Device(st)
+ c.Assert(err, IsNil)
+ c.Check(ds.Brand, Equals, "my-brand")
+ c.Check(ds.Model, Equals, "my-model-classic")
}
func (s *FirstBootTestSuite) TestPopulateFromSeedErrorsOnState(c *C) {
@@ -164,7 +224,7 @@ version: 1.0`
c.Assert(err, IsNil)
// add a model assertion and its chain
- assertsChain := s.makeModelAssertionChain(c, "foo")
+ assertsChain := s.makeModelAssertionChain(c, "my-model", "foo")
for i, as := range assertsChain {
fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i))
err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
@@ -201,7 +261,8 @@ snaps:
c.Check(markSeededTask.WaitTasks(), testutil.Contains, otherTask)
// now run the change and check the result
- chg := st.NewChange("run-it", "run the populate from seed changes")
+ // use the expected kind otherwise settle with start another one
+ chg := st.NewChange("seed", "run the populate from seed changes")
for _, ts := range tsAll {
chg.AddAll(ts)
}
@@ -345,7 +406,7 @@ version: 1.0`
writeAssertionsToFile("bar.asserts", []asserts.Assertion{devAcct, snapDeclBar, snapRevBar})
// add a model assertion and its chain
- assertsChain := s.makeModelAssertionChain(c)
+ assertsChain := s.makeModelAssertionChain(c, "my-model")
writeAssertionsToFile("model.asserts", assertsChain)
// create a seed.yaml
@@ -366,7 +427,8 @@ snaps:
tsAll, err := devicestate.PopulateStateFromSeedImpl(st)
c.Assert(err, IsNil)
- chg := st.NewChange("run-it", "run the populate from seed changes")
+ // use the expected kind otherwise settle with start another one
+ chg := st.NewChange("seed", "run the populate from seed changes")
for _, ts := range tsAll {
chg.AddAll(ts)
}
@@ -419,9 +481,13 @@ func (s *FirstBootTestSuite) makeModelAssertion(c *C, modelStr string, reqSnaps
"architecture": "amd64",
"store": "canonical",
"gadget": "pc",
- "kernel": "pc-kernel",
"timestamp": time.Now().Format(time.RFC3339),
}
+ if strings.HasSuffix(modelStr, "-classic") {
+ headers["classic"] = "true"
+ } else {
+ headers["kernel"] = "pc-kernel"
+ }
if len(reqSnaps) != 0 {
reqs := make([]interface{}, len(reqSnaps))
for i, req := range reqSnaps {
@@ -434,7 +500,7 @@ func (s *FirstBootTestSuite) makeModelAssertion(c *C, modelStr string, reqSnaps
return model.(*asserts.Model)
}
-func (s *FirstBootTestSuite) makeModelAssertionChain(c *C, reqSnaps ...string) []asserts.Assertion {
+func (s *FirstBootTestSuite) makeModelAssertionChain(c *C, modName string, reqSnaps ...string) []asserts.Assertion {
assertChain := []asserts.Assertion{}
brandAcct := assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{
@@ -446,7 +512,7 @@ func (s *FirstBootTestSuite) makeModelAssertionChain(c *C, reqSnaps ...string) [
brandAccKey := assertstest.NewAccountKey(s.storeSigning, brandAcct, nil, s.brandPrivKey.PublicKey(), "")
assertChain = append(assertChain, brandAccKey)
- model := s.makeModelAssertion(c, "my-model", reqSnaps...)
+ model := s.makeModelAssertion(c, modName, reqSnaps...)
assertChain = append(assertChain, model)
storeAccountKey := s.storeSigning.StoreAccountKey("")
@@ -454,13 +520,57 @@ func (s *FirstBootTestSuite) makeModelAssertionChain(c *C, reqSnaps ...string) [
return assertChain
}
+func (s *FirstBootTestSuite) TestImportAssertionsFromSeedClassicModelMismatch(c *C) {
+ release.OnClassic = true
+
+ ovld, err := overlord.New()
+ c.Assert(err, IsNil)
+ st := ovld.State()
+
+ // add a bunch of assert files
+ assertsChain := s.makeModelAssertionChain(c, "my-model")
+ for i, as := range assertsChain {
+ fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i))
+ err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
+ c.Assert(err, IsNil)
+ }
+
+ // import them
+ st.Lock()
+ defer st.Unlock()
+
+ _, err = devicestate.ImportAssertionsFromSeed(st)
+ c.Assert(err, ErrorMatches, "cannot seed a classic system with an all-snaps model")
+}
+
+func (s *FirstBootTestSuite) TestImportAssertionsFromSeedAllSnapsModelMismatch(c *C) {
+ ovld, err := overlord.New()
+ c.Assert(err, IsNil)
+ st := ovld.State()
+
+ // add a bunch of assert files
+ assertsChain := s.makeModelAssertionChain(c, "my-model-classic")
+ for i, as := range assertsChain {
+ fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i))
+ err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
+ c.Assert(err, IsNil)
+ }
+
+ // import them
+ st.Lock()
+ defer st.Unlock()
+
+ _, err = devicestate.ImportAssertionsFromSeed(st)
+ c.Assert(err, ErrorMatches, "cannot seed an all-snaps system with a classic model")
+}
+
func (s *FirstBootTestSuite) TestImportAssertionsFromSeedHappy(c *C) {
ovld, err := overlord.New()
c.Assert(err, IsNil)
st := ovld.State()
// add a bunch of assert files
- assertsChain := s.makeModelAssertionChain(c)
+ assertsChain := s.makeModelAssertionChain(c, "my-model")
for i, as := range assertsChain {
fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i))
err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
@@ -501,7 +611,7 @@ func (s *FirstBootTestSuite) TestImportAssertionsFromSeedMissingSig(c *C) {
defer st.Unlock()
// write out only the model assertion
- assertsChain := s.makeModelAssertionChain(c)
+ assertsChain := s.makeModelAssertionChain(c, "my-model")
for _, as := range assertsChain {
if as.Type() == asserts.ModelType {
fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model")
@@ -544,7 +654,7 @@ func (s *FirstBootTestSuite) TestImportAssertionsFromSeedNoModelAsserts(c *C) {
st.Lock()
defer st.Unlock()
- assertsChain := s.makeModelAssertionChain(c)
+ assertsChain := s.makeModelAssertionChain(c, "my-model")
for _, as := range assertsChain {
if as.Type() != asserts.ModelType {
fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model")
diff --git a/overlord/managers_test.go b/overlord/managers_test.go
index cbf85438b6..8645357a20 100644
--- a/overlord/managers_test.go
+++ b/overlord/managers_test.go
@@ -104,6 +104,7 @@ func (ms *mgrsSuite) SetUpTest(c *C) {
c.Assert(err, IsNil)
os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
+ snapstate.CanAutoRefresh = nil
// create a fake systemd environment
os.MkdirAll(filepath.Join(dirs.SnapServicesDir, "multi-user.target.wants"), 0755)
diff --git a/overlord/overlord_test.go b/overlord/overlord_test.go
index a2d2fd62dc..a010c02ea1 100644
--- a/overlord/overlord_test.go
+++ b/overlord/overlord_test.go
@@ -51,6 +51,7 @@ func (ovs *overlordSuite) SetUpTest(c *C) {
tmpdir := c.MkDir()
dirs.SetRootDir(tmpdir)
dirs.SnapStateFile = filepath.Join(tmpdir, "test.json")
+ snapstate.CanAutoRefresh = nil
}
func (ovs *overlordSuite) TearDownTest(c *C) {
@@ -173,10 +174,19 @@ func (wm *witnessManager) Stop() {
func (wm *witnessManager) Wait() {
}
+// markSeeded flags the state under the overlord as seeded to avoid running the seeding code in these tests
+func markSeeded(o *overlord.Overlord) {
+ st := o.State()
+ st.Lock()
+ st.Set("seeded", true)
+ st.Unlock()
+}
+
func (ovs *overlordSuite) TestTrivialRunAndStop(c *C) {
o, err := overlord.New()
c.Assert(err, IsNil)
+ markSeeded(o)
o.Loop()
err = o.Stop()
@@ -196,6 +206,7 @@ func (ovs *overlordSuite) TestEnsureLoopRunAndStop(c *C) {
}
o.Engine().AddManager(witness)
+ markSeeded(o)
o.Loop()
defer o.Stop()
@@ -231,6 +242,7 @@ func (ovs *overlordSuite) TestEnsureLoopMediatedEnsureBeforeImmediate(c *C) {
se := o.Engine()
se.AddManager(witness)
+ markSeeded(o)
o.Loop()
defer o.Stop()
@@ -261,6 +273,7 @@ func (ovs *overlordSuite) TestEnsureLoopMediatedEnsureBefore(c *C) {
se := o.Engine()
se.AddManager(witness)
+ markSeeded(o)
o.Loop()
defer o.Stop()
@@ -293,6 +306,7 @@ func (ovs *overlordSuite) TestEnsureBeforeSleepy(c *C) {
se := o.Engine()
se.AddManager(witness)
+ markSeeded(o)
o.Loop()
defer o.Stop()
@@ -324,6 +338,7 @@ func (ovs *overlordSuite) TestEnsureLoopMediatedEnsureBeforeOutsideEnsure(c *C)
se := o.Engine()
se.AddManager(witness)
+ markSeeded(o)
o.Loop()
defer o.Stop()
@@ -357,6 +372,7 @@ func (ovs *overlordSuite) TestEnsureLoopPrune(c *C) {
chg2.SetStatus(state.DoneStatus)
st.Unlock()
+ markSeeded(o)
o.Loop()
time.Sleep(150 * time.Millisecond)
err = o.Stop()
@@ -391,6 +407,7 @@ func (ovs *overlordSuite) TestEnsureLoopPruneRunsMultipleTimes(c *C) {
c.Check(st.Changes(), HasLen, 2)
st.Unlock()
+ markSeeded(o)
// start the loop that runs the prune ticker
o.Loop()
@@ -508,6 +525,7 @@ func (ovs *overlordSuite) TestTrivialSettle(c *C) {
s.Unlock()
+ markSeeded(o)
o.Settle()
s.Lock()
@@ -542,6 +560,7 @@ func (ovs *overlordSuite) TestSettleChain(c *C) {
s.Unlock()
+ markSeeded(o)
o.Settle()
s.Lock()
@@ -584,6 +603,7 @@ func (ovs *overlordSuite) TestSettleExplicitEnsureBefore(c *C) {
chg.AddTask(t)
s.Unlock()
+ markSeeded(o)
o.Settle()
s.Lock()
diff --git a/overlord/snapstate/check_snap.go b/overlord/snapstate/check_snap.go
index 79e0e97626..f3b66f249f 100644
--- a/overlord/snapstate/check_snap.go
+++ b/overlord/snapstate/check_snap.go
@@ -272,14 +272,9 @@ func checkGadgetOrKernel(st *state.State, snapInfo, curInfo *snap.Info, flags Fl
return nil
}
- if release.OnClassic {
- // for the time being
- return fmt.Errorf("cannot install a %s snap on classic", kind)
- }
-
currentSnap, err := currentInfo(st)
// in firstboot we have no gadget/kernel yet - that is ok
- // devicestate considers that case
+ // first install rules are in devicestate!
if err == state.ErrNoState {
return nil
}
diff --git a/overlord/snapstate/check_snap_test.go b/overlord/snapstate/check_snap_test.go
index b7bd5679ad..92fcb5702c 100644
--- a/overlord/snapstate/check_snap_test.go
+++ b/overlord/snapstate/check_snap_test.go
@@ -460,34 +460,6 @@ version: 1
c.Check(err, IsNil)
}
-func (s *checkSnapSuite) TestCheckSnapGadgetCannotBeInstalledOnClassic(c *C) {
- reset := release.MockOnClassic(true)
- defer reset()
-
- st := state.New(nil)
- st.Lock()
- defer st.Unlock()
-
- const yaml = `name: gadget
-type: gadget
-version: 1
-`
-
- info, err := snap.InfoFromSnapYaml([]byte(yaml))
- c.Assert(err, IsNil)
-
- var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
- return info, nil, nil
- }
- restore := snapstate.MockOpenSnapFile(openSnapFile)
- defer restore()
-
- st.Unlock()
- err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{})
- st.Lock()
- c.Check(err, ErrorMatches, "cannot install a gadget snap on classic")
-}
-
func (s *checkSnapSuite) TestCheckSnapErrorOnDevModeDisallowed(c *C) {
const yaml = `name: hello
version: 1.10
diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go
index ac2829bc29..e92b291077 100644
--- a/overlord/snapstate/export_test.go
+++ b/overlord/snapstate/export_test.go
@@ -84,6 +84,12 @@ func MockReadInfo(mock func(name string, si *snap.SideInfo) (*snap.Info, error))
return func() { readInfo = old }
}
+func MockLastUbuntuCoreTransitionAttempt(m *SnapManager, last time.Time) (restorer func()) {
+ orig := m.lastUbuntuCoreTransitionAttempt
+ m.lastUbuntuCoreTransitionAttempt = last
+ return func() { m.lastUbuntuCoreTransitionAttempt = orig }
+}
+
func MockOpenSnapFile(mock func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error)) (restore func()) {
prevOpenSnapFile := openSnapFile
openSnapFile = mock
diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go
index 660cdb573b..3024841ab3 100644
--- a/overlord/snapstate/snapmgr.go
+++ b/overlord/snapstate/snapmgr.go
@@ -80,6 +80,8 @@ type SnapManager struct {
refreshRandomness time.Duration
lastRefreshAttempt time.Time
+ lastUbuntuCoreTransitionAttempt time.Time
+
runner *state.TaskRunner
}
@@ -425,7 +427,7 @@ func (m *SnapManager) blockedTask(cand *state.Task, running []*state.Task) bool
return false
}
-var CanAutoRefresh func(st *state.State) bool
+var CanAutoRefresh func(st *state.State) (bool, error)
// ensureRefreshes ensures that we refresh all installed snaps periodically
func (m *SnapManager) ensureRefreshes() error {
@@ -433,9 +435,12 @@ func (m *SnapManager) ensureRefreshes() error {
defer m.state.Unlock()
// see if it even makes sense to try to refresh
- if CanAutoRefresh == nil || !CanAutoRefresh(m.state) {
+ if CanAutoRefresh == nil {
return nil
}
+ if ok, err := CanAutoRefresh(m.state); err != nil || !ok {
+ return err
+ }
tr := config.NewTransaction(m.state)
@@ -537,6 +542,23 @@ func (m *SnapManager) ensureUbuntuCoreTransition() error {
}
}
+ // ensure we limit the retries in case something goes wrong
+ var retryCount int
+ err = m.state.Get("ubuntu-core-transition-retry", &retryCount)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ if retryCount > 5 {
+ // limit amount of retries
+ return nil
+ }
+ now := time.Now()
+ if !m.lastUbuntuCoreTransitionAttempt.IsZero() && m.lastUbuntuCoreTransitionAttempt.Add(12*time.Hour).After(now) {
+ return nil
+ }
+ m.lastUbuntuCoreTransitionAttempt = now
+ m.state.Set("ubuntu-core-transition-retry", retryCount+1)
+
tss, err := TransitionCore(m.state, "ubuntu-core", "core")
if err != nil {
return err
diff --git a/overlord/snapstate/snapmgr_test.go b/overlord/snapstate/snapmgr_test.go
index 1986852000..2cbd87ef39 100644
--- a/overlord/snapstate/snapmgr_test.go
+++ b/overlord/snapstate/snapmgr_test.go
@@ -38,6 +38,7 @@ import (
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/snapstate/backend"
"github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/store"
@@ -3862,7 +3863,7 @@ func (s *snapmgrTestSuite) verifyRefreshLast(c *C) {
func (s *snapmgrTestSuite) TestEnsureRefreshesNoUpdate(c *C) {
s.state.Lock()
defer s.state.Unlock()
- snapstate.CanAutoRefresh = func(*state.State) bool { return true }
+ snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
tr := config.NewTransaction(s.state)
tr.Set("core", "refresh.last", time.Time{})
@@ -3881,7 +3882,7 @@ func (s *snapmgrTestSuite) TestEnsureRefreshesNoUpdate(c *C) {
func (s *snapmgrTestSuite) TestEnsureRefreshesWithUpdate(c *C) {
s.state.Lock()
defer s.state.Unlock()
- snapstate.CanAutoRefresh = func(*state.State) bool { return true }
+ snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
tr := config.NewTransaction(s.state)
tr.Set("core", "refresh.last", time.Time{})
@@ -3913,7 +3914,7 @@ func (s *snapmgrTestSuite) TestEnsureRefreshesWithUpdate(c *C) {
func (s *snapmgrTestSuite) TestEnsureRefreshDisabled(c *C) {
s.state.Lock()
defer s.state.Unlock()
- snapstate.CanAutoRefresh = func(*state.State) bool { return true }
+ snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
// can only be disabled in debug mode
oldEnv := os.Getenv("SNAPD_DEBUG")
@@ -3954,7 +3955,7 @@ func (s *snapmgrTestSuite) TestEnsureRefreshDisabled(c *C) {
func (s *snapmgrTestSuite) TestEnsureRefreshesWithUpdateError(c *C) {
s.state.Lock()
defer s.state.Unlock()
- snapstate.CanAutoRefresh = func(*state.State) bool { return true }
+ snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
tr := config.NewTransaction(s.state)
tr.Set("core", "refresh.last", time.Time{})
@@ -3995,7 +3996,7 @@ func (s *snapmgrTestSuite) TestEnsureRefreshesWithUpdateError(c *C) {
func (s *snapmgrTestSuite) TestEnsureRefreshesInFlight(c *C) {
s.state.Lock()
defer s.state.Unlock()
- snapstate.CanAutoRefresh = func(*state.State) bool { return true }
+ snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
tr := config.NewTransaction(s.state)
tr.Set("core", "refresh.last", time.Time{})
@@ -4026,7 +4027,7 @@ func (s *snapmgrTestSuite) TestEnsureRefreshesInFlight(c *C) {
func (s *snapmgrTestSuite) TestEnsureRefreshesWithUpdateStoreError(c *C) {
s.state.Lock()
defer s.state.Unlock()
- snapstate.CanAutoRefresh = func(*state.State) bool { return true }
+ snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil }
tr := config.NewTransaction(s.state)
tr.Set("core", "refresh.last", time.Time{})
@@ -5142,8 +5143,12 @@ volumes:
`
func (s *snapmgrTestSuite) prepareGadget(c *C) {
- gadgetSideInfo := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}
- gadgetInfo := snaptest.MockSnap(c, "name: the-gadget\nversion: 1.0", "", gadgetSideInfo)
+ gadgetSideInfo := &snap.SideInfo{RealName: "the-gadget", SnapID: "the-gadget-id", Revision: snap.R(1)}
+ gadgetInfo := snaptest.MockSnap(c, `
+name: the-gadget
+type: gadget
+version: 1.0
+`, "", gadgetSideInfo)
err := ioutil.WriteFile(filepath.Join(gadgetInfo.MountDir(), "meta/gadget.yaml"), []byte(gadgetYaml), 0600)
c.Assert(err, IsNil)
@@ -5157,9 +5162,14 @@ func (s *snapmgrTestSuite) prepareGadget(c *C) {
}
func (s *snapmgrTestSuite) TestGadgetDefaults(c *C) {
+ r := release.MockOnClassic(false)
+ defer r()
dirs.SetRootDir(c.MkDir())
defer dirs.SetRootDir("")
+ // using MockSnap, we want to read the bits on disk
+ snapstate.MockReadInfo(snap.ReadInfo)
+
s.state.Lock()
defer s.state.Unlock()
@@ -5182,6 +5192,9 @@ func (s *snapmgrTestSuite) TestGadgetDefaultsInstalled(c *C) {
dirs.SetRootDir(c.MkDir())
defer dirs.SetRootDir("")
+ // using MockSnap, we want to read the bits on disk
+ snapstate.MockReadInfo(snap.ReadInfo)
+
s.state.Lock()
defer s.state.Unlock()
@@ -5540,6 +5553,46 @@ func (s *snapmgrTestSuite) TestTransitionCoreStartsAutomatically(c *C) {
c.Check(s.state.Changes()[0].Kind(), Equals, "transition-ubuntu-core")
}
+func (s *snapmgrTestSuite) TestTransitionCoreBackoffWorks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "ubuntu-core", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "corecore", SnapID: "core-snap-id", Revision: snap.R(1)}},
+ Current: snap.R(1),
+ SnapType: "os",
+ })
+ s.state.Set("ubuntu-core-transition-retry", 6)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Check(s.state.Changes(), HasLen, 0)
+}
+
+func (s *snapmgrTestSuite) TestTransitionCoreTimeLimitWorks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "ubuntu-core", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "corecore", SnapID: "core-snap-id", Revision: snap.R(1)}},
+ Current: snap.R(1),
+ SnapType: "os",
+ })
+ snapstate.MockLastUbuntuCoreTransitionAttempt(s.snapmgr, time.Now().Add(-6*time.Hour))
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle()
+ s.state.Lock()
+
+ c.Check(s.state.Changes(), HasLen, 0)
+}
+
func (s *snapmgrTestSuite) TestTransitionCoreNoOtherChanges(c *C) {
s.state.Lock()
defer s.state.Unlock()
diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go
index 663909d4c6..44d2c37cc3 100644
--- a/overlord/snapstate/snapstate.go
+++ b/overlord/snapstate/snapstate.go
@@ -189,12 +189,13 @@ func doInstall(st *state.State, snapst *SnapState, snapsup *SnapSetup) (*state.T
var defaults map[string]interface{}
if !snapst.HasCurrent() && snapsup.SideInfo != nil && snapsup.SideInfo.SnapID != "" {
+ // FIXME: this doesn't work during seeding itself
gadget, err := GadgetInfo(st)
if err != nil && err != state.ErrNoState {
return nil, err
}
if err == nil {
- gadgetInfo, err := snap.ReadGadgetInfo(gadget)
+ gadgetInfo, err := snap.ReadGadgetInfo(gadget, release.OnClassic)
if err != nil {
return nil, err
}
diff --git a/packaging/ubuntu-14.04/changelog b/packaging/ubuntu-14.04/changelog
index ea812a6038..ec2c5d9b8b 100644
--- a/packaging/ubuntu-14.04/changelog
+++ b/packaging/ubuntu-14.04/changelog
@@ -5,6 +5,14 @@ snapd (2.23~14.04) UNRELEASED; urgency=medium
-- Michael Vogt <michael.vogt@ubuntu.com> Mon, 30 Jan 2017 15:04:26 +0100
+snapd (2.22.3~14.04) trusty; urgency=medium
+
+ * New bugfix release, LP: #1665729:
+ - Limit the number of retries for the ubuntu-core -> core
+ transition to fix possible store overload.
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 17 Feb 2017 18:58:34 +0100
+
snapd (2.22.2~14.04) trusty; urgency=medium
* New upstream release, LP: #1659522
diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog
index d9c4f0b0fa..66ef020a4a 100644
--- a/packaging/ubuntu-16.04/changelog
+++ b/packaging/ubuntu-16.04/changelog
@@ -5,6 +5,14 @@ snapd (2.23) UNRELEASED; urgency=medium
-- Michael Vogt <michael.vogt@ubuntu.com> Mon, 30 Jan 2017 14:42:46 +0100
+snapd (2.22.3) xenial; urgency=medium
+
+ * New bugfix release, LP: #1665729:
+ - Limit the number of retries for the ubuntu-core -> core
+ transition to fix possible store overload.
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 17 Feb 2017 18:58:34 +0100
+
snapd (2.22.2) xenial; urgency=medium
* New upstream release, LP: #1659522
diff --git a/snap/gadget.go b/snap/gadget.go
index 30cd1dc1e5..0143bd2ba7 100644
--- a/snap/gadget.go
+++ b/snap/gadget.go
@@ -22,6 +22,7 @@ package snap
import (
"fmt"
"io/ioutil"
+ "os"
"path/filepath"
"gopkg.in/yaml.v2"
@@ -68,20 +69,38 @@ type VolumeContent struct {
Unpack bool `yaml:"unpack"`
}
-func ReadGadgetInfo(info *Info) (*GadgetInfo, error) {
+// ReadGadgetInfo reads the gadget specific metadata from gadget.yaml
+// in the snap. classic set to true means classic rules apply,
+// i.e. content/presence of gadget.yaml is fully optional.
+func ReadGadgetInfo(info *Info, classic bool) (*GadgetInfo, error) {
const errorFormat = "cannot read gadget snap details: %s"
+ if info.Type != TypeGadget {
+ return nil, fmt.Errorf(errorFormat, "not a gadget snap")
+ }
+
+ var gi GadgetInfo
+
gadgetYamlFn := filepath.Join(info.MountDir(), "meta", "gadget.yaml")
gmeta, err := ioutil.ReadFile(gadgetYamlFn)
+ if classic && os.IsNotExist(err) {
+ // gadget.yaml is optional for classic gadgets
+ return &gi, nil
+ }
if err != nil {
return nil, fmt.Errorf(errorFormat, err)
}
- var gi GadgetInfo
if err := yaml.Unmarshal(gmeta, &gi); err != nil {
return nil, fmt.Errorf(errorFormat, err)
}
+ if classic && len(gi.Volumes) == 0 {
+ // volumes can be left out on classic
+ // can still specify defaults though
+ return &gi, nil
+ }
+
// basic validation
foundBootloader := false
for _, v := range gi.Volumes {
diff --git a/snap/gadget_test.go b/snap/gadget_test.go
index a979b1ba3f..72032075c7 100644
--- a/snap/gadget_test.go
+++ b/snap/gadget_test.go
@@ -69,6 +69,12 @@ volumes:
unpack: false
`)
+var mockClassicGadgetYaml = []byte(`
+defaults:
+ core:
+ something: true
+`)
+
var mockGadgetSnapContents = "SNAP"
func (s *gadgetYamlTestSuite) SetUpTest(c *C) {
@@ -79,18 +85,57 @@ func (s *gadgetYamlTestSuite) TearDownTest(c *C) {
dirs.SetRootDir("/")
}
+func (s *gadgetYamlTestSuite) TestReadGadgetNotAGadget(c *C) {
+ info := snaptest.MockInfo(c, `
+name: other
+`, &snap.SideInfo{Revision: snap.R(42)})
+ _, err := snap.ReadGadgetInfo(info, false)
+ c.Assert(err, ErrorMatches, "cannot read gadget snap details: not a gadget snap")
+}
+
func (s *gadgetYamlTestSuite) TestReadGadgetYamlMissing(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
- _, err := snap.ReadGadgetInfo(info)
+ _, err := snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, ".*meta/gadget.yaml: no such file or directory")
}
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlOnClassicOptional(c *C) {
+ info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
+ gi, err := snap.ReadGadgetInfo(info, true)
+ c.Assert(err, IsNil)
+ c.Check(gi, NotNil)
+}
+
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlOnClassicEmptyIsValid(c *C) {
+ info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
+ err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), nil, 0644)
+ c.Assert(err, IsNil)
+
+ ginfo, err := snap.ReadGadgetInfo(info, true)
+ c.Assert(err, IsNil)
+ c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{})
+}
+
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlOnClassicOnylDefaultsIsValid(c *C) {
+ info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
+ err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockClassicGadgetYaml, 0644)
+ c.Assert(err, IsNil)
+
+ ginfo, err := snap.ReadGadgetInfo(info, true)
+ c.Assert(err, IsNil)
+ c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{
+ Defaults: map[string]map[string]interface{}{
+ "core": {"something": true},
+ },
+ })
+}
+
func (s *gadgetYamlTestSuite) TestReadGadgetYamlValid(c *C) {
info := snaptest.MockSnap(c, mockGadgetSnapYaml, mockGadgetSnapContents, &snap.SideInfo{Revision: snap.R(42)})
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYaml, 0644)
c.Assert(err, IsNil)
- ginfo, err := snap.ReadGadgetInfo(info)
+ ginfo, err := snap.ReadGadgetInfo(info, false)
c.Assert(err, IsNil)
c.Assert(ginfo, DeepEquals, &snap.GadgetInfo{
Defaults: map[string]map[string]interface{}{
@@ -142,7 +187,7 @@ volumes:
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644)
c.Assert(err, IsNil)
- _, err = snap.ReadGadgetInfo(info)
+ _, err = snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader cannot be empty")
}
@@ -157,7 +202,7 @@ volumes:
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYamlBroken, 0644)
c.Assert(err, IsNil)
- _, err = snap.ReadGadgetInfo(info)
+ _, err = snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader must be either grub or u-boot")
}
@@ -167,6 +212,6 @@ func (s *gadgetYamlTestSuite) TestReadGadgetYamlMissingBootloader(c *C) {
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), nil, 0644)
c.Assert(err, IsNil)
- _, err = snap.ReadGadgetInfo(info)
+ _, err = snap.ReadGadgetInfo(info, false)
c.Assert(err, ErrorMatches, "cannot read gadget snap details: bootloader not declared in any volume")
}
diff --git a/store/store.go b/store/store.go
index 0bb62f2653..86650846fc 100644
--- a/store/store.go
+++ b/store/store.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2016 Canonical Ltd
+ * Copyright (C) 2014-2017 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
@@ -497,19 +497,20 @@ func refreshDischarges(user *auth.UserState) ([]string, error) {
// refreshUser will refresh user discharge macaroon and update state
func (s *Store) refreshUser(user *auth.UserState) error {
+ if s.authContext == nil {
+ return fmt.Errorf("user credentials need to be refreshed but update in place only supported in snapd")
+ }
newDischarges, err := refreshDischarges(user)
if err != nil {
return err
}
- if s.authContext != nil {
- curUser, err := s.authContext.UpdateUserAuth(user, newDischarges)
- if err != nil {
- return err
- }
- // update in place
- *user = *curUser
+ curUser, err := s.authContext.UpdateUserAuth(user, newDischarges)
+ if err != nil {
+ return err
}
+ // update in place
+ *user = *curUser
return nil
}
diff --git a/tests/lib/assertions/developer1-my-classic-w-gadget.model b/tests/lib/assertions/developer1-my-classic-w-gadget.model
new file mode 100644
index 0000000000..774e30133a
--- /dev/null
+++ b/tests/lib/assertions/developer1-my-classic-w-gadget.model
@@ -0,0 +1,20 @@
+type: model
+authority-id: developer1
+series: 16
+brand-id: developer1
+model: my-classic-w-gadget
+classic: true
+gadget: classic-gadget
+timestamp: 2017-02-07T15:41:09+00:00
+sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+
+AcLBUgQAAQoABgUCWJn09gAAK1oQACjPbQXpVURQcfNpQfsiPNVMu1kmwvCibFDoI7TuAyuHlfM8
+X2QlW/kQwuqTDJ4RpHD5sSJsuPYN12QUBckPX7RdNFdeAsToejuFkDyyLQxWhqynFIaf2fnctyxx
+x4CnT8IoGH/JbAlAHhMCdS26MoXbeNlFtONbeH952xTIoTnfcQkXPHdkoBfqDa0/4Vd5ihxOc7xI
+CFooaSdQjlfSN0zpT2Z41mJVix0w3XFxXaMKsFgkt30zMSwSGCQNBP5Dn4/To14JsEXyonVRpzi5
+omEsQH7WlugXqNc1vw0Nhk5dDpNvlUMmvv89Azs/wpW8ex8xfBOtSE3oNkB3EG6YpkU8xclzRSRE
+5FaeSF0usZFm8p4jP40gPr7ScOyTRWwjB6GHs3tP0lgdX91hVwFUfYK1UxHOWgNK+zbZPGvscavY
+aYFMVn3m68MeeGfqJMgr88SsOwRC4Xn6fDs+F+RMOd5+QeMVG7J+RojpmiT3pPaPuPvrgF4VRNSz
+GUkh1gaN6v/VrPlpwTH6HXGbLDo5OwXxRz+RmGlk5cPrnSCfVkr79RyLw3i7ebhlhI8fWU7of43p
+QNeBkkWIAF4TZrYrpLM/XOkjfNn4MXDtcyyvBKc47mvnUDvY+OuGctzXx61HuZmUNz9k0mySKWgS
+LA4HD9QoduCufllURfUcJrhe6WxL
diff --git a/tests/lib/assertions/developer1-my-classic.model b/tests/lib/assertions/developer1-my-classic.model
new file mode 100644
index 0000000000..c0922e1771
--- /dev/null
+++ b/tests/lib/assertions/developer1-my-classic.model
@@ -0,0 +1,19 @@
+type: model
+authority-id: developer1
+series: 16
+brand-id: developer1
+model: my-classic
+classic: true
+timestamp: 2017-02-07T15:41:09+00:00
+sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+
+AcLBUgQAAQoABgUCWJnq8wAAN3IQABb3sWbLyF9hBR/x5BaQLHpd2QCwmnkSsZwrJ5inTzSMfYuf
+5fwxhsAsNDmtupK5SsCIRqGyWIW/3ERHnELNJs2z/ghYY7GYfB/QR4KiJgQd/JXB6WqU5VoJ/qHP
+tezkApd/nAh1UdyRPYfTlJg9C3tOHtrtODL4spbwSmqsOz1D8UiKdeut45qu38kWyugMXWP7Rdh7
+E2ai2fo7I67Dh4UU6iZXYSIVINzmgbjoXjy+li/o3YcSol2V/Hfq5KUHhpgQYFQxOfB1jaur7mFJ
+/n6SSk+4I/DkOxKqDgpWuPHavRF5PKS840P2Eww9aVGiEg6V36lxZSzs+p1nDaJyVEoEKk7Txmxi
+NnmY49IfoFxZ377I8U7Q4/+ruHqhOmgoOP1MGDVK9fDjf0iCtP5X1dF6VqwrmPM+nx66sxs+yPRl
+DcBqOvF+tAzvLYjuLXrl9FNRFwnedqI8kk2ZBR2ueWog7h+uqew7IV378Nrk7R3KVTtzNldoos2n
+DQaSJzZXb8XwJDA4xjCnqyFCEVOASUTyJ656XOnzWyFNk2ivjc8ML5Ztqd7NcgVCJlojVxUgZYps
+zuQCl7eHqB7xzbWO2FIpOsMFreCAfLUBHjr/pnX4oY/8sRq1bt8BZfT8t8D8GvLwQ0G1UDxK19BG
+xCBz7A+6hsw7sZmpGnhGnrZEqtuh
diff --git a/tests/lib/changes.sh b/tests/lib/changes.sh
new file mode 100755
index 0000000000..7974f6e9ef
--- /dev/null
+++ b/tests/lib/changes.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+change_id() {
+ # takes <summary pattern> [<status>]
+ local SUMMARY_PAT=$1
+ local STATUS=${2:-}
+ snap changes|grep -o -P "^\d+(?= *${STATUS}.*${SUMMARY_PAT}.*)"
+}
diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh
index e522704fa4..1be320e70b 100755
--- a/tests/lib/reset.sh
+++ b/tests/lib/reset.sh
@@ -32,10 +32,13 @@ reset_classic() {
systemctl start $unit
done
fi
- systemctl start snapd.socket
- # wait for snapd listening
- while ! printf "GET / HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket; do sleep 0.5; done
+ if [ "$1" != "--keep-stopped" ]; then
+ systemctl start snapd.socket
+
+ # wait for snapd listening
+ while ! printf "GET / HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket; do sleep 0.5; done
+ fi
}
reset_all_snap() {
@@ -58,7 +61,9 @@ reset_all_snap() {
rm -rf /var/lib/snapd/*
$(cd / && tar xzf $SPREAD_PATH/snapd-state.tar.gz)
rm -rf /root/.snap
- systemctl start snapd.service snapd.socket
+ if [ "$1" != "--keep-stopped" ]; then
+ systemctl start snapd.service snapd.socket
+ fi
}
if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then
diff --git a/tests/lib/snaps/classic-gadget/meta/gadget.yaml b/tests/lib/snaps/classic-gadget/meta/gadget.yaml
new file mode 100644
index 0000000000..9c4cdeddac
--- /dev/null
+++ b/tests/lib/snaps/classic-gadget/meta/gadget.yaml
@@ -0,0 +1 @@
+# on classic this can be empty (or absent) \ No newline at end of file
diff --git a/tests/lib/snaps/classic-gadget/meta/hooks/prepare-device b/tests/lib/snaps/classic-gadget/meta/hooks/prepare-device
new file mode 100755
index 0000000000..ac974b4d96
--- /dev/null
+++ b/tests/lib/snaps/classic-gadget/meta/hooks/prepare-device
@@ -0,0 +1,2 @@
+#!/bin/sh
+snapctl set device-service.url=http://localhost:11029
diff --git a/tests/lib/snaps/classic-gadget/meta/icon.png b/tests/lib/snaps/classic-gadget/meta/icon.png
new file mode 100644
index 0000000000..1ec92f1241
--- /dev/null
+++ b/tests/lib/snaps/classic-gadget/meta/icon.png
Binary files differ
diff --git a/tests/lib/snaps/classic-gadget/meta/snap.yaml b/tests/lib/snaps/classic-gadget/meta/snap.yaml
new file mode 100644
index 0000000000..fcc8537fc5
--- /dev/null
+++ b/tests/lib/snaps/classic-gadget/meta/snap.yaml
@@ -0,0 +1,4 @@
+name: classic-gadget
+type: gadget
+version: 1.0
+summary: Classic gadget
diff --git a/tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-down b/tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-down
new file mode 100755
index 0000000000..8446ee1dac
--- /dev/null
+++ b/tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-down
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+$SNAP/usr/bin/virsh destroy ping-unikernel
diff --git a/tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-up b/tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-up
new file mode 100755
index 0000000000..c3608c6fd3
--- /dev/null
+++ b/tests/lib/snaps/test-snapd-libvirt-consumer/bin/machine-up
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+$SNAP/usr/bin/virsh create /snap/test-snapd-libvirt-consumer/current/vm/ping-unikernel.xml
diff --git a/tests/lib/snaps/test-snapd-libvirt-consumer/snapcraft.yaml b/tests/lib/snaps/test-snapd-libvirt-consumer/snapcraft.yaml
new file mode 100644
index 0000000000..c115eb2182
--- /dev/null
+++ b/tests/lib/snaps/test-snapd-libvirt-consumer/snapcraft.yaml
@@ -0,0 +1,49 @@
+name: test-snapd-libvirt-consumer
+
+version: 1.0
+
+summary: Basic snap declaring a plug on the libvirt interface
+
+description: |
+ Apart from consuming the libvirt interface this snap packages a tiny vm
+ to be run with libvirt and be able to run some checks. The vm is a solo5
+ unikernel which is built during snap creation.
+
+ As a prerrequisite, a tap interface must be available for the vm to be
+ connected, the default name is tap100 and can be created with
+ ip tuntap add tap100 mode tap
+ ip addr add 10.0.0.1/24 dev tap100
+ ip link set dev tap100 up
+
+ With this in place, the vm would be accessible at 10.0.0.2
+
+ Once you execute machine-up, the vm will respond to ping requests.
+
+apps:
+ machine-up:
+ command: bin/machine-up
+ plugs: [libvirt]
+
+ machine-down:
+ command: bin/machine-down
+ plugs: [libvirt]
+
+parts:
+ unikernel:
+ plugin: make
+ source: https://github.com/fgimenez/solo5.git
+ source-type: git
+ build-packages:
+ - gcc
+ artifacts:
+ - tests/test_ping_serve/test_ping_serve.virtio
+ glue:
+ plugin: make
+ stage-packages:
+ - libvirt-bin
+ after: [unikernel]
+ source: .
+ snap:
+ - bin/
+ - vm/
+ - usr/
diff --git a/tests/lib/snaps/test-snapd-libvirt-consumer/vm/ping-unikernel.xml b/tests/lib/snaps/test-snapd-libvirt-consumer/vm/ping-unikernel.xml
new file mode 100644
index 0000000000..c02a5a55ef
--- /dev/null
+++ b/tests/lib/snaps/test-snapd-libvirt-consumer/vm/ping-unikernel.xml
@@ -0,0 +1,22 @@
+<domain type='qemu' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
+ <name>ping-unikernel</name>
+ <memory>128000</memory>
+ <os>
+ <type>hvm</type>
+ <kernel>/snap/test-snapd-libvirt-consumer/current/tests/test_ping_serve/test_ping_serve.virtio</kernel>
+ </os>
+ <features></features>
+ <serial type='stdio'>
+ <target port='0'/>
+ </serial>
+ <qemu:commandline>
+ <qemu:arg value='-device'/>
+ <qemu:arg value='virtio-net,netdev=n0'/>
+ <qemu:arg value='-netdev'/>
+ <qemu:arg value='tap,id=n0,ifname=tap100,script=no,downscript=no'/>
+ <qemu:arg value='-device'/>
+ <qemu:arg value='isa-debug-exit'/>
+ <qemu:arg value='-append'/>
+ <qemu:arg value='limit'/>
+ </qemu:commandline>
+</domain>
diff --git a/tests/main/classic-custom-device-reg/task.yaml b/tests/main/classic-custom-device-reg/task.yaml
new file mode 100644
index 0000000000..7093a29376
--- /dev/null
+++ b/tests/main/classic-custom-device-reg/task.yaml
@@ -0,0 +1,75 @@
+summary: |
+ Test gadget customized device initialisation and registration also on classic
+systems: [-ubuntu-core-16-*]
+environment:
+ SEED_DIR: /var/lib/snapd/seed
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ . $TESTSLIB/systemd.sh
+
+ snapbuild $TESTSLIB/snaps/classic-gadget .
+ snap download --$CORE_CHANNEL core
+
+ $TESTSLIB/reset.sh --keep-stopped
+ mkdir -p $SEED_DIR/snaps
+ mkdir -p $SEED_DIR/assertions
+ cat > $SEED_DIR/seed.yaml <<EOF
+ snaps:
+ - name: core
+ channel: $CORE_CHANNEL
+ file: core.snap
+ - name: classic-gadget
+ unasserted: true
+ file: classic-gadget.snap
+ EOF
+
+ echo Copy the needed assertions to /var/lib/snapd/
+ cp core_*.assert $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/developer1.account $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/developer1.account-key $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/developer1-my-classic-w-gadget.model $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/testrootorg-store.account-key $SEED_DIR/assertions
+ echo Copy the needed snaps to $SEED_DIR/snaps
+ cp ./core_*.snap $SEED_DIR/snaps/core.snap
+ cp ./classic-gadget_1.0_all.snap $SEED_DIR/snaps/classic-gadget.snap
+ # start fake device svc
+ systemd_create_and_start_unit fakedevicesvc "$(which fakedevicesvc) localhost:11029"
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ . $TESTSLIB/systemd.sh
+ systemctl stop snapd.service snapd.socket
+ systemd_stop_and_destroy_unit fakedevicesvc
+
+ rm -r $SEED_DIR
+ rm -f *.snap
+ rm -f *.assert
+ systemctl start snapd.socket snapd.service
+kill-timeout: 3m
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+
+ # kick seeding
+ systemctl start snapd.service snapd.socket
+
+ echo "Wait for seeding to be done"
+ while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done
+ echo "We have a model assertion"
+ snap known model|grep "model: my-classic-w-gadget"
+
+ echo "Wait for device initialisation to be done"
+ while ! snap changes | grep -q "Done.*Initialize device"; do sleep 1; done
+
+ echo "Check we have a serial"
+ snap known serial|grep "authority-id: developer1"
+ snap known serial|grep "brand-id: developer1"
+ snap known serial|grep "model: my-classic-w-gadget"
+ snap known serial|grep "serial: 7777"
diff --git a/tests/main/classic-firstboot/task.yaml b/tests/main/classic-firstboot/task.yaml
new file mode 100644
index 0000000000..12c88ec5e0
--- /dev/null
+++ b/tests/main/classic-firstboot/task.yaml
@@ -0,0 +1,72 @@
+summary: Check that firstboot assertions are imported and snaps installed also on classic
+systems: [-ubuntu-core-16-*]
+environment:
+ SEED_DIR: /var/lib/snapd/seed
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+
+ snapbuild $TESTSLIB/snaps/basic .
+ snap download --$CORE_CHANNEL core
+
+ $TESTSLIB/reset.sh --keep-stopped
+ mkdir -p $SEED_DIR/snaps
+ mkdir -p $SEED_DIR/assertions
+ cat > $SEED_DIR/seed.yaml <<EOF
+ snaps:
+ - name: core
+ channel: $CORE_CHANNEL
+ file: core.snap
+ - name: basic
+ unasserted: true
+ file: basic.snap
+ EOF
+
+ echo Copy the needed assertions to /var/lib/snapd/
+ cp core_*.assert $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/developer1.account $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/developer1.account-key $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/developer1-my-classic.model $SEED_DIR/assertions
+ cp $TESTSLIB/assertions/testrootorg-store.account-key $SEED_DIR/assertions
+ echo Copy the needed snaps to $SEED_DIR/snaps
+ cp ./core_*.snap $SEED_DIR/snaps/core.snap
+ cp ./basic_1.0_all.snap $SEED_DIR/snaps/basic.snap
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+
+ rm -r $SEED_DIR
+ rm -f *.snap
+ rm -f *.assert
+ systemctl start snapd.socket snapd.service
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+
+ echo "Start the daemon with an empty state, this will make it import "
+ echo "assertions from the $SEED_DIR/assertions subdirectory and "
+ echo "install the seed snaps."
+ systemctl start snapd.socket snapd.service
+
+ echo "Wait for Seed change to be finished"
+ for i in `seq 120`; do
+ if snap list 2>/dev/null |grep -q -E "^basic" ; then
+ break
+ fi
+ sleep 1
+ done
+
+ echo "Verifying the imported assertions"
+ if ! snap known model|MATCH "model: my-classic" ; then
+ echo "Model assertion was not imported on firstboot"
+ exit 1
+ fi
+
+ snap list|grep -q -E "^basic"
+ test -f $SEED_DIR/snaps/basic.snap
diff --git a/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml b/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml
index 88ae77db91..00a12b277f 100644
--- a/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml
+++ b/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml
@@ -34,9 +34,10 @@ execute: |
systemctl stop snapd.service snapd.socket
systemctl start snapd.service snapd.socket
+ . $TESTSLIB/changes.sh
while ! snap changes|grep ".*Done.*Transition ubuntu-core to core"; do
snap changes
- snap change 3||true
+ snap change $(change_id "Transition ubuntu-core to core")||true
sleep 1
done
diff --git a/tests/main/classic-ubuntu-core-transition/task.yaml b/tests/main/classic-ubuntu-core-transition/task.yaml
index 5e0ca22402..84c66e1b87 100644
--- a/tests/main/classic-ubuntu-core-transition/task.yaml
+++ b/tests/main/classic-ubuntu-core-transition/task.yaml
@@ -39,9 +39,10 @@ execute: |
systemctl stop snapd.service snapd.socket
systemctl start snapd.service snapd.socket
+ . $TESTSLIB/changes.sh
while ! snap changes|grep ".*Done.*Transition ubuntu-core to core"; do
snap changes
- snap change 3||true
+ snap change $(change_id "Transition ubuntu-core to core")||true
sleep 1
done
diff --git a/tests/main/interfaces-libvirt/task.yaml b/tests/main/interfaces-libvirt/task.yaml
new file mode 100644
index 0000000000..9de818bab7
--- /dev/null
+++ b/tests/main/interfaces-libvirt/task.yaml
@@ -0,0 +1,84 @@
+summary: Ensure that the libvirt interface works.
+
+systems: [ubuntu-16.04-64]
+
+details: |
+ The libvirt interface allows a snap to access the libvirtd socket in order to manage
+ libvirt domains and other resources.
+
+ A snap which defines a libvirt plug must be shown in the interfaces list.
+ The plug must not be autoconnected on install and, as usual, must be able to be
+ reconnected.
+
+ A snap declaring a plug on this interface must be able to create and destroy a domain.
+ The test uses a snap that carries a unikernel built to be run on top of qemu, boot and
+ respond to ping. Once the domain is created, the test checks connectivity to the unikernel.
+
+prepare: |
+ echo "Given libvirt and qemu are installed"
+ sysctl -w net.ipv6.conf.all.disable_ipv6=1
+ trap "sysctl -w net.ipv6.conf.all.disable_ipv6=0" EXIT
+ apt install -y libvirt-bin qemu
+ # add test user to the libvirtd group
+ adduser test libvirtd
+
+ echo "And libvirt is configured to manage /dev/net/tun"
+ systemctl stop libvirtd.service || true
+ echo 'cgroup_device_acl = ["/dev/net/tun", "/dev/random", "/dev/urandom"]' | tee -a /etc/libvirt/qemu.conf
+
+ echo "And the required services up"
+ systemctl start libvirtd.service
+ systemctl start virtlogd.socket
+
+ echo "And a snap declaring a plug on the libvirt interface is installed"
+ snap install --edge test-snapd-libvirt-consumer
+
+ echo "And the required tap interface is in place"
+ ip tuntap add tap100 mode tap
+ ip addr add 10.0.0.1/24 dev tap100
+ ip link set dev tap100 up
+
+restore: |
+ apt autoremove -y --purge libvirt-bin qemu
+
+ ip link delete tap100
+
+ # remove test user from the libvirtd group
+ deluser test libvirtd
+
+execute: |
+ CONNECTED_PATTERN=":libvirt +test-snapd-libvirt-consumer"
+ DISCONNECTED_PATTERN="\- +test-snapd-libvirt-consumer:libvirt"
+
+ echo "The plug is not connected by default"
+ snap interfaces | MATCH "$DISCONNECTED_PATTERN"
+
+ echo "==================================="
+
+ echo "When the plug is connected"
+ snap connect test-snapd-libvirt-consumer:libvirt
+ snap interfaces | MATCH "$CONNECTED_PATTERN"
+
+ echo "Then the snap is able to create the unikernel domain"
+ su -l -c "test-snapd-libvirt-consumer.machine-up" test
+ virsh list | MATCH ping-unikernel
+
+ echo "And the unikernel is accesible"
+ ping -c 1 -q -W 1 10.0.0.2
+
+ echo "And the snap is able to destroy the unikernel domain"
+ su -l -c "test-snapd-libvirt-consumer.machine-down" test
+ virsh list | MATCH -v ping-unikernel
+
+ echo "==================================="
+
+ echo "When the plug is disconnected"
+ snap disconnect test-snapd-libvirt-consumer:libvirt
+ snap interfaces | MATCH "$DISCONNECTED_PATTERN"
+
+ echo "Then the snap is not able to create a domain"
+ if su -l -c "test-snapd-libvirt-consumer.machine-up 2>${PWD}/creation.error" test; then
+ echo "Expected permission error accessing libvirtd socket with disconnected plug"
+ exit 1
+ fi
+ cat creation.error | MATCH "Failed to connect socket to '/var/run/libvirt/libvirt-sock': Permission denied"
diff --git a/tests/main/refresh-all-undo/task.yaml b/tests/main/refresh-all-undo/task.yaml
index 4e5ec35dc4..5cd1d53b78 100644
--- a/tests/main/refresh-all-undo/task.yaml
+++ b/tests/main/refresh-all-undo/task.yaml
@@ -57,7 +57,10 @@ execute: |
echo "But the bad snap did not get updated"
snap list | MATCH -E "${BAD_SNAP}"| MATCH -v "fake"
+ . $TESTSLIB/changes.sh
+ chg_id=$(change_id "Refresh snap" Error)
+
echo "Verify the snap change"
- snap change 5 | MATCH "Undone.*Download snap \"${BAD_SNAP}\""
- snap change 5 | MATCH "Done.*Download snap \"${GOOD_SNAP}\""
- snap change 5 | MATCH "ERROR cannot verify snap \"test-snapd-tools\", no matching signatures found"
+ snap change $chg_id | MATCH "Undone.*Download snap \"${BAD_SNAP}\""
+ snap change $chg_id | MATCH "Done.*Download snap \"${GOOD_SNAP}\""
+ snap change $chg_id | MATCH "ERROR cannot verify snap \"test-snapd-tools\", no matching signatures found"
diff --git a/tests/regression/lp-1665004/task.yaml b/tests/regression/lp-1665004/task.yaml
new file mode 100644
index 0000000000..10cd16e558
--- /dev/null
+++ b/tests/regression/lp-1665004/task.yaml
@@ -0,0 +1,13 @@
+summary: ensure that /var/lib/snapd/hostfs is group-owned by root
+details: |
+ On a system that never ran any snap before the /var/lib/snapd/hostfs
+ directory does not exist. When snap-confine is used it will create the
+ directory on demand but that directory will retain the group identity of
+ the user.
+prepare: |
+ . $TESTSLIB/snaps.sh
+ install_local test-snapd-tools
+ rmdir /var/lib/snapd/hostfs
+execute: |
+ test-snapd-tools.cmd true
+ [ $(stat -c '%g' /var/lib/snapd/hostfs) -eq 0 ]